Writing policies
Policies are plain YAML, reviewable, diffable, and enforced deterministically. They live in your repository, and they stay there: a rule set that can be changed over HTTP is one nobody can review in a diff.
version: 1
project: acme-checkout
policies:
- name: production-database-protection
match:
actions: ["database.delete", "database.drop"]
environments: ["production"]
decision:
effect: block
reason: No AI-initiated destructive database operations in production.Agent exports customer records
data.export · pii.eu → us
eu-data-stays-in-eu
EU personal data may not be exported outside the EU.
- data.export
- pii.eu
- us
What every agent knows
- 4
- 12
- 1,285
Policy verdict
matching rules…
Export to Drive
never ran
Email the file
never ran
Notify #legal
never ran
Record the decision
writing reason…
The rule above is four lines of YAML. Everything to the right of it, the work that did not happen and the reason that did get written down, follows from those four lines and nothing else.
Matching
All match fields take wildcard patterns (* matches anything). An omitted
field matches everything, which is the single most common source of a rule
that fires more widely than intended.
Field
actionstargetsenvironmentsbranchesworkingDirectoriesargumentsagentswindowsWhen several policies match, the most restrictive effect wins:
block > require_approval > redact > allowThe four effects
- name: mask-secrets-in-outbound-calls
match: { actions: ["mcp.*"] }
decision:
effect: redact # mask the secret, forward the call
- name: candidate-rule
match: { actions: ["deploy.*"] }
decision:
effect: block
mode: monitor # record what it would have done; do not apply it
- name: deploy-budget
match: { actions: ["deploy.*"] }
decision:
effect: allow
rateLimit: { max: 10, windowSeconds: 3600 } # the 11th in an hour blocks- redact masks secrets in the arguments and lets the call through. It needs an enforcement point that can rewrite the payload, the MCP firewall can; an editor hook, which only answers allow or deny, blocks instead. Masked text is re-scanned before it is accepted; a finding that survives blocks the call.
- mode: monitor rolls one rule out without enforcing it. The action proceeds and the audit event records the verdict it withheld.
- rateLimit is counted per agent and rule by the runtime, and only an action that actually proceeds spends a slot. It needs a running runtime, the local gate never counts.
Down to the argument
The same tool can be routine in one directory and refused in another:
- name: no-recursive-delete-in-payments
match:
actions: ["shell.execute", "mcp.run_shell"]
arguments:
command: ["*rm -rf*"]
workingDirectories: ["/srv/payments*"]
decision:
effect: block
reason: Recursive delete is not an agent action here.
- name: release-branches-need-a-human
match:
actions: ["shell.execute"]
arguments: { command: ["*git push*--force*"] }
branches: ["main", "release/*"]
decision:
effect: require_approval
approvers: ["eng-lead"]Every named argument must match. An argument the call does not carry matches only
the bare "*".
Quorum and time windows
- name: production-deploy-two-person
match:
actions: ["deploy.service"]
environments: ["production"]
windows:
- { days: [1,2,3,4,5], startHour: 17, endHour: 9 }
- { days: [0,6], startHour: 0, endHour: 24 }
decision:
effect: require_approval
approvers: ["eng-lead", "security"]
minApprovals: 2Grants accumulate until the quorum is met, one person counts once, and a single denial ends it.
Time windows do not break determinism: the instant is passed into evaluation rather than read from a clock inside the engine, so replaying an audit event with its recorded timestamp reproduces the same verdict.
The default effect
No policy matched means the configured default:
--default-effect allow(the default), monitor-first onboarding;--default-effect block, strict mode, where anything unnamed is refused.
Moving to block is the real hardening step, and it is a decision about how much
you trust your policy coverage. See
From observing to enforcing.
Lifecycle
memnox validate # is the file well formed
memnox reload # re-read it without restarting
memnox policy version # content hash of the current rule set
memnox policy simulate -f candidate.yamlsimulate replays your real audit history through a candidate rule set and
reports every decision that would differ, warning loudly wherever an action
becomes more permissive. Run it before every rule change that matters.
Writing a good reason
The reason string is what a human reads at the moment they are blocked, usually while irritated and in a hurry.
Bad: Blocked by policy.
Good: Recursive delete is not an agent action here, ask #platform if you need it.

