# Rule reference

Every key a rule can match on, every touches value, every condition and fact, and exactly how commands and paths are matched.

## Rule fields

| Field | Required | Value |
|---|---|---|
| `deny` / `block`, `ask`, `allow` | Exactly one | What to match: a command string, a mapping of match keys, or `"*"` for any tool call |
| `when` | No | Conditions that must all hold |
| `unless` | No | Exceptions: the rule fires only if none holds |
| `id` | No | A stable name, shown in Decisions, exceptions and tests |
| `reason` | No | Shown to you and to the agent when the rule fires |

Top level: `default: allow` (anything not denied or asked about runs) or `default: deny` (a strict allow-list), and `rules:`.

## Match keys

Use these as the effect's value, as in `deny: { domain: pastebin.com }`, or in `when:` and `unless:`. Several keys in one mapping must all hold.

| Key | Matches | Example |
|---|---|---|
| command string | The program (path stripped), then subcommand and operation words; other words must appear in the call. Extra arguments still match. | `deny: "git push --force"` |
| `domain` | The host a call reaches. For a shell command, the first URL in it. | `{ domain: "*.pastebin.com" }` |
| `path` | Any path the call names: file tool paths, path-like words, redirect targets, the files given to `rm`, `cp`, `cat` and similar. | `{ path: "infra/prod/*" }` |
| `tool` | The tool name: `Bash`, `Read`, `Write`, `Edit`, `WebFetch`, `WebSearch`, `Glob`, `Grep`, MCP tool names. `"*"` is any tool. | `{ tool: WebFetch }` |
| `touches` | One meaning-level fact (below). One value per rule. | `{ touches: private_key }` |
| `contains` | A whole word anywhere in the call. The call is split into words of letters, digits and `_`, so `pastebin.com` is two words. | `{ contains: production }` |
| `secret` | Use of a brokered secret by name, or `"*"`. One value. | `{ secret: STRIPE_KEY }` |

## `touches` values

`touches` names what a call affects, worked out by Warden rather than read from the text. Hyphens and underscores both work.

