02RULES
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 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.
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:. Underunless:, 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. cdis 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, sodeny: sudoanddeny: awsboth catchsudo 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,-Rfand--recursive --force; notrm -r. Case is kept. - Force-push in every form:
deny: "git push --force"anddeny: "git push -f"both catch-f,-uf,--force-with-leaseand+branchrefspecs. - Unreadable code (
eval,$(...), backticks,python -c,curl … | sh,timeout/xargs/watchwrappers): inner commands are still checked, and the whole line is alsotouches: unverifiable.
How paths are matched#
~,$HOMEand..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. Underunless:, 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#
- Every rule file in force (rule set, custom rules, automated-run rules, company rules; Global and project) is combined into one decision.
- Deny beats ask beats allow. An allow can't override a deny or an ask.
- 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.
- Order doesn't matter.