The gate
The pre-flight check that runs before every file write, edit, and shell command — the single decision point all agents share.
The gate is a small command-line program
(python -m steelmoth_runtime.operator_authority.gate) that every coding agent’s hook or
shim calls before it is allowed to write a file, edit one, or run a shell command. It reads
AGENT_POLICY.yaml and the worktree’s
scope manifest, and returns one of two answers: allow or deny.
How it’s called
Section titled “How it’s called”There are two equivalent invocation forms:
- Argument form — used by the Codex shim and the Morpheus
drain:
--tool=Write --path=…or--command=…. - Stdin form — used by Claude Code’s PreToolUse hook, which passes the tool input
as a JSON object on stdin (
--hook-input-stdin). An earlier configuration tried to read the input from environment variables that don’t exist, and silently failed — which is why the canonical hook uses the stdin form.
The decision flow
Section titled “The decision flow”Inside evaluate(), the gate works through a fixed sequence. The first stage is loading
the policy — and if that fails, the gate denies (a broken policy must not mean
“anything goes”).
load policy ──(fails)──▶ DENY + log "policy-load-failed" │ ▼ load the worktree's scope manifest │ ├── tool is Write / Edit ──────────────┐ │ ▼ │ no path given? ─▶ ALLOW (fail-open: misconfigured hook, log it) │ normalise the path to worktree-relative │ manifest grants this exact path? ─▶ ALLOW │ matches a protected glob? │ ├─ holds required token? ─▶ ALLOW │ ├─ approval-eligible? ─▶ ask the approval queue │ └─ otherwise ─▶ DENY + log │ not protected, but a code path needing task approval? │ └─ no task-approved token ─▶ DENY (submit a plan first) │ otherwise ─▶ ALLOW │ └── tool is Bash ───────────────────────┐ no command given? ─▶ ALLOW (fail-open: misconfigured hook, log it) command needs approval? ─▶ ask the approval queue command on the deny list? ├─ holds unlocking token? ─▶ ALLOW └─ otherwise ─▶ DENY + logThe path-normalisation trap
Section titled “The path-normalisation trap”The hook hands the gate an absolute file path, but the policy’s
protected globs are written repo-relative (e.g.
engineering-standards/**). The gate converts the absolute path to its worktree-relative
form before matching. Without that step the gate “fails open on every absolute-path write” — the
relative form would be denied while the identical absolute form sailed through. It is a small function
doing load-bearing work.
Fail-open vs fail-closed — chosen per case
Section titled “Fail-open vs fail-closed — chosen per case”The gate does not have one blanket failure posture; it picks the safe direction for each situation:
- Policy won’t load → deny. If the rulebook is unreadable, refuse everything.
- Malformed JSON on stdin → deny. A garbled hook payload could be a bug or an attack; refusing is safer.
- Empty input (no path / no command) → allow, and log it. This specific case means
the hook is misconfigured (the old env-var form passing an empty value). Denying here would
brick every Write/Edit until settings were fixed, so the gate allows but records a
hook-missing-inputnote so the operator can repair the configuration.
One gate, every agent
Section titled “One gate, every agent”The same program is the decision point for all coding agents — there is no parallel scope-check living in any agent’s own code, by rule. That is what makes the gate worth hardening: fix it once and every agent is covered. It is the write-time twin of the merge-time CODEOWNERS gate and the kernel-time wall of Landlock.

