Policy file
memnox.policies.toml, written by memnox protect and living in the repository
it governs. TOML is the format new files are written in; a memnox.policies.yaml
somebody already has is still read, and the shape is identical either way.
version = 1
project = "acme-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."Top level
versionnumber
projectstring
policieslist
Two repositories that declare the same project share one policy and memory
scope. This is the runtime's own scope, not the same record as a workspace in
the console. See Orgs, workspaces, memories.
A policy
namestring
descriptionstring
matchobject
decisionobject
match
Every field takes wildcard patterns (*), and an omitted field matches
everything. That default is the most common source of a rule that fires more
widely than intended.
Field
actionstargetsenvironmentsbranchesworkingDirectoriesagentsargumentswindowsrolesprincipalsmodels, providersdataClassifications, jurisdictionsaboveAmountscope, statearguments
[policies.match]
actions = [ "shell.execute" ]
arguments = { command = [ "*rm -rf*" ] }Every named argument must match. An argument the call does not carry matches only
the bare "*".
Evaluated in-process, inside the MCP proxy or the PATH wrapper, which already held the arguments because they sit in the path. Raw payloads never leave the machine and the record keeps a digest.
windows
[policies.match]
windows = [
{ days = [ 1, 2, 3, 4, 5 ], startHour = 17, endHour = 9 },
{ days = [ 0, 6 ], startHour = 0, endHour = 24 },
]days runs 0 to 6, where 0 is Sunday. Hours are on a 24-hour clock, and a
startHour greater than endHour wraps past midnight, so 17 to 9 means
"overnight". Several windows are OR-ed: the rule applies if the moment falls in
any of them.
The instant is passed into evaluation rather than read from a clock inside the engine, so replay reproduces the same verdict.
decision
effectenum
reasonstring
approverslist
minApprovalsnumber
modeenum
rateLimitobject
Precedence
When several policies match, the most restrictive effect wins:
deny > ask > allowOrder in the file does not matter. There is no "first match wins", because a rule set whose meaning depends on line order is one that breaks when somebody sorts it.
decision.alternative
[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."What the agent may use instead. action and note are required. note is the
sentence the agent actually reads, and "use something else" is not an instruction
anything can act on. It rides all the way into the MCP denial the client sees.
Name one only where a substitute exists. Why that matters, and how to choose one, is on Writing policies.
match.scope
[policies.match]
actions = [ "filesystem.read" ]
scope = [ "out_of_scope" ]How the request sat against the scope the session declared: in_scope,
out_of_scope, or undeclared. A rule naming no scope matches everything; a
request whose caller declared no task never matches a scope-bearing rule, because
undeclared is a silence rather than a guess.
match.state
[policies.match]
actions = [ "gh.pr-merge" ]
state = [ "freeze:payments" ]Applies only while one of the named state facts is in force. A fact is kind:subject,
so freeze:payments is what memnox freeze payments --for 2h declares. Facts are
handed to the gate rather than queried by it, so a freeze costs nothing to check,
and every one carries a mandatory expiry: a freeze that outlived its incident
would be worse than no freeze, because the next one gets ignored.
mode: observe
[policies.decision]
effect = "deny"
mode = "observe"The action proceeds and the recorded event carries the verdict it would have applied. This is how one rule is rolled out while the rest of the file enforces.
rateLimit
[policies.decision]
effect = "allow"
rateLimit = { max = 10, windowSeconds = 3600 }Counted per agent and per rule by the runtime. Only an action that actually proceeds spends a slot, and the local gate never counts, this needs a running runtime.
Quorum
[policies.decision]
effect = "ask"
approvers = [ "eng-lead", "security" ]
minApprovals = 2Grants accumulate until the quorum is met. One person counts once, and a single denial ends it.
Checking a file
memnox policy check # every rule file this machine loads
memnox policy check memnox.policies.toml
memnox policy test 'git push --force' # what one action would getpolicy check reads each file on its own, so one repository's broken file never
blanks another repository's rules, and it exits non-zero when something will not
parse. memnox doctor --wiring prints the content hash of the set in force, which
is how two machines are compared without diffing files.
A fuller example
version = 1
project = "acme-checkout"
[[policies]]
name = "no-recursive-delete-in-payments"
[policies.match]
actions = [ "shell.execute" ]
targets = [ "*rm -rf*" ]
workingDirectories = [ "/srv/payments*" ]
[policies.decision]
effect = "deny"
reason = "Recursive delete is not an agent action here, ask #platform."
[[policies]]
name = "release-branches-need-a-human"
[policies.match]
actions = [ "git.push-force" ]
branches = [ "main", "release/*" ]
[policies.decision]
effect = "ask"
approvers = [ "eng-lead" ]
reason = "A force-push can destroy work that exists nowhere else."
[[policies]]
name = "no-merging-while-frozen"
[policies.match]
actions = [ "gh.pr-merge" ]
state = [ "freeze:payments" ]
[policies.decision]
effect = "deny"
reason = "Payments is frozen."git.push-force is a separate action from git.push on purpose. Collapsing them
would make one rule about force-pushing deny every push, which is how a gate stops
being used. Every CLI with a verb table works the same way, and memnox explain <cli> prints the table enforcement reads.

