Documentation
¶
Overview ¶
Package claudehook manages Claude Code's multi-event hook for Quil.
Quil tracks Claude session-id rotation (/clear, compaction, /resume) and forwards Claude's lifecycle/notification hooks by registering a hook command via --settings (inline JSON) when it spawns a claude-code pane. The hook command invokes the quil daemon binary's `claude-hook` subcommand (see runhook.go + cmd/quild), which reads the hook JSON on stdin and writes the per-pane session id to $QUIL_HOME/sessions/<paneID>.id and/or appends a hookevents JSONL line to $QUIL_HOME/events/<paneID>.jsonl.
Invoking the native binary instead of a per-event shell script eliminates the dominant hook latency (PowerShell 5.1 cold start was ~1-4 s) and the hand-rolled JSON escaping the shell producers required.
This package never reads or writes ~/.claude/settings.json — the hook is per-invocation, lives entirely under QuilDir, and no user Claude config is mutated.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func BuildSettingsJSON ¶
BuildSettingsJSON returns the inline JSON string Quil passes to `claude --settings <json>`. Registers Quil's hook command under every entry in forwardedHookEvents — the native subcommand then branches on the hook_event_name field of the stdin JSON.
func HookCommand ¶
HookCommand returns the command Claude runs for each registered hook event. It invokes the running quil daemon binary's `claude-hook` subcommand, which reads the hook JSON on stdin and writes the session-id file / spool line natively. exePath must be the absolute path to the quild binary (the daemon passes os.Executable()); it is double-quoted so paths with spaces work under the shell Claude uses to run hook commands. The command is embedded in the --settings JSON via BuildSettingsJSON, which JSON-escapes it.
exePath is the daemon's own binary location — OS-controlled, never user input — so a shell-metacharacter break-out is not a realistic concern; a `"` is illegal in a Windows path and effectively never appears on Unix.
func ReadPersistedSessionID ¶
ReadPersistedSessionID returns just the session id from the pane's record.
No production caller needs it today — the daemon reads the whole record so it can use the transcript path — but it is kept as the package's id-only accessor, mirroring opencodehook.ReadPersistedSessionID, and it is the narrower contract most of the package's own tests assert against.
func RunHook ¶ added in v1.18.0
RunHook processes one Claude Code hook invocation. It reads the hook JSON from r and routes by hook_event_name:
- SessionStart writes the rotating session-id file (resume infrastructure)
- every other forwarded event appends one hookevents.Payload JSONL line to the pane's spool file, which the daemon's watcher picks up within 200 ms.
Best-effort by contract: an empty pane id is a no-op (Claude invoked outside Quil), and filesystem failures are logged to $QuilDir/claudehook/hook.log. It returns an error only so the subcommand and tests can observe failures; the subcommand always exits 0 so Claude is never blocked. nowMs is injected for deterministic tests (the subcommand passes time.Now().UnixMilli()).
Unlike the shell producers this replaces, the spool line is built with encoding/json — no hand-rolled escaping, no codepage/BOM hazard.
Types ¶
type HookEnv ¶ added in v1.18.0
type HookEnv struct {
PaneID string // QUIL_PANE_ID — empty means "invoked outside Quil" (no-op)
QuilDir string // resolved via QUIL_HOOK_HOME (QUIL_HOME fallback) — root for sessions/ and events/
Mode string // QUIL_HOOK_MODE: "default" | "verbose" | "off"
RecordHistory bool // QUIL_RECORD_HISTORY=1 — append full prompts to the history store
}
HookEnv carries the per-invocation context the hook needs, sourced from the QUIL_* environment the daemon sets on a claude-code pane at spawn.
type SessionRecord ¶ added in v1.46.2
SessionRecord is what the hook persists for a pane: the live Claude session id, plus the absolute path of that session's transcript.
The path is recorded because it cannot be derived. Claude keys a transcript's project directory off the session's OWN working directory, and an agent that moves into a git worktree moves the transcript with it — so reconstructing the path from the pane's spawn CWD looks in a directory the file was never in. TranscriptPath is empty for records written by an older Quil (and for a session whose SessionStart carried no path); callers must treat that as "unknown", never as "missing".
func ReadPersistedSession ¶ added in v1.46.2
func ReadPersistedSession(quilDir, paneID string) (SessionRecord, error)
ReadPersistedSession returns the session record the hook last wrote for the given pane. A missing file is reported via the returned error satisfying errors.Is(err, os.ErrNotExist) so callers can cleanly distinguish "no hook fired yet" from a corrupted file.
Reads the file via a single open file descriptor + Stat so the reported ModTime always corresponds to the bytes returned, even if the hook rotates the file between calls.