hook

package
v0.8.1 Latest Latest
Warning

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

Go to latest
Published: Sep 20, 2026 License: MIT Imports: 34 Imported by: 0

Documentation

Overview

Package hook implements the three Claude Code lifecycle hooks of plan 6.3 — `brigade hook session-start`, `brigade hook prompt` and `brigade hook session-end` — the processes Claude Code runs from plugin/hooks/hooks.json with a JSON document on stdin and the session's environment (6.5).

The hooks are the only code that reads the CLAUDE_PLUGIN_OPTION_* values, resolves the profile and adapter (D36), registers the Brigade session and writes the by-pid and by-native maps every other command reads (3.2), and they are the only legitimate producer of the detached watcher's environment (6.6): the messaging token reaches the watcher ONLY through the environment the hook builds, is never written to a file (its SHA-256 in the pidfile is the one derived form), never put on argv, never logged and never handed to an adapter child.

Every subcommand exits 0 on every failure, with one stderr diagnostic through the redacting logger and, where useful, a context line on stdout: a non-zero exit from the prompt hook would block and erase the user's prompt, and SessionStart/SessionEnd cannot block at all (6.3). Nothing a hook prints carries a remote string unsanitised: team and session names go through protocol.SanitizeAttribute (quotes, angle brackets and newlines dropped, 64 code points), because hook stdout is attached to the user's own turn with no harness preamble.

Inside a session every inherited BRIGADE_* variable is ignored (U-27): the configuration comes from the options, the by-pid map and the CLAUDE_* facts alone, all read through internal/harness/config and adapterkit.Getenv.

Every side effect is injectable through Deps — the clock, the watcher Spawner, the registry fs.FS, the settings reader, the adapter spawn seam, the process-facts lookup and the PATH search — so the tests run hermetically against the fake adapter, a fake registry, a temp XDG triple and a reaped sleeper as CLAUDE_PID.

Index

Constants

View Source
const (
	SubSessionStart = "session-start"
	SubPrompt       = "prompt"
	SubSessionEnd   = "session-end"
)

The three subcommands (6.3, hooks.json).

View Source
const (

	// DefaultPidfileWait is how long the hook waits for a freshly spawned
	// watcher to write its pidfile (6.6) before it logs and moves on; the
	// next prompt respawns a watcher that never appeared.
	DefaultPidfileWait = 2 * time.Second

	// OutputCap is the 10,000-character hook-output cap of 6.3: frames
	// are printed until it would be exceeded, and only printed frames are
	// acknowledged.
	OutputCap = 10000
)

The hook's budgets. The per-call adapter budgets are adapterclient's 4.1 constants; these bound the whole hook run under the timeouts hooks.json declares (60 s, 5 s, 5 s — the SessionEnd budget is really 1.5 s shared across every SessionEnd hook, E0-5 (h)).

Variables

This section is empty.

Functions

func Run

func Run(args []string, streams cli.Streams, environ []string, deps Deps) int

Run executes one `brigade hook <subcommand>` invocation and returns the process exit status. args excludes the words `brigade hook`; the only flag is --log-level. environ is the process environment in os.Environ form. Every subcommand returns 0 on every failure (6.3); the one non-zero answer is `usage` (2) for a missing or unknown subcommand or flag, which no hooks.json ever produces.

Types

type Deps

type Deps struct {
	// Now is the clock for every timestamp the hook records (map times,
	// the prune stamp, the retry stamp) and for the pipeline's rate
	// window; nil means time.Now. Real waits (for a pidfile) use the
	// system clock regardless.
	Now func() time.Time
	// Spawner starts the watcher; nil means RealSpawner.
	Spawner Spawner
	// Registry is the fs.FS the session registry is read from; nil means
	// registry.Dir(config.ClaudeConfigDir(environ)) per run.
	Registry fs.FS
	// ReadFile reads the three settings files of the native scan (6.10),
	// the candidate settings files of the doing-rules scan (card 25, plan
	// 5.2) and, at SessionStart, the frame_file option's file (P5-12);
	// nil means os.ReadFile.
	ReadFile func(string) ([]byte, error)
	// Spawn is the adapter request/response seam (adapterclient.Client.
	// Spawn); nil means adapterkit.Spawn, a real child.
	Spawn adapterclient.SpawnFunc
	// Lookup reads process facts for the pidfile guard; nil means
	// procutil.Lookup.
	Lookup pidfile.LookupFunc
	// LookPath searches a PATH value for an executable name (the
	// shadowing check of 6.2); nil means the package's own search.
	LookPath func(pathVar, name string) (string, bool)
	// Sink, when set, is passed to the watcher as `--sink <file>` (6.6).
	// Only a test harness sets it; production never does.
	Sink string
	// PidfileWait bounds the wait for a spawned watcher's pidfile; zero
	// means DefaultPidfileWait.
	PidfileWait time.Duration
	// PromptBudget bounds the whole prompt-hook run; zero means
	// promptBudget. Only a test lowers it, to reach the doing reminder's
	// budget guard (plan 5.4) without a real 3.5 s stall.
	PromptBudget time.Duration
}

Deps are the injectable side effects of a hook run. RealDeps returns the production set; a zero field means the production default, so a test overrides only what it needs.

func RealDeps

func RealDeps() Deps

RealDeps returns the production dependencies.

type RealSpawner

type RealSpawner struct{}

RealSpawner starts the detached watcher for real (6.6): stdin from the null device, stdout and stderr to the log file, its own session and process group (Setsid), no controlling terminal, the given environment and nothing else; started, released, never waited for.

func (RealSpawner) Spawn

func (RealSpawner) Spawn(ctx context.Context, spec SpawnSpec) (int, error)

Spawn implements Spawner.

type SpawnSpec

type SpawnSpec struct {
	// Args is the argv after the program name: "watch", plus "--sink
	// <file>" when Deps.Sink is set.
	Args []string
	// Env is the child's whole environment, built from scratch by
	// watcherEnviron. The messaging token is in here and nowhere else.
	Env []string
	// Dir is the child's working directory (the user's HOME).
	Dir string
	// LogPath is the 0600 append-mode file both of the child's output
	// streams go to: ${stateDir}/logs/watcher-<claude_pid>.log.
	LogPath string
}

A SpawnSpec is everything the watcher child is started with. The executable is this binary (os.Executable) and is not a member: a map or an option must never be able to name the program the hook detaches.

type Spawner

type Spawner interface {
	// Spawn starts `brigade watch` per spec and returns its pid. It never
	// waits for the child.
	Spawn(ctx context.Context, spec SpawnSpec) (int, error)
}

A Spawner starts the detached watcher (6.6). RealSpawner is the one exec.CommandContext of this package (spawn.go); tests inject a recorder.

Jump to

Keyboard shortcuts

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