← All docs

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.

Last updated July 27, 2026