Fail-Open vs. Fail-Closed: the local execution gate's reliability posture
Mergen sits inline in front of the agent's shell commands and file edits. That makes reliability a first-class safety property: a gate that misbehaves must not silently stop enforcing (fail-open) and must not permanently brick a developer's terminal (fail-closed with no escape). This document states, precisely, what the gate does in every failure mode, and how an operator recovers from a broken gate.
TL;DR, in two halves that are routinely confused:
- Internal faults fail CLOSED. A rule that throws, a corrupt policy file, a command that will not parse. Each resolves to BLOCK or HOLD, never a silent PASS. This has always been true and still is.
- Only a gate that is not running at all can let a call through, and only on a machine no organization manages. There
MERGEN_GATE_AVAILABILITY_MODEdefaults tofail-open: when nothing answers, there is no verdict to enforce and the tool call proceeds unevaluated. Setfail-closedto block instead. A machine an organization manages always fails closed, whatever is set locally. A gate that is running but too slow (a timeout) or answers with an error blocks in every mode.There are therefore two paths on which a call can run without a real enforcement decision: the fail-open default above, on an unmanaged machine, and the bounded, operator-only, fully-audited panic bypass.
Principle
A fault must never become a SILENT pass. When the gate reaches a verdict but something inside it went wrong (evaluation errored, the policy file is corrupt, a command will not parse), it fails closed (BLOCK, or HOLD for human review). No probabilistic guardrails, no silent enforcement downgrade, even when Mergen itself is the thing that broke.
Availability is a separate question, and a configured one. If the gate cannot
be reached at all, there is no verdict to fail closed from.
MERGEN_GATE_AVAILABILITY_MODE decides what happens, and since 2026-09-19 it
defaults to fail-open: a stopped runtime, one nothing answers for, lets the call
through. Three things narrow that:
- An organization-managed machine fails closed. A machine signed in under an
organization, or one that has applied an organization's policy, blocks when the
gate cannot answer. A local
fail-opencannot loosen it. - A timeout is not an absent gate. A gate that is running but does not answer in time blocks, in every mode: it may be holding the call for a person.
- An error is not an absent gate. A gate that answers with an error (401, 413, 429, 500) blocks, in every mode, and says how to recover. Only "nothing is running" (connection refused, or 502/503/504 from a router whose worker is gone) takes the fail-open path.
That default is deliberate (a background daemon being down should not brick a
developer's every Bash call), and it is not silent. mergen doctor --security,
GET /coverage/reliability, and the startup banner all report it, and a stock
install reports overallPosture: advisory rather than hardened partly because
of it.
The word doing the work in the principle is silent. A documented, reported, one-setting-away allow path is a tradeoff a team can weigh. An undocumented one is a lie.
Behavior in every failure mode
| Failure | Behavior |
|---|---|
| Policy evaluation throws (malformed rule, unexpected input) | Fail-closed → BLOCK, logged, pipeline_block audit entry |
Gate heartbeat stale/dead (MERGEN_REQUIRE_GATE_HEARTBEAT) |
Fail-closed → BLOCK |
enterprise-policy.json fails HMAC / schema validation |
Fall back to the built-in default policy (still enforcing), policy_integrity_failure audit entry |
| Script/command can't be structurally parsed | Fail-closed → HOLD (obfuscation-as-signal, script-trust) |
| No local gate reachable at all (hook/CLI call, nothing running) | Organization-managed machine: fail-closed → BLOCK. Otherwise MERGEN_GATE_AVAILABILITY_MODE governs: fail-open by default (the call runs unevaluated); fail-closed blocks. Auto-start is attempted first on a clean unreachable |
| Gate running but does not answer in time | Fail-closed → BLOCK, every mode (it may be holding the call for a person) |
| Gate answers with an error (401, 413, 429, 500) | Fail-closed → BLOCK, every mode, with the way out |
| Operator activates the panic bypass | Fail-OPEN → PASS, every call panic_bypass-audited |
Two rows above can let a call through without an enforcement decision: the
unreachable-gate row under its fail-open default, and the panic bypass. Every
other row fails closed.
The two-command bypass, stated plainly
Under the default, an agent with shell access can run:
mergen-server stop && <anything>
and the second command executes unevaluated. Setting
MERGEN_GATE_AVAILABILITY_MODE=fail-closed closes it.
This is named here rather than left for a reader to derive, because a security
control's known bypass belongs in its own documentation. mergen-server stop does
leave a suppression marker, and gate-check keys on its presence rather than on
who wrote it (stoppedBy is derived from isTTY, which an agent can spoof with
a pseudo-TTY), but under fail-open that marker changes what gets reported, not
the verdict.
Choosing a mode
fail-closed: the stronger posture, and the recommended one for shared
machines, CI, and any deployment where the agent is less trusted than the
developer running it. An agent with write access to prod is the threat model: if
the gate stopped enforcing whenever it became unreachable, an attacker (or a
confused agent) would be incentivised to induce that state. Fail-closed removes
the incentive: breaking the gate stops work, it does not unlock it.
fail-open (default on a machine no organization manages): favours availability on a developer's own machine. The
judgement is that on a single-developer workstation, a Mergen process that died at
2am should not block every command until someone notices, and that the realistic
threat there is a mistaken agent rather than one deliberately engineering a
bypass. Auto-start (MERGEN_GATE_AUTOSTART) is what makes this recoverable in
practice: a clean unreachable respawns the server and evaluates against the fresh
gate rather than simply allowing.
The cost of fail-closed, and the fix
Fail-closed has a real failure mode of its own: a bad policy edit, or a gate bug, can BLOCK every command and halt the developer until Mergen is fixed or restarted. That is exactly what the panic bypass exists to release, without abandoning the audit trail.
The panic bypass
An operator-only, time-bounded, fully-audited liveness escape hatch.
While active, the gate passes every tool call without enforcement, but records
each one as a loud, hash-chained panic_bypass entry in the Agent Blunder Log
noting what the policy would have done.
mergen-server panic on --minutes 15 --reason "bad policy edit blocking all commands"
mergen-server panic status
mergen-server panic off
Ephemeral/containerised alternative (no persisted file):
MERGEN_PANIC_BYPASS_UNTIL=$(( $(date +%s%3N) + 900000 )) # epoch-ms, 15 min out
MERGEN_PANIC_BYPASS_REASON="incident-1234"
Guarantees
- Bounded. Every window has an expiry; the TTL is clamped to a hard maximum (4h) so it can never be left on indefinitely by mistake. An expired window reads as inactive and its stale file is cleaned up.
- Loud. Activation logs at error level. Every bypassed call is a
hash-chained
panic_bypassaudit entry, visible inGET /agent-blunders, the SIEM stream, and evidence-pack exports, so any action taken while enforcement was off is fully reviewable after the fact. - Checked first. The bypass is evaluated before the heartbeat fail-closed and every policy check, so it can un-brick a gate whose evaluation itself is the fault (the "would have" verdict is best-effort and wrapped: a broken evaluator can't re-brick the call it's meant to release).
- Fails closed on ambiguity. A missing or corrupt panic file reads as inactive (enforce), never active. Absence of a proven bypass means enforce.
Threat-model honesty
The panic bypass is not a security boundary against a same-user process: an agent with the developer's shell can already write the panic file or set the env var, exactly as it could kill the Mergen process. That is acceptable because the bypass hides nothing: its entire contract is "trade enforcement for liveness, temporarily, in exchange for a complete audit trail." Abuse is loud, not silent. A hard security boundary against a hostile same-user process requires OS-level privilege separation, which is out of scope for the Layer-1 local gate (see the Inside-Out progression in the project README).
Operator runbook: "the gate is blocking everything"
- Confirm it's the gate:
mergen-server panic status(is it already on?), check recentGET /agent-blundersfor a flood ofpipeline_block/policy_integrity_failure. - Release the developer:
mergen-server panic on --minutes 15 --reason "<what happened>". - Fix the root cause, usually a bad
~/.mergen/enterprise-policy.jsonedit (mergen-server policy-rollback) or a stale heartbeat (restart Mergen). - Re-enforce:
mergen-server panic off. - Review what got through while off:
GET /agent-blunders?type=panic_bypass.