Skip to content

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.

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.

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 + log

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-input note so the operator can repair the configuration.

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.