Documentation
¶
Overview ¶
Package hook implements a programmatic event-gate framework for the agent loop. Hooks react to four agent events — pre_tool, post_tool, pre_message, post_message — and can allow, deny, or modify the event payload.
Hook sources ¶
Hooks are configured via hooks.toml discovered at the standard four-layer paths:
- ~/.agents/hooks.toml — vendor-neutral, per-user
- ~/.config/hygge/hooks.toml — hygge-native, per-user
- <project-root>/.agents/hooks.toml — vendor-neutral, per-project
- <project-root>/.hygge/hooks.toml — hygge-native, per-project
Shell hook protocol ¶
Each hook invokes an external command. The agent serialises the Input struct as JSON and pipes it to the command's stdin. The command writes an Action JSON object to stdout (or nothing, which is treated as Allow). A non-zero exit code is treated as Deny; the deny reason is read from stderr (truncated to 1 KiB).
Sync vs async ¶
Sync hooks (the default) block the agent until Run returns or the timeout fires. Async hooks are dispatched in a goroutine and are only valid for post_* events; declaring async on a pre_* event is rejected at load time and the hook is skipped with a warning.
Post-message hooks are always treated as async by the registry: if a hook with events=["post_message"] is declared sync, the registry coerces it and logs a slog.Warn.
Lifecycle ¶
Call New to build an empty registry, Registry.Register to add hooks, or Load for the full TOML-discovery path. After use call Registry.Close to wait up to 2 s for in-flight async goroutines.
Index ¶
- type Action
- type Decision
- type Event
- type Hook
- type Input
- type LoadOptions
- type Mode
- type Registry
- func (r *Registry) All() []Hook
- func (r *Registry) Close()
- func (r *Registry) For(event Event) []Hook
- func (r *Registry) Register(h Hook) error
- func (r *Registry) RunPost(ctx context.Context, event Event, in Input) (out Input, warns []Warning)
- func (r *Registry) RunPre(ctx context.Context, event Event, in Input) (out Input, dec Decision, denier, reason string, warns []Warning)
- func (r *Registry) Unregister(name string)
- type ToolResult
- type Warning
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Action ¶
type Action struct {
// Decision is "allow", "deny", or "modify". Empty defaults to
// "allow".
Decision Decision `json:"decision,omitempty"`
// Reason is surfaced to the agent on deny.
Reason string `json:"reason,omitempty"`
// ModifiedToolInput, when Decision="modify" on a pre_tool event,
// replaces the tool's input args before execution.
ModifiedToolInput json.RawMessage `json:"modified_tool_input,omitempty"`
// ModifiedMessage, when Decision="modify" on a pre_message event,
// replaces the user's message text. On post_message it replaces
// the assistant text (use with care — this is redaction territory).
ModifiedMessage string `json:"modified_message,omitempty"`
// ModifiedToolResult, when Decision="modify" on a post_tool event,
// replaces the tool's result before returning to the model.
ModifiedToolResult *ToolResult `json:"modified_tool_result,omitempty"`
// SystemPromptAppend, when returned from a pre_message hook, appends
// one-turn context to the model's system prompt. Unlike
// ModifiedMessage, this does not alter or persist the visible user
// message. It is honored for allow and modify decisions.
SystemPromptAppend string `json:"system_prompt_append,omitempty"`
}
Action is the hook's response, parsed from its stdout. The zero value is "allow".
type Decision ¶
type Decision string
Decision is the hook's verdict on the event.
const ( // DecisionAllow lets the event proceed unchanged. The zero value of // Decision is treated as Allow. DecisionAllow Decision = "allow" // DecisionDeny blocks the event. The agent surfaces Reason to the // model as an error result. DecisionDeny Decision = "deny" // DecisionModify allows the event but replaces parts of the payload // before continuing. Only ModifiedToolInput (pre_tool), // ModifiedMessage (pre_message / post_message), and // ModifiedToolResult (post_tool) are acted on. DecisionModify Decision = "modify" )
type Event ¶
type Event string
Event identifies which agent event a hook is reacting to.
const ( // EventPreTool fires before a tool is executed, after the permission // gate has passed. Sync-only. Can deny or modify tool input. EventPreTool Event = "pre_tool" // EventPostTool fires after a tool returns. Sync hooks run first // (modify accumulates); async hooks are dispatched afterwards. EventPostTool Event = "post_tool" // EventPreMessage fires before the user message is persisted, right // at the start of Send. Sync-only. Can deny, modify the text, or // append one-turn system prompt context. EventPreMessage Event = "pre_message" // EventPostMessage fires after the assistant message is committed. // Always treated as async regardless of the hook's declared mode. EventPostMessage Event = "post_message" )
type Hook ¶
type Hook interface {
// Name returns the unique identifier used in TOML and log output.
Name() string
// Description is the one-line human summary.
Description() string
// Source is "user" or "project".
Source() string
// Events returns the set of events this hook is registered for.
Events() []Event
// Mode returns the hook's declared mode.
Mode() Mode
// Timeout is the per-invocation timeout. Zero means no timeout.
Timeout() time.Duration
// Run executes the hook. in carries the event payload; the
// returned Action describes the hook's decision. A non-nil error
// means the hook itself failed (not a deny decision).
Run(ctx context.Context, in Input) (Action, error)
}
Hook is a single hook instance. Implementations run synchronously from the agent's perspective (the agent waits for Run to return, up to the hook's Timeout). Async dispatch is handled by the Registry.
Implementations must be safe for concurrent Run calls from multiple sessions.
type Input ¶
type Input struct {
// Event is the lifecycle event that triggered this hook.
Event Event `json:"event"`
// SessionID identifies the active session.
SessionID string `json:"session_id"`
// HookName is the name of this specific hook.
HookName string `json:"hook_name"`
// Pwd is the working directory of the agent process.
Pwd string `json:"pwd"`
// ToolName is set for pre_tool / post_tool.
ToolName string `json:"tool_name,omitempty"`
// ToolInput is the raw JSON arguments. Set for pre_tool / post_tool.
ToolInput json.RawMessage `json:"tool_input,omitempty"`
// ToolResult is the result returned by the tool. Set for post_tool
// only.
ToolResult *ToolResult `json:"tool_result,omitempty"`
// Message is the text payload. Set for pre_message (user input) and
// post_message (assistant text).
Message string `json:"message,omitempty"`
// ModeName is set when a pre_message hook is invoked to refresh
// one-turn system prompt additions after a UI mode switch.
ModeName string `json:"mode_name,omitempty"`
// SystemPromptAdditions are one-turn additions collected from
// pre_message hooks. They are intentionally not exposed to hook
// subprocesses or plugin handlers; callers append them to the system
// prompt without persisting them into visible message history.
SystemPromptAdditions []string `json:"-"`
}
Input is the JSON payload written to the hook's stdin.
type LoadOptions ¶
type LoadOptions struct {
// HomeDir overrides $HOME for tests.
HomeDir string
// XDGConfigHome overrides $XDG_CONFIG_HOME for tests.
XDGConfigHome string
// Pwd is the starting directory for the project walk-up. When
// empty no project layers are consulted.
Pwd string
}
LoadOptions configures Load.
type Mode ¶
type Mode string
Mode controls whether a hook runs synchronously (blocking) or asynchronously (fire-and-forget).
const ( // ModeSync is the default. The agent waits for Run to return (or // timeout to fire) before continuing. ModeSync Mode = "sync" // ModeAsync runs the hook in a goroutine. Only valid for post_* // events; if declared on a pre_* event the hook is skipped with a // warning. ModeAsync Mode = "async" )
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry holds the loaded hooks, indexed by event type. Construct via New or Load; the zero value is a valid empty registry but Close must still be called.
func Load ¶
func Load(opts LoadOptions) (*Registry, error)
Load assembles a Registry from hooks.toml at the standard four discovery paths, plus the built-in zero hooks. Missing files are silently ignored. Malformed entries log slog.Warn and are skipped.
func (*Registry) All ¶
All returns every hook registered, deduplicated by name (first seen wins), sorted by name for deterministic output.
func (*Registry) Close ¶
func (r *Registry) Close()
Close waits up to 2 s for in-flight async hooks to finish, then returns. Idempotent.
func (*Registry) Register ¶
Register adds h to the registry for each of h's declared events. Duplicate names within an event are allowed (later hooks run later in the chain).
func (*Registry) RunPost ¶
func (r *Registry) RunPost( ctx context.Context, event Event, in Input, ) (out Input, warns []Warning)
RunPost executes hooks for a post_* event. Sync hooks run in order (modify decisions accumulate). Async hooks are dispatched in goroutines after sync hooks finish; they receive the post-sync-modified Input.
If all post_message hooks are coerced to async at load time, this function is effectively async-only for that event.
func (*Registry) RunPre ¶
func (r *Registry) RunPre( ctx context.Context, event Event, in Input, ) (out Input, dec Decision, denier, reason string, warns []Warning)
RunPre executes all sync hooks registered for a pre_* event in registration order, stopping on the first Deny. Modify decisions accumulate: later hooks see the updated payload.
Returns:
- out: the (possibly modified) Input.
- dec: Allow or Deny.
- denier: name of the hook that denied (empty when dec=Allow).
- reason: the deny reason (empty when dec=Allow).
- warns: non-fatal errors from hooks that fell open.
func (*Registry) Unregister ¶ added in v0.4.0
Unregister removes hooks with the given name from every event list. It is a no-op when the name is not present.
type ToolResult ¶
ToolResult carries a tool's outcome for post_tool hooks.