The four steps
- The runtime reaches one of its hook points: a prompt was submitted, a tool is about to run, or a tool has finished.
- A short-lived
crossing-guard hookprocess reads the event from standard input and sends it to the daemon on your machine. - The daemon stores the event, evaluates any rules that apply, and checks whether a note is waiting for this session.
- The hook prints the decision or the added context in the runtime's own format, and exits.
The hooks tutorial writes steps 1 and 4 by hand. Crossing Guard adds steps 2 and 3.
What installation writes
For Claude Code, installation adds one entry per event to ~/.claude/settings.json: prompt submission, stop, before and after each tool call (including failed calls), session start and end, notifications that ask for your permission or input, subagent stop, and compaction. Every entry runs the same command, and turn events add --observe with the kind of observation they record. In source
"PreToolUse": [{ "matcher": "*", "hooks": [{ "type": "command",
"command": "\"/path/to/crossing-guard\" hook --runtime claude" }] }],
"UserPromptSubmit": [{ "hooks": [{ "type": "command",
"command": "\"/path/to/crossing-guard\" hook --runtime claude --observe turn.started" }] }]
Codex gets the same command in ~/.codex/hooks.json (or $CODEX_HOME/hooks.json) for its own set of events. There's no tool matcher, a permission-request event takes the place of notifications, and there's no failed-tool event. In source Codex runs each entry only after you trust it with /hooks. Vendor docs
OpenCode gets one plugin file in its plugins directory. The plugin runs the same executable before and after each tool call. When your OpenCode version matches the one we verified, it turns a deny into a thrown error, and otherwise it only records. In source
These are user-level settings, so desktop sessions pick them up too. On our install, nearly every recent Claude desktop session produced live hook events with no setup per session. Observed
From hook to daemon
The hook process finds the daemon through two files in the Crossing Guard data directory. One holds the address the daemon listens on, and the other holds an access token that only your user account can read. Every request carries that token. The requests that can carry a note back also carry a deadline: the moment the hook will stop waiting for an answer. In source
The deadline lets the daemon decide whether there's still time to add a note to its reply before the hook gives up. On this path the hook talks only to the address the daemon published, which is loopback (127.0.0.1) by default. In source Traffic between an agent and its own model provider is separate, and the runtime controls it.
Time budgets
The model's next action waits for the hook, so each kind of request has its own time limit: In source
| Request | Budget | If the budget runs out |
|---|---|---|
| Record an observation | 1.5 s | The tool runs; the event is kept in a local spool and sent later |
| Check stateful rules | 400 ms | The tool runs |
| Ask a person to approve (Claude Code and Codex) | 55 s | The call is denied |
The vendors allow far longer. Claude Code's default timeout for a command hook is 600 seconds, Vendor docs so these budgets are Crossing Guard's choice.
When something fails
| Failure | What happens | Why |
|---|---|---|
| Daemon not running | The event goes to the local spool and is replayed later. Rules evaluated locally still apply, so the tool runs unless one of them denies it or needs approval | Recording an event shouldn't stop your tool call |
| Event can't be parsed | The call is allowed, with a note on standard error | A change in a vendor's format should leave one event unrecorded, not block work |
| Rules file present but broken | The call is denied | A broken policy shouldn't quietly become no policy |
| Approval needed and nobody answers | The call is denied | Silence isn't approval |
These are the Claude Code and Codex behaviours. OpenCode's plugin allows the call when something inside it fails, and denies anything that would need approval. In source
Sessions you start and sessions it starts
A session you start yourself, from a desktop app, a terminal or an IDE, reaches Crossing Guard through the hooks above. A session Crossing Guard starts for you, such as a helper agent, runs the same runtime and fires the same installed hooks, plus Crossing Guard's own approval bridge. Helpers get the same session signals and delivery path either way, and are never told who launched the session. In source
Hooks don't carry everything. The model's own replies are read back from the runtime's session files on disk, and for Claude Code so is the context a hook added. The transcript path in a hook event tells Crossing Guard which file to read again. In source
Sending a note to a session
A hook can speak only when the runtime asks it something. A note for a session, such as a recollection from Spontaneous Memory, waits in the daemon until that session's next suitable hook call. For Claude Code and OpenCode, and for Codex when it's set to use its hooks, that works like this: In source
- The note is stored as pending for that session, with an expiry time and a limit on how many can wait. If no hook call comes before it expires, it's dropped rather than delivered late.
- When the session next submits a prompt or makes an allowed tool call, the daemon claims the note, provided enough of the hook's deadline is left to write the reply. That's our configuration; the shipped default also uses tool results.
- The note goes back in the hook's reply, and the hook prints it as added context.
A denied tool call never carries a note. A note is claimed when the reply is prepared, returned to the queue if writing the reply fails, and counted as handed off only after the write succeeds. In source Claude Code sessions receive these notes today, and we've matched delivered notes to the transcripts that contain them. Observed
| Runtime | How a note arrives | Status |
|---|---|---|
| Claude Code | In the hook reply, as additionalContext | Observed |
| Codex | By default through Codex's queue subcommand, which skips the pending and expiry steps above. Delivery through the hook is an option | In source |
| OpenCode | The plugin adds the note through OpenCode's SDK without asking the model to reply | In source |
What we haven't verified
- Notes reaching Codex and OpenCode sessions. The code is there, but we haven't seen it on a live session.
- Codex hooks other than
PreToolUse. All of them are registered, but only that one has fired on our install. - Linux and Windows. Everything here ran on macOS. Not verified
Vendor documentation: Claude Code hooks · Codex hooks · OpenCode plugins
Last verified 2026-09-25 · review by 2026-10-25