Skip to content
WARDEN DOCS Get Warden free
02RULES

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.

View .md

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.

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.

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:

EffectMeans
deny: (or block:)BLOCKED The action doesn't run.
ask:ASKS YOU A person decides. Held for review in runs nobody is watching.
allow:RUNS 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:

WriteMatches
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 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 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#