hook

package
v0.9.5 Latest Latest
Warning

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

Go to latest
Published: May 20, 2026 License: MIT Imports: 14 Imported by: 0

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:

  1. ~/.agents/hooks.toml — vendor-neutral, per-user
  2. ~/.config/hygge/hooks.toml — hygge-native, per-user
  3. <project-root>/.agents/hooks.toml — vendor-neutral, per-project
  4. <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

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 New

func New() *Registry

New returns an empty Registry.

func (*Registry) All

func (r *Registry) All() []Hook

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

func (r *Registry) For(event Event) []Hook

For returns the hooks registered for event, in registration order.

func (*Registry) Register

func (r *Registry) Register(h Hook) error

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

func (r *Registry) Unregister(name string)

Unregister removes hooks with the given name from every event list. It is a no-op when the name is not present.

type ToolResult

type ToolResult struct {
	IsError bool   `json:"is_error"`
	Content string `json:"content"`
}

ToolResult carries a tool's outcome for post_tool hooks.

type Warning

type Warning struct {
	HookName string
	Err      string
}

Warning captures a non-fatal hook execution error (e.g. malformed stdout) that fell-open.

Jump to

Keyboard shortcuts

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