Home › Learn › How Crossing Guard connects

How it works · 10 min read

How Crossing Guard connects to live Claude Code, Codex and OpenCode sessions

Crossing Guard puts one command in each runtime's hook configuration. The command passes each event to a daemon on your machine, waits a limited time for an answer, and hands the answer back to the runtime as a decision or as a note for the model. This page follows an event through those steps: what installation writes, how long each step may take, what happens when one fails, and how a note gets back into a session.

The four steps

  1. The runtime reaches one of its hook points: a prompt was submitted, a tool is about to run, or a tool has finished.
  2. A short-lived crossing-guard hook process reads the event from standard input and sends it to the daemon on your machine.
  3. The daemon stores the event, evaluates any rules that apply, and checks whether a note is waiting for this session.
  4. 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

~/.claude/settings.json (excerpt)
"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

RequestBudgetIf the budget runs out
Record an observation1.5 sThe tool runs; the event is kept in a local spool and sent later
Check stateful rules400 msThe tool runs
Ask a person to approve (Claude Code and Codex)55 sThe 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

FailureWhat happensWhy
Daemon not runningThe 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 approvalRecording an event shouldn't stop your tool call
Event can't be parsedThe call is allowed, with a note on standard errorA change in a vendor's format should leave one event unrecorded, not block work
Rules file present but brokenThe call is deniedA broken policy shouldn't quietly become no policy
Approval needed and nobody answersThe call is deniedSilence 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

  1. 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.
  2. 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.
  3. 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

RuntimeHow a note arrivesStatus
Claude CodeIn the hook reply, as additionalContextObserved
CodexBy default through Codex's queue subcommand, which skips the pending and expiry steps above. Delivery through the hook is an optionIn source
OpenCodeThe plugin adds the note through OpenCode's SDK without asking the model to replyIn 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