Skip to content
WARDEN DOCS Get Warden free
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.

View .md

Rule fields#

FieldRequiredValue
deny / block, ask, allowExactly oneWhat to match: a command string, a mapping of match keys, or "*" for any tool call
whenNoConditions that must all hold
unlessNoExceptions: the rule fires only if none holds
idNoA stable name, shown in Decisions, exceptions and tests
reasonNoShown 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.

KeyMatchesExample
command stringThe program (path stripped), then subcommand and operation words; other words must appear in the call. Extra arguments still match.deny: "git push --force"
domainThe host a call reaches. For a shell command, the first URL in it.{ domain: "*.pastebin.com" }
pathAny path the call names: file tool paths, path-like words, redirect targets, the files given to rm, cp, cat and similar.{ path: "infra/prod/*" }
toolThe tool name: Bash, Read, Write, Edit, WebFetch, WebSearch, Glob, Grep, MCP tool names. "*" is any tool.{ tool: WebFetch }
touchesOne meaning-level fact (below). One value per rule.{ touches: private_key }
containsA whole word anywhere in the call. The call is split into words of letters, digits and _, so pastebin.com is two words.{ contains: production }
secretUse 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.

ValueThe call…
dotenvReads or writes a .env file (not .env.example, .sample, .template or .dist), or runs a program through a dotenv loader
private_keyTouches a private key file
cloud_credsTouches cloud credential files
secret_literalWrites a secret value into a file
dotgitTouches a .git directory directly, rather than through git
ci_configTouches CI configuration: .github/workflows/, .gitlab-ci.yml, .circleci/, Jenkinsfile and others
package_installAdds a dependency (restoring from a lockfile doesn't count)
infra_changeChanges cloud or SaaS configuration with a CLI such as aws, terraform, kubectl or gcloud (read-only calls don't count)
pipes_to_shellPipes downloaded content into a shell
unverifiableCan't be read in advance: eval, $(...), backticks, python -c, a variable as the command name
outside_projectNames a path outside the project folder
temp_dirUses the system temp folder
warden_controlTouches Warden's own settings and rule files

Conditions and facts#

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

Commands#

KeyFact
commandThe program name
subcommand, operationThe 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
branchFor git push: the branch actually pushed, read from the real repo
git_push_modeforce for every forcing form (-f, +refspec, --force-with-lease)
git_current_branch, git_default_branch, git_push_to_defaultRepository state
cwdThe working directory

Files#

KeyFact
pathAny path the call names (see path matching below)
extensionThe file extension without the dot: pem, sql. A hidden file like .env gives env
accessread or write

Data, cloud and APIs#

KeyFact
envThe environment Warden works out from host names, profiles, paths or MCP arguments: prod, staging or dev
host, clientDatabase host and client
stmt, table, target_table, columnSQL statements and tables (select, update, delete, drop…)
nosql.op, nosql.collection, graphql.opNoSQL and GraphQL operations
method, http.host, http.pathHTTP requests
cloud.provider, cloud.service, cloud.actionCloud calls; cloud.action is read, mutate or destroy
mcp.tool, mcp.server, arg.<name>MCP tool, server, and any argument

Session#

KeyFact
trigger_modeinteractive (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_roleThe model doing the work, as provider/model
assurance_reached, assurance_target, assurance_meets_l0 … assurance_meets_l4The assurance level the change has earned ("true"/"false")
spec_bound, coverage_greenWhether 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:. 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.