hook

package
v0.1.5 Latest Latest
Warning

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

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

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

View Source
const (
	SettingsDirname  = ".fairpeer"
	SettingsFilename = "settings.json"
)

SettingsDirname / SettingsFilename locate a scope's settings.json.

View Source
const TrustFilename = "trust.json"

TrustFilename is the user-global trust store under ~/.fairpeer.

Variables

Events is every event, in a stable order — drives loading and `/hooks`.

Functions

func FormatOutcome

func FormatOutcome(o Outcome) string

FormatOutcome renders a non-pass outcome as a one-line human message.

func GlobalSettingsPath

func GlobalSettingsPath(homeDir string) string

GlobalSettingsPath is ~/.fairpeer/settings.json (homeDir overrides ~).

func IsBlocking

func IsBlocking(e Event) bool

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 IsTrusted

func IsTrusted(projectRoot, homeDir string) bool

IsTrusted reports whether projectRoot has been trusted to run its hooks.

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

func ProjectDefinesHooks(projectRoot string) bool

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

func ProjectSettingsPath(projectRoot string) string

ProjectSettingsPath is <root>/.fairpeer/settings.json.

func Trust

func Trust(projectRoot, homeDir string) error

Trust marks projectRoot as trusted, persisting the flag. Idempotent.

func TrustPath

func TrustPath(homeDir string) string

TrustPath is ~/.fairpeer/trust.json (homeDir overrides ~).

Types

type Decision

type Decision string

Decision is a single hook invocation's verdict.

const (
	DecisionPass  Decision = "pass"
	DecisionBlock Decision = "block"
	DecisionWarn  Decision = "warn"
	DecisionError Decision = "error" // spawn failed (ENOENT, EACCES, …)
)

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

type LoadOptions struct {
	ProjectRoot string
	HomeDir     string
	Trusted     bool
}

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.

func Run

func Run(ctx context.Context, payload Payload, hooks []ResolvedHook, spawner Spawner) Report

Run executes the hooks matching payload.Event (and, for tool events, the tool name), feeding each the JSON payload on stdin. It stops at the first block so a gating hook can prevent later hooks running against a phantom success.

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) Enabled

func (r *Runner) Enabled() bool

Enabled reports whether any hooks are configured.

func (*Runner) Has

func (r *Runner) Has(event Event) bool

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

func (r *Runner) HasPostLLMCall() bool

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

func (r *Runner) Notification(ctx context.Context, message string)

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

func (r *Runner) PostLLMCall(ctx context.Context, reasoning string, turn int) string

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

func (r *Runner) PostToolUse(ctx context.Context, name string, args json.RawMessage, result string)

PostToolUse fires after a tool call. It can't block; non-pass outcomes are surfaced to the user via notify.

func (*Runner) PreCompact

func (r *Runner) PreCompact(ctx context.Context, trigger string) string

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

func (r *Runner) SessionEnd(ctx context.Context)

SessionEnd fires when a session is closed or rotated (/new). It can't block.

func (*Runner) SessionStart

func (r *Runner) SessionStart(ctx context.Context)

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

func (r *Runner) Startup(ctx context.Context)

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.

func (*Runner) Stop

func (r *Runner) Stop(ctx context.Context, lastAssistant string, turn int)

Stop fires after a turn finishes. It can't block.

func (*Runner) SubagentStop

func (r *Runner) SubagentStop(ctx context.Context, last string)

SubagentStop fires when a `task` sub-agent finishes. It can't block; last is the sub-agent's final answer.

type Scope

type Scope string

Scope records which settings.json a hook came from. Project hooks fire before global ones.

const (
	ScopeProject Scope = "project"
	ScopeGlobal  Scope = "global"
)

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

type SpawnInput struct {
	Command string
	Cwd     string
	Stdin   string
	Timeout time.Duration
}

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

Jump to

Keyboard shortcuts

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