Writing policies
Policies are plain TOML, 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 = "checkout"
[[policies]]
name = "production-database-protection"
[policies.match]
actions = [ "database.delete", "database.drop" ]
environments = [ "production" ]
[policies.decision]
effect = "deny"
reason = "No AI-initiated destructive database operations in production."The rule above is the whole of it. What it refuses, and the reason a person reads when it fires, follow from those fields and nothing else.
The fields, in one paragraph
A rule is a match and a decision. Match takes the action, and optionally the target, the environment, the branch, the working directory, the agent or the role, a time window, the state in force, how the request sat against the task the session declared, and, locally only, the call's own arguments. Decision takes an effect and, where it needs them, a reason, approvers, a quorum, a rate limit, and the alternative a refusal names.
An omitted field matches everything, which is the single most common source of a rule that fires more widely than intended. When several rules match, the most restrictive effect wins:
deny > ask > allowOrder in the file does not matter, and neither does which file: every rule file
the machine loads is evaluated together. One of them is not in any repository.
The denies on secret reads that setup writes are about this machine, since a key
in ~/.ssh is the same key from every checkout, so they live in
~/.memnox/machine.policies.toml and apply in every repository, where no
checkout can delete or move them.
Order in the file does not matter. Every field, with its type, default and the precedence rules in full, is on the policy file reference. The rest of this page is what the reference cannot tell you: which rules to write, and where they come from.
The one field that does the most work
[[policies]]
name = "secrets-not-required"
[policies.match]
actions = [ "filesystem.read" ]
targets = [ ".env" ]
[policies.decision]
effect = "deny"
reason = "This task declared no credential need."
[policies.decision.alternative]
action = "filesystem.read"
resource = ".env.example"
note = ".env.example is readable."alternative is what a denying rule permits instead. An agent told only no
abandons the task; one told what to use instead finishes it under constraint. It
is resolved from the rule rather than invented at the moment of refusal, which is
what makes redirection reliable enough to depend on.
Name one only where a substitute exists. A container socket has no example beside it, and sending an agent at a path that is not there is worse than telling it no.
Down to the argument
The same tool can be routine in one directory and refused in another:
[[policies]]
name = "no-recursive-delete-in-payments"
[policies.match]
actions = [ "shell.execute" ]
workingDirectories = [ "/srv/payments*" ]
arguments = { command = [ "*rm -rf*" ] }
[policies.decision]
effect = "deny"
reason = "Recursive delete is not an agent action here."No match is not approval
When no rule matches, the action is ungoverned, not endorsed, and the
configured default decides what happens to it. On a first install that default is
allow, so the runtime observes rather than refuses: a rule nobody has read yet
must not wedge an editor on minute one.
What changes with confidence is the mode rather than the default. memnox config set mode enforce, or memnox protect --enforce, is the step where recorded
verdicts start biting. See
From watching it to letting it run.
Lifecycle
memnox policy check # every rule file this machine loads
memnox policy check memnox.policies.toml # one file
memnox policy test 'gh pr merge 12' # what one action would get
memnox doctor --wiring # is anything gating, and on which rule setpolicy check exits non-zero when something will not parse, which is what lets
CI run it. doctor --wiring answers a different question, whether Memnox is
gating anything at all right now. Both are described in full in the
CLI reference.
Deciding on this machine
Sometimes the decision comes before anybody writes a file: this should always ask, this should never run, I have approved this enough times.
$memnox protect --ask <action...>$memnox protect --deny <action...>$memnox protect --allow <action...>On a machine enrolled in a workspace, each of these is also offered to the team on the next sync. It arrives as a proposal, never as a rule in force, and a second admin decides whether it becomes a team rule. The person who decided it is the owner of the machine that sent it, never a name the machine supplies, so a machine nobody owns offers nothing.
Publishing a set to a workspace
A rule file on one machine is that machine's business. A rule set published to a workspace binds every machine in it, so it is not published by the call that sends it:
Step
Proposed
Announced
Approved
Withdrawing your own draft needs nobody, because rejecting publishes nothing. Making somebody find a colleague to take back their own proposal is how a proposal gets left pending instead.
An approval counts in either place, the console or the conversation it was
announced in. The console has no setting for it; a script can still narrow it
to one of the two with PUT :ws/governance and approveIn. And
the second approver is only required where a second administrator exists, so a
team with one admin is not locked out of publishing, since publishing policy is
not a paid feature.
A rule remembers who decided it
A rule that says "ask first" and nothing else reads as the product being difficult, and the person it stops cannot find out that a colleague decided it on purpose. So a team rule can carry its source: which act it came from, who decided it and when, the decision in plain words, and once published, who approved it.
Three doors lead there, and all three end as a proposal a second admin approves:
a held call answered "always" on its chat message (see Approvals),
a rule somebody decided on an enrolled machine with protect, and a
recommendation from the approval history under Autonomy, in What you could
stop being asked about. Who decided is never a
caller's claim: it is the person signed in, or the owner of the machine that
sent it.
Machines show it with no change of their own, because the reason they already print in a terminal prompt, a hook refusal and an MCP refusal is composed from the source:
Team rule: <the decision>. Decided by <who> on <day>, approved by <who>.An approved rule with a source is also remembered as a decision, so what the team decided and why reaches the context agents are given as well as the gate.
Exceptions, with an owner and an end
A rule scoped to one repository used to be the only way to say "except here",
and a scoped rule has no owner, no reason and no end. An exception records one as
its own thing, proposed through POST :ws/policies/exceptions: the rule it bends, or an action
pattern to hold more strictly, whether it relaxes or tightens, where it applies
(a repository, an agent, or both), who answers for it and why, and the day it
stops. It stands for at most ninety days.
An exception is approved by a second admin like any rule set, under Waiting on you while it waits, and nowhere else once it is decided. It reaches machines as part of the rule bundle. When it expires the bundle is published again without it, so it ends on the machines too rather than only in the console.
Writing a good reason
The reason string is what a human reads at the moment they are refused, usually while irritated and in a hurry.
Bad: Denied by policy.
Good: Recursive delete is not an agent action here, ask #platform if you need it.
Where a starting set comes from
The runtime has no pack registry and nothing to install. Rules are generated from what is actually on your machine, which is the only starting point that is true about your machine. A rule that has to hold across forty machines is a different problem, and it is solved by publishing a set to a workspace rather than by installing a catalogue.
memnox protect --yes # a baseline from the scan
memnox protect --for gh # rules for one CLI, from its verb table
memnox protect --from-usage 30d # ask rules for what was never usedprotect --yes denies the sensitive paths it found, denies destructive commands,
puts write-capable MCP tools behind an ask, and allows reads. protect --for
narrows that to one thing: for an authenticated CLI it denies the credential
file and leaves the CLI working, which is the distinction that makes any of
this adoptable.
The verb tables
Nineteen CLIs have a table saying which of their subcommands read, which write
and which destroy: aws, gcloud, az, gh, kubectl, terraform, docker,
vercel, railway, fly, heroku, netlify, psql, mysql, mongosh,
npm, stripe, git, playwright.
memnox explain gh # the credential, the projects, the table, the ruleThe table explain prints is the one enforcement reads, so what the screen
promises is what the gate does.
Generated rules and your own are evaluated together under the same most-restrictive-wins semantics, so you never fork a generated rule to tighten it: add one beside it. To loosen one, edit it, and the diff shows what your team actually chose.

