DocsGovern AI agentsWriting policies

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.

yaml
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.
IdentityPolicyAdvisorsApprovalAudit
memnox.policies.yaml

Agent exports customer records

data.export · pii.eu → us

eu-data-stays-in-eu

EU personal data may not be exported outside the EU.

action
data.export
data
pii.eu
destination
us
verdictblocked

What every agent knows

rules in force
4
agents governed
12
decisions on record
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…

FinanceLegalregulated dataEngineeringOperations
allowedneeds a humanblocked

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

actions

The action verb, file.write, deploy.service, mcp.*

targets

The path, resource or service named

environments

production, staging, whatever you declared

branches

The git branch the work sits on

workingDirectories

Where the call was made

arguments

The call's own arguments, by name

agents

Which agent is asking

windows

Days and hours the rule applies in

When several policies match, the most restrictive effect wins:

block  >  require_approval  >  redact  >  allow

The four effects

yaml
  - 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:

yaml
  - 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

yaml
  - 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: 2

Grants 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

bash
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.yaml

simulate 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.