Policies — Allow, Hold, Block, and Conditions
Every tool call an AI agent makes is evaluated against your policy and resolved
to exactly one of three verdicts. This page documents that vocabulary, how a
rule's conditions narrow when it applies, and the day-to-day commands for
reading, testing, and shipping a policy change.
The three verdicts
| Verdict | What happens | Rule action field |
|---|---|---|
| PASS (allow) | The handler runs. No further gate involvement. | pass |
| HOLD (review) | Execution suspends. A human approves or denies via Slack, the CLI (mergen-server approve), or the dashboard. |
warn |
| BLOCK | The handler never runs. The agent gets a structured error — why and what to do instead — and can reformulate. | block |
A HOLD is not a slower BLOCK — it is a suspended Promise waiting on a human
decision, not a rejection. See docs/hitl-setup for wiring
up Slack approve/deny.
Nothing above the rule engine is allowed to invent a BLOCK or a PASS.
Confidence scores, corpus similarity, and behavioral signals may only escalate
a PASS to a HOLD — never the reverse, and never straight to BLOCK. See
docs/override-corpus for the one place human
decisions do promote into a live rule.
Reading the active policy
mergen-server policies # active rules, action, trigger counts
mergen-server policies --json # machine-readable
Immutable rules (the hard safety layer — "never terraform destroy regardless
of confidence") always run first and can't be edited or bypassed. Everything
below them is an editable rule your team owns.
Writing a rule
A rule is an action plus a conditions object that scopes when it applies.
Every field in conditions is ANDed together by default:
{
"id": "no-friday-migrations",
"name": "No schema migrations during Friday settlement",
"description": "Blast-radius: migrations queued behind the settlement job cause a multi-hour lock.",
"action": "block",
"reason": "Settlement window is Fri 16:00–20:00 UTC — see incident #12",
"conditions": {
"commands": ["alembic upgrade", "prisma migrate deploy", "rails db:migrate"],
"daysOfWeek": [5],
"hourWindow": [16, 20],
"environments": ["production"]
}
}
Condition fields
| Field | Narrows the rule to... |
|---|---|
commands / files |
Matching command patterns or touched file globs |
actorType |
ai, human, or all |
environments |
e.g. ['production'] |
repos |
e.g. ['acme/payments-api'] |
services |
Named services in your stack graph |
agentIds / roles |
Specific registered agents, or RBAC roles (viewer/responder/admin) |
branches |
Glob-matched git branches, e.g. ['main', 'release/*'] |
daysOfWeek / hourWindow |
A recurring time window, e.g. a Friday settlement freeze |
ciPipeline |
true restricts to CI-originated calls, false excludes them |
resourceGlobs / excludeResourceGlobs |
Narrows a broad command match (e.g. aws s3 rm) to specific resources, or carves out an exception |
requireCorpusMatch |
Only fire when the Override Corpus has recorded prior overrides for this incident tag |
anyOf / allOf / not |
Compose the fields above with OR / AND / NOT instead of the implicit AND |
Ship a rule the same way you ship any config change — through the signed settings file, never a hand-edited file an agent could also write to:
mergen-server config set K V --acknowledge-risk # guarded settings
or the policy file workflow below.
The policy-as-code loop
mergen-server policy-push # live server → .mergen/policy.json (commit this)
mergen-server policy-pull # .mergen/policy.json → live server
mergen-server policy-pull --merge # add new rules only, keep existing
mergen-server policy-pull --cloud # pull the org policy from mergen.app/governance/policies
mergen-server policy-diff # what would change before applying
mergen-server policy-simulate <file> # replay a candidate file against historical traffic
mergen-server policy-test [file.json] # {input, expectedVerdict} test cases — CI-usable, no server needed
mergen-server policy-versions # list restorable snapshots
mergen-server policy-rollback <versionId> # restore a prior snapshot (immutable rules always survive)
policy-simulate and policy-test are the two commands to run before
merging a policy change: simulate against real historical traffic to see what
would newly block or hold, and run the test suite in CI so a bad edit fails
the build instead of reaching a developer's machine.
Review — promoting an override into policy
When a human overrides a suggested action or approves a HOLD with context, that decision is recorded in the Override Corpus. It does not automatically become a live rule — promotion is an explicit step:
curl -X POST http://127.0.0.1:3000/overrides/:id/review \
-H 'x-mergen-secret: '"$(cat ~/.mergen/secret)" \
-d '{"promote": true}'
This is one of exactly four paths by which the corpus can affect the live
gate, and the only one that can produce something other than a HOLD (because a
human explicitly reviewed and approved it). The other three — auto-staged
proposals, requireCorpusMatch above, and MERGEN_CORPUS_HOLD — are all
HOLD-only. Full detail in docs/override-corpus.