Documentation
¶
Overview ¶
Package hook runs user-configured shell-command hooks around the agent loop: PreToolUse / PostToolUse fire around each tool call, UserPromptSubmit before a turn, Stop after it. Hooks come from settings.json — a project (.fairpeer/settings.json, only when the project is trusted) and a global (~/.fairpeer/settings.json) file. A hook's exit code is its verdict: 0 = pass, 2 = block (only on the gating events), other = warn. The payload is delivered as JSON on stdin; output is captured (capped) and surfaced to the user. This package only loads, matches, and runs hooks; the agent and controller decide what a block means (see internal/agent, internal/control).
Index ¶
- Constants
- Variables
- func FormatOutcome(o Outcome) string
- func GlobalSettingsPath(homeDir string) string
- func IsBlocking(e Event) bool
- func IsTrusted(projectRoot, homeDir string) bool
- func MatchesTool(h ResolvedHook, toolName string) bool
- func ProjectDefinesHooks(projectRoot string) bool
- func ProjectSettingsPath(projectRoot string) string
- func Trust(projectRoot, homeDir string) error
- func TrustPath(homeDir string) string
- type Decision
- type Event
- type HookConfig
- type LoadOptions
- type Outcome
- type Payload
- type Report
- type ResolvedHook
- type Runner
- func (r *Runner) Enabled() bool
- func (r *Runner) Has(event Event) bool
- func (r *Runner) HasPostLLMCall() bool
- func (r *Runner) Hooks() []ResolvedHook
- func (r *Runner) Notification(ctx context.Context, message string)
- func (r *Runner) PostLLMCall(ctx context.Context, reasoning string, turn int) string
- func (r *Runner) PostToolUse(ctx context.Context, name string, args json.RawMessage, result string)
- func (r *Runner) PreCompact(ctx context.Context, trigger string) string
- func (r *Runner) PreToolUse(ctx context.Context, name string, args json.RawMessage) (block bool, message string)
- func (r *Runner) PromptSubmit(ctx context.Context, prompt string, turn int) (block bool, message string)
- func (r *Runner) SessionEnd(ctx context.Context)
- func (r *Runner) SessionStart(ctx context.Context)
- func (r *Runner) Startup(ctx context.Context)
- func (r *Runner) Stop(ctx context.Context, lastAssistant string, turn int)
- func (r *Runner) SubagentStop(ctx context.Context, last string)
- type Scope
- type Settings
- type SpawnInput
- type SpawnResult
- type Spawner
Constants ¶
const ( SettingsDirname = ".fairpeer" SettingsFilename = "settings.json" )
SettingsDirname / SettingsFilename locate a scope's settings.json.
const TrustFilename = "trust.json"
TrustFilename is the user-global trust store under ~/.fairpeer.
Variables ¶
var Events = []Event{ PreToolUse, PostToolUse, PermissionRequest, UserPromptSubmit, Stop, PostLLMCall, SessionStart, SessionEnd, SubagentStop, Notification, PreCompact, Startup, }
Events is every event, in a stable order — drives loading and `/hooks`.
Functions ¶
func FormatOutcome ¶
FormatOutcome renders a non-pass outcome as a one-line human message.
func GlobalSettingsPath ¶
GlobalSettingsPath is ~/.fairpeer/settings.json (homeDir overrides ~).
func IsBlocking ¶
IsBlocking reports whether a non-zero/exit-2 (or timed-out) hook on this event can block the loop. Only the gating events qualify. (PreCompact does not block; it only contributes guidance via stdout.)
func MatchesTool ¶
func MatchesTool(h ResolvedHook, toolName string) bool
MatchesTool reports whether a hook applies to toolName. The match field is an anchored regex; non-tool events always match. A malformed regex never fires (safer than firing on everything).
func ProjectDefinesHooks ¶
ProjectDefinesHooks reports whether a project's settings.json exists and declares at least one hook — regardless of trust. Frontends use this to decide whether to prompt the user to trust the project.
func ProjectSettingsPath ¶
ProjectSettingsPath is <root>/.fairpeer/settings.json.
Types ¶
type Event ¶
type Event string
Event is a point in the agent loop a hook can fire at.
const ( PreToolUse Event = "PreToolUse" PostToolUse Event = "PostToolUse" PermissionRequest Event = "PermissionRequest" UserPromptSubmit Event = "UserPromptSubmit" Stop Event = "Stop" // PostLLMCall fires after every model turn completes (streaming finishes) but // before the reasoning_content is stored in the session. The hook receives the // raw reasoning text in the payload; its stdout, if non-empty on exit 0, // replaces the reasoning stored and displayed to the user. It can't block — a // non-zero exit or empty stdout leaves the reasoning unchanged. PostLLMCall Event = "PostLLMCall" // SessionStart fires once when a session becomes active (fresh, resumed, or // after /new). SessionEnd fires when it is closed or rotated. SubagentStop // fires when a `task` sub-agent finishes. Notification fires when the agent // needs the user's attention (e.g. a pending approval). PreCompact fires just // before a compaction pass; its stdout is injected as extra summary guidance. // Startup fires once when the process boots — before any session is active — // for one-time cold-start setup (logging, workspace prep, notifications). It // is not lazy like SessionStart (which waits for the first turn): it runs as // soon as hooks are loaded at boot. SessionStart Event = "SessionStart" SessionEnd Event = "SessionEnd" SubagentStop Event = "SubagentStop" Notification Event = "Notification" PreCompact Event = "PreCompact" Startup Event = "Startup" )
type HookConfig ¶
type HookConfig struct {
// Match is an anchored regex selecting tools (Pre/PostToolUse only); "" or
// "*" = every tool. Anchored: "file" won't match "read_file" — use ".*file".
Match string `json:"match,omitempty"`
// Command is the shell command to run (spawned through the platform shell).
Command string `json:"command"`
// Description is an optional human label surfaced in `/hooks`.
Description string `json:"description,omitempty"`
// Timeout overrides the per-event default, in milliseconds.
Timeout int `json:"timeout,omitempty"`
// Cwd overrides the working directory (defaults to the payload's cwd).
Cwd string `json:"cwd,omitempty"`
}
HookConfig is one hook as written in settings.json.
type LoadOptions ¶
LoadOptions configure Load. Project hooks load only when Trusted; global hooks always load.
type Outcome ¶
type Outcome struct {
Hook ResolvedHook
Decision Decision
ExitCode int // -1 when unknown (killed / spawn error)
Stdout string
Stderr string
TimedOut bool
Truncated bool
Duration time.Duration
}
Outcome records one hook invocation.
type Payload ¶
type Payload struct {
Event Event `json:"event"`
Cwd string `json:"cwd"`
ToolName string `json:"toolName,omitempty"`
ToolArgs json.RawMessage `json:"toolArgs,omitempty"`
Subject string `json:"subject,omitempty"`
ToolResult string `json:"toolResult,omitempty"`
Prompt string `json:"prompt,omitempty"`
LastAssistant string `json:"lastAssistantText,omitempty"`
Turn int `json:"turn,omitempty"`
Message string `json:"message,omitempty"` // Notification: what needs attention
Trigger string `json:"trigger,omitempty"` // PreCompact: "auto" | "manual"
Reasoning string `json:"reasoning,omitempty"` // PostLLMCall: the model's raw reasoning text
}
Payload is the JSON envelope written to a hook's stdin.
type Report ¶
type Report struct {
Event Event
Outcomes []Outcome
Blocked bool // at least one outcome blocked (only meaningful on gating events)
}
Report aggregates the outcomes of running an event's hooks.
type ResolvedHook ¶
type ResolvedHook struct {
HookConfig
Event Event
Scope Scope
Source string // absolute path to the settings.json it came from
}
ResolvedHook is a loaded hook with its origin baked in.
func Load ¶
func Load(opts LoadOptions) []ResolvedHook
Load resolves hooks: project first (only when trusted), then global; within a scope, settings.json array order. A malformed file yields no hooks (never an error — a typo shouldn't take down the CLI).
type Runner ¶
type Runner struct {
// contains filtered or unexported fields
}
Runner binds a set of resolved hooks to a session: a working directory, the spawner, and a notify callback that surfaces non-blocking hook messages to the user. It is the single object the agent (tool events) and the controller (prompt/stop events) fire hooks through, so neither has to know how hooks load or run. A nil *Runner is a valid no-op (no hooks configured).
func NewRunner ¶
func NewRunner(hooks []ResolvedHook, cwd string, spawner Spawner, notify func(string)) *Runner
NewRunner builds a Runner. spawner nil uses DefaultSpawner; notify nil drops non-blocking messages.
func (*Runner) Has ¶
Has reports whether any configured hook listens for the given event. Callers use it to skip work that only matters when a specific hook exists (e.g. the agent buffers reasoning for transform only when a PostLLMCall hook is set).
func (*Runner) HasPostLLMCall ¶
HasPostLLMCall reports whether a PostLLMCall hook is configured, so the agent keeps streaming reasoning live unless a transform is actually wired up.
func (*Runner) Hooks ¶
func (r *Runner) Hooks() []ResolvedHook
Hooks returns the resolved hooks (for `/hooks` listing).
func (*Runner) Notification ¶
Notification fires when the agent needs the user's attention (e.g. a pending approval). It can't block; message describes what's waiting.
func (*Runner) PostLLMCall ¶
PostLLMCall fires after every model turn completes but before the reasoning_content is stored in the session. It returns the hook's stdout as the new reasoning text, or the original reasoning if the hook passes with empty stdout / doesn't exist / fails. A non-pass outcome is surfaced via notify but doesn't block.
func (*Runner) PostToolUse ¶
PostToolUse fires after a tool call. It can't block; non-pass outcomes are surfaced to the user via notify.
func (*Runner) PreCompact ¶
PreCompact fires just before a compaction pass and returns the concatenated stdout of its hooks as extra summary guidance, so a hook can steer what the summary keeps. Non-pass outcomes are surfaced via notify.
func (*Runner) PreToolUse ¶
func (r *Runner) PreToolUse(ctx context.Context, name string, args json.RawMessage) (block bool, message string)
PreToolUse fires before a tool call. block=true means the call must be refused; message is the reason (fed back to the model and shown to the user).
func (*Runner) PromptSubmit ¶
func (r *Runner) PromptSubmit(ctx context.Context, prompt string, turn int) (block bool, message string)
PromptSubmit fires before a turn starts. block=true aborts the turn; message is the reason.
func (*Runner) SessionEnd ¶
SessionEnd fires when a session is closed or rotated (/new). It can't block.
func (*Runner) SessionStart ¶
SessionStart fires when a session becomes active. It can't block; its purpose is setup side effects (logging, prepping the workspace, desktop notifications).
func (*Runner) Startup ¶
Startup fires once when the process boots, before any session becomes active. It can't block; its purpose is one-time cold-start setup side effects (logging, prepping the workspace, desktop notifications). Unlike SessionStart (which is lazy — it waits for the first turn), Startup runs as soon as hooks are loaded.
type Scope ¶
type Scope string
Scope records which settings.json a hook came from. Project hooks fire before global ones.
type Settings ¶
type Settings struct {
Hooks map[Event][]HookConfig `json:"hooks"`
}
Settings is the shape of a settings.json (only hooks for now).
type SpawnInput ¶
SpawnInput / SpawnResult / Spawner are the test seam around the real spawn.
type SpawnResult ¶
type SpawnResult struct {
ExitCode int
Stdout string
Stderr string
TimedOut bool
SpawnErr error
Truncated bool
}
func DefaultSpawner ¶
func DefaultSpawner(ctx context.Context, in SpawnInput) SpawnResult
DefaultSpawner runs the command through the platform shell with the payload on stdin, capping captured output and honoring both the per-hook timeout and the parent context's cancellation.
type Spawner ¶
type Spawner func(ctx context.Context, in SpawnInput) SpawnResult