- Fix exit-code table: change incorrect codes 3/4 to correct values 124 (timeout) and 130 (interrupted), matching src/error.rs implementation - Add missing timeout flags to flags table: --first-output-timeout (90s), --stream-json-timeout (90s), --stop-hook-timeout (120s), matching src/cli.rs defaults - Add docs/notes/hook-design.md covering relay hook mechanics, FIFO protocol, and keeper fd pattern (from src/hook.rs, src/poller.rs) - Add docs/notes/terminal-probes.md covering Ink probe table and response bytes (from src/terminal.rs) Co-Authored-By: Claude <noreply@anthropic.com>
5 KiB
Hook Design
The Stop hook is the IPC mechanism between Claude Code and claude-print. When the AI completes a turn, Claude Code fires the Stop hook with a JSON payload containing session metadata; claude-print reads this from a FIFO to know when the response is ready.
Temp Directory Structure
Each claude-print invocation creates a per-run temp directory:
<TMPDIR>/claude-print-<pid>-<rand>/
├── settings.json # Relay Stop hook configuration
├── hook.sh # Hook script executed by Claude Code
└── stop.fifo # Named pipe for IPC
The temp dir is created with mode 0700 (owner-only) to prevent local users from reading the Stop payload (session ID, prompt text).
Relay Hook
The relay hook is a minimal Claude Code Stop hook that writes the Stop payload to the FIFO:
#!/bin/sh
cat > '<fifo-path>' 2>/dev/null || true
Key points:
- The FIFO path is embedded as a shell single-quoted string — no variable expansion at execution time
- If FIFO write fails, the hook exits cleanly (
|| true); Claude Code does not wait beyond the 10s timeout - The hook is executed by Claude Code via
--settings <temp>/settings.json
settings.json
The per-run settings file contains only the Stop relay hook:
{
"hooks": {
"Stop": [{
"hooks": [{"type": "command", "command": "<temp>/hook.sh", "timeout": 10}]
}]
}
}
Claude Code merges this with any user hooks from ~/.claude/settings.json, so both user hooks and the relay hook fire.
FIFO Protocol
The FIFO is a POSIX named pipe created with mkfifo(path, mode=0600):
- Writer: Claude Code executes
hook.sh, which opens the FIFO for writing and writes the JSON payload - Reader:
claude-printopens the FIFO for reading and blocks until data is available
Stop Hook Payload
All fields are optional for forward compatibility:
{
"hook_event_name": "Stop",
"session_id": "abc123",
"transcript_path": "/home/user/.claude/projects/home-user-myproject/abc123.jsonl",
"last_assistant_message": "Response text...",
"cwd": "/home/user/myproject"
}
Transcript Path Derivation
If transcript_path is absent from the payload, it is derived from session_id and cwd:
<HOME>/.claude/projects/<slug>/<session_id>.jsonl
Where <slug> is the cwd with leading / stripped and remaining / replaced with -:
/home/user/myproject → home-user-myproject
/tmp → tmp
Keeper FD Pattern
Linux FIFO semantics: open(O_WRONLY|O_NONBLOCK) returns ENXIO if no reader is present. To prevent the hook's cat > fifo from blocking or failing, claude-print uses a keeper write-end fd:
- Open FIFO read-end
O_RDONLY|O_NONBLOCK— always succeeds immediately - Open keeper write-end
O_WRONLY|O_NONBLOCK— succeeds because read-end is now open - Hold keeper write-end open until Stop fires
- When Stop fires, read the payload, then close the keeper write-end
- Claude Code's
cat > fifoopens its own write-end (simultaneous write-ends are valid)
Cleanup on Non-Stop Exit Paths
On exit paths where Stop never fires (SIGINT, timeout, child exit), the keeper write-end must be explicitly closed before waitpid:
- Closing the keeper causes any pending
cat > fifoinhook.shto receiveEPIPE/ENXIOand exit - Without this, the hook runner would hang indefinitely waiting for a reader that will never come
FIFO Poller States
UNOPENED
│ opened O_NONBLOCK at TRUST_DISMISSED → PROMPT_INJECTED transition
▼
OPEN_WAITING
│ FIFO becomes readable (Stop hook wrote payload)
▼
PAYLOAD_READ → DONE
The FIFO read-end is opened before the bracketed paste is injected (at the TRUST_DISMISSED → PROMPT_INJECTED transition), so Stop cannot fire before the reader is ready.
Hook Inheritance
Default Mode (inherit hooks)
By default, claude-print does not redirect CLAUDE_CONFIG_DIR. The inner claude process:
- Writes transcripts to
~/.claude/projects/<cwd-slug>/<session-id>.jsonl - Writes session entry to
~/.claude/sessions/<pid>.json - Appends to
~/.claude/history.jsonl - Fires all hooks in
~/.claude/settings.jsonalongside the relay hook
Isolation Mode (--no-inherit-hooks)
When --no-inherit-hooks is passed:
--setting-sources=(empty) is forwarded to claude — suppresses loading of standard settings sources- Only
--settings <temp>/settings.jsonis active — contains solely the relay hook - User hooks (SessionStart, Stop, PreToolUse, ccdash, trail-boss, etc.) do not fire
Use this mode for NEEDLE workers to prevent hook noise, or when user hooks have side effects.
Cleanup
The temp directory is cleaned up on all exit paths via tempfile::TempDir drop:
- Normal exit: Stop fires, payload read, cleanup completes
- Timeout: keeper write-end closed, child SIGTERM'd, cleanup runs
- SIGINT: interrupted flag set, poll loop breaks, cleanup runs
- Panic:
TempDirdestructor runs during unwind
Cleanup is idempotent — can be called multiple times safely. The FIFO is removed before the directory to avoid permission issues.