# Writing rules

The rule file format, from one line to a full policy, and the three ways to write it - the rule builder, plain English, or YAML.

A rule file is a short YAML list. Each rule says what to match and what to do: `deny`, `ask` or `allow`. Most rule files are under twenty lines.

```yaml title="A typical project's custom rules"
default: allow
rules:
  - deny: { touches: dotenv }
    id: team.no_env
    reason: "Never read .env files: ask a person for the value"
  - ask: "git push"
    when: { branch: [main, master] }
    reason: "Pushing to main needs a person"
  - deny: { domain: pastebin.com }
    reason: "No uploads to paste sites"
  - deny: { path: "infra/prod/*" }
    reason: "Production infrastructure is managed by the platform team"
  - deny: "rm -rf"
    unless: { path: "./*" }
    reason: "Recursive deletes stay inside the project"
```

## Three ways to write a rule

All three live in **Policies → Custom rules**, for Global or for one project.

1. **The rule builder.** **+ Add rule**: pick the effect, what it matches (a command, domain, path or tool), and a reason (*Why? Shown when the rule fires*). Warden verifies and saves it.
2. **Plain English.** **Author with AI**: *Describe a rule in plain English and Warden will draft, check, and add it here.* Warden writes the YAML and test cases, checks the rule compiles (*Compiles clean*), and runs the tests against the live engine (*2/2 checks passed*). Nothing is saved until you press **Add to policies**.
3. **YAML.** **Source** opens the file itself; **Save & compile** saves it.

> [!TIP] Test before you trust it
> **Policies → Tests** runs any command, file read or write, URL or tool call against your rules and shows the verdict and the rule behind it. Pin the result and it re-runs every time your rules change. See [Exceptions, tests and decisions](exceptions-and-tests#policy-tests).

## The parts of a rule

```yaml
  - deny: "kubectl"                 # effect: deny, ask or allow, and what to match
    when: { flag.context: "prod-*" } # optional: extra conditions, all must hold
    unless:                          # optional: exceptions, none may hold
      subcommand: [get, describe, logs]
    id: platform.kubectl_prod_readonly  # optional: a stable name for Decisions and tests
    reason: "Production clusters are read-only for agents"  # shown to you and the agent
```

### The effect

Exactly one of:

| Effect | Means |
|---|---|
| `deny:` (or `block:`) | {{deny}} The action doesn't run. |
| `ask:` | {{ask}} A person decides. Held for review in runs nobody is watching. |
| `allow:` | {{allow}} The action may run, unless a deny or ask rule also matches. |

**Deny always wins.** An `allow` rule can never carve an exception out of a `deny`. Use `unless:` on the deny rule instead. The order of rules in the file never changes a verdict.

### What it matches

The effect's value says what the rule is about:

| Write | Matches |
|---|---|
| `deny: "git push"` | A **command**: the program, then its subcommand. Extra arguments still match, so this covers `git push origin main`. |
| `deny: { domain: pastebin.com }` | A **web host** the call reaches. |
| `deny: { path: "infra/prod/*" }` | Any **file path** the call names, after resolving `~`, `..` and symlinks. |
| `deny: { tool: WebFetch }` | A **tool** by name: `Bash`, `Read`, `Write`, `Edit`, `WebFetch`, an MCP tool. `"*"` is any tool. |
| `deny: { touches: dotenv }` | Something the call **touches**, worked out by Warden: `.env` files, private keys, CI config and more. |
| `deny: { contains: production }` | A whole **word** anywhere in the call. |

The [rule reference](rule-reference) lists every key and every `touches` value.

### Conditions: `when` and `unless`

`when:` narrows a rule: every condition must hold. `unless:` makes exceptions: the rule fires only when none of them hold.

```yaml
rules:
  - deny: aws
    when: { flag.profile: prod }
    reason: "No AWS calls against the prod profile"
  - deny: { tool: WebFetch }
    unless: { domain: [github.com, "*.github.com", docs.python.org] }
    reason: "Web fetches only to the docs we trust"
```

A condition's value can be a single value, a glob (`"prod-*"`), or a list (any of them). Matching ignores case.

### `id` and `reason`

- **`reason`** is shown to you and to the agent when the rule fires. Write it for a person: say what to do instead. A blocked agent is also told not to retry or work around the rule.
- **`id`** names the rule in Decisions, exceptions and tests. Without one, Warden generates it. Set one if you will refer to the rule later.

### `default`

`default: allow` means anything no rule denies or asks about may run. That is how Warden's rule sets work: a deny list. `default: deny` turns your file into a strict allow-list, where a call runs only if a rule allows it. That is stricter than most people want: even reading a README needs an allow rule.

## Global and project

Rules are set **Global** (every project) or for **one project**. Both apply, combined into one decision. A project can make Global stricter, never looser: a deny in Global still wins in every project.

Rule files are kept by Warden on your Mac, not in your repository. To share rules with your team, use **Export bundle** in Policies (*Safe to commit to your repo*) and **Import bundle** on the other Macs, or [Warden Team](team) for company rules that sync to every seat.

## A few things to know

- **Quote `true`, `false`, `yes`, `no`, `on`, `off` and numbers** when they are values: `when: { assurance_meets_l2: "false" }`. Unquoted, YAML turns them into booleans.
- **`*` matches across `/`** in globs, so `infra/prod/*` covers everything below `infra/prod/`. `**` means the same as `*`.
- **A command rule matches every spelling of its flags.** `deny: "rm -rf"` also catches `rm -fr`, `rm -r -f` and `rm --recursive --force`, but not `rm -r`. Case is kept: `ls -R` is not `ls -r`.
- **A rule that can't be enforced isn't saved.** Verify & save, Author with AI and the Source view tell you which rule and why: a `touches` value Warden doesn't detect, a list where one value belongs, a word match under `unless:`. If a rule file is edited by hand and has an error, Warden fails closed: every action asks you (and is held in runs nobody is watching) until the file is fixed, and no exception can waive that.
- **For "outside the project", use `unless: { path: "./*" }`** or `when: { touches: outside_project }`. `path: "/*"` would match paths inside the project too.

## Next

:::cards
- [Rule reference](rule-reference) - Every match key, condition and fact.
- [Rule recipes](rule-recipes) - Ready-to-paste rules for common needs.
- [Rule sets and protections](rule-sets) - Open, Guarded and Hardened, and every ready-made protection.
:::