| Value | The call… |
|---|---|
| `dotenv` | Reads or writes a `.env` file (not `.env.example`, `.sample`, `.template` or `.dist`), or runs a program through a dotenv loader |
| `private_key` | Touches a private key file |
| `cloud_creds` | Touches cloud credential files |
| `secret_literal` | Writes a secret value into a file |
| `dotgit` | Touches a `.git` directory directly, rather than through git |
| `ci_config` | Touches CI configuration: `.github/workflows/`, `.gitlab-ci.yml`, `.circleci/`, `Jenkinsfile` and others |
| `package_install` | Adds a dependency (restoring from a lockfile doesn't count) |
| `infra_change` | Changes cloud or SaaS configuration with a CLI such as `aws`, `terraform`, `kubectl` or `gcloud` (read-only calls don't count) |
| `pipes_to_shell` | Pipes downloaded content into a shell |
| `unverifiable` | Can't be read in advance: `eval`, `$(...)`, backticks, `python -c`, a variable as the command name |
| `outside_project` | Names a path outside the project folder |
| `temp_dir` | Uses the system temp folder |
| `warden_control` | Touches Warden's own settings and rule files |

## Conditions and facts

Any of these can appear in `when:` and `unless:`.

### Commands

| Key | Fact |
|---|---|
| `command` | The program name |
| `subcommand`, `operation` | The second and third words, when they read as verbs |
| `flag.<name>` | A flag's value: `--context prod`, `--context=prod`, `-h db1` |
| `env.<NAME>` | A leading `NAME=value` on the command |
| `branch` | For `git push`: the branch actually pushed, read from the real repo |
| `git_push_mode` | `force` for every forcing form (`-f`, `+refspec`, `--force-with-lease`) |
| `git_current_branch`, `git_default_branch`, `git_push_to_default` | Repository state |
| `cwd` | The working directory |

### Files

| Key | Fact |
|---|---|
| `path` | Any path the call names (see path matching below) |
| `extension` | The file extension without the dot: `pem`, `sql`. A hidden file like `.env` gives `env` |
| `access` | `read` or `write` |

### Data, cloud and APIs

| Key | Fact |
|---|---|
| `env` | The environment Warden works out from host names, profiles, paths or MCP arguments: `prod`, `staging` or `dev` |
| `host`, `client` | Database host and client |
| `stmt`, `table`, `target_table`, `column` | SQL statements and tables (`select`, `update`, `delete`, `drop`…) |
| `nosql.op`, `nosql.collection`, `graphql.op` | NoSQL and GraphQL operations |
| `method`, `http.host`, `http.path` | HTTP requests |
| `cloud.provider`, `cloud.service`, `cloud.action` | Cloud calls; `cloud.action` is `read`, `mutate` or `destroy` |
| `mcp.tool`, `mcp.server`, `arg.<name>` | MCP tool, server, and any argument |

### Session

| Key | Fact |
|---|---|
| `trigger_mode` | `interactive` (a person started it) or `automated` (automations, Factory, assurance checks). Set when the session starts; the agent can't change it. |
| `model_route`, `model_provider`, `model_in_use`, `model_role` | The model doing the work, as `provider/model` |
| `assurance_reached`, `assurance_target`, `assurance_meets_l0` … `assurance_meets_l4` | The [assurance level](assurance) the change has earned (`"true"`/`"false"`) |
| `spec_bound`, `coverage_green` | Whether the work is bound to a spec, and whether its checks are linked and passing |

The Software Factory adds its own facts for its moves (`factory_transition`, `factory_lane`…). See [Write your own Factory rule](factory-lanes#write-your-own-factory-rule).

## How values match

- **Single value**: exact match, ignoring case.
- **Glob**: a value containing `*`, `?` or `[`. `*` also matches `/`.
- **List**: matches if any item matches.
- **A missing fact** never satisfies `when:`. Under `unless:`, a missing fact means the exception doesn't hold, so the rule fires. Rules fail closed.

## How commands are matched

- **Chains are split.** `&&`, `||`, `;`, pipes, subshells and new lines: each simple command is judged on its own, and if any is denied the whole line is.
- **`cd` is followed**, so later relative paths resolve from the new directory.
- **Wrappers are unwrapped**: `sudo`, `doas`, `env`, `nohup`, `nice`, `time`, `exec`, `command`. Both the wrapper and the inner command are judged, so `deny: sudo` and `deny: aws` both catch `sudo aws s3 rm …`.
- **Shells are read**: `bash -c "…"`, `sh -c`, and heredocs fed to a shell are parsed command by command.
- **Flags are normalised**: `deny: "rm -rf"` matches `-fr`, `-r -f`, `-Rf` and `--recursive --force`; not `rm -r`. Case is kept.
- **Force-push in every form**: `deny: "git push --force"` and `deny: "git push -f"` both catch `-f`, `-uf`, `--force-with-lease` and `+branch` refspecs.
- **Unreadable code** (`eval`, `$(...)`, backticks, `python -c`, `curl … | sh`, `timeout`/`xargs`/`watch` wrappers): inner commands are still checked, and the whole line is also `touches: unverifiable`.

## How paths are matched

- `~`, `$HOME` and `..` are resolved and symlinks are followed; each path is also tried relative to the project.
- Under `when:`, the condition holds if **any** path the call names matches. Under `unless:`, the exception holds only if **every** path is exempt. A call that names no path is never exempt.
- `unless: { path: "./*" }` means *inside the project*.

## How rules combine

1. Every rule file in force (rule set, custom rules, automated-run rules, company rules; Global and project) is combined into one decision.
2. **Deny beats ask beats allow.** An allow can't override a deny or an ask.
3. When an ask fires, Warden checks again without ask rules: a deny behind it still wins, so approving an ask can't approve a deny.
4. Order doesn't matter.
