claudehook

package
v1.46.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 1, 2026 License: MIT Imports: 13 Imported by: 0

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

func BuildSettingsJSON(cmd string) (string, error)

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

func HookCommand(exePath string) string

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

func ReadPersistedSessionID(quilDir, paneID string) (id string, modTime time.Time, err error)

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

func RunHook(r io.Reader, env HookEnv, nowMs int64) error

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

type SessionRecord struct {
	ID             string
	TranscriptPath string
	ModTime        time.Time
}

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL