The three heads have no idea which harness triggered them. Each harness's
own wire format and hook-registration mechanism gets normalized at the
edge, in cerberus guard and cerberus init; the rules, the policies,
and the tirith overlay all run once, unmodified, no matter where the call
came from. cerberus init wires whichever of these are actually
installed, each behind its own detection check, so running it on a
machine without a given harness doesn't touch that harness's files at
all:
- Claude Code —
~/.claude/settings.json, wired ifclaudeis on$PATH. The first harness cerberus supported, and still the shape its internal payload is built around. - Codex CLI —
~/.codex/hooks.json, wired ifcodexis on$PATH. Codex'sPreToolUse/SessionStarthooks use Claude's exact request/response shape (confirmed against OpenAI's own docs), soguard/healthrun unmodified here too. Codex's hooks are opt-in though:cerberus initalso sets[features] codex_hooks = truein~/.codex/config.toml, since hooks are documented to be silent no-ops without it. - Cursor —
~/.cursor/hooks.json, wired if a~/.cursordirectory exists.beforeShellExecution/beforeMCPExecutionsend a real payload shape and expect a real response vocabulary (permission, notpermissionDecision), soguardtranslates both directions throughcrates/cerberus/src/harnesses/cursor.rs, dispatched off the payload's ownhook_event_name, no--harnessflag needed. Cursor has no pre-write file hook, only the post-hocafterFileEdit, so this covers Bash and MCP calls only; Write/Edit/NotebookEdit aren't guardable there yet. - Hermes Agent —
~/.hermes/plugins/cerberus/, wired ifhermesis on$PATH. Hermes's best-documented integration point is an in-process Pythonpre_tool_callcallback rather than a subprocess given JSON on stdin, socerberus initwrites a small Python plugin (templates/hermes/) that shells out to the realcerberus guardbinary instead. It allowlists the handful of Hermes's ~86 tools cerberus actually guards (terminal,write_file,patch), mapped to theirBash/Write/Editnames. Its manifest follows Hermes's documented plugin-discovery format but hasn't been checked against the real binary;cerberus initsays so and points athermes doctor. - opencode —
~/.config/opencode/plugin/cerberus-guard.ts, wired ifopencodeis on$PATH. Same problem as Hermes, same fix: an in-processtool.execute.beforehook rather than a subprocess contract, so a plugin shells out tocerberus guardvia Bun's$. opencode has no per-tool matcher at all, so the plugin allowlistsbash/edit/write/webfetchitself, and remaps opencode's camelCase argument names (filePath) to the snake_case shape every rule expects. Checked end to end against the realcerberusbinary: a fakeediton a Claudesettings.jsoncorrectly tripped SANDBOX-003 through that remap.
More harnesses land as cerberus init learns to wire them.
guard is wired to the tools that change something or reach the network:
Bash | Write | Edit | NotebookEdit | WebFetch | mcp__.*
Read, Grep, Glob, and the rest are deliberately absent. Nothing they
do is worth denying, and keeping the hook off the hottest tools in a session
is what lets guard stay a subprocess-per-call design with no cache.
Guarding more than Bash isn't cosmetic. sandbox-integrity has always
blocked a shell command that edits the sandbox configuration in a Claude
settings.json; while the hook was Bash-only, the agent could reach the
exact same outcome by using the Edit tool instead of sed, and the guard
never saw the call.
The heads are not equally general. policy is tool-agnostic — cupcake gets
the hook payload verbatim. judgement sees every guarded tool. risk stays
Bash-only by design: tirith scans shell command strings, and there's
nothing meaningful to hand it for a Write or a WebFetch.