hooks

package
v1.52.0 Latest Latest
Warning

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

Go to latest
Published: Apr 27, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

Documentation

Overview

Package hooks provides lifecycle hooks for agent tool execution. Hooks allow users to run shell commands or prompts at various points during the agent's execution lifecycle, providing deterministic control over agent behavior.

Index

Constants

This section is empty.

Variables

View Source
var DefaultRegistry = NewRegistry()

DefaultRegistry is the process-wide registry used by NewExecutor. It is a fully-loaded NewRegistry (HookTypeCommand and HookTypeBuiltin both wired in) with no named builtins registered. Callers that need runtime-owned builtins should construct a private registry rather than mutating the default one.

Functions

This section is empty.

Types

type BuiltinFunc added in v1.52.0

type BuiltinFunc func(ctx context.Context, in *Input, args []string) (*Output, error)

BuiltinFunc is the signature of an in-process hook handler. It receives the parsed Input (no JSON unmarshaling on the user's side), any per-hook arguments declared as [Hook.Args] in the YAML, and returns a parsed Output, short-circuiting the stdout-as-JSON protocol that command hooks rely on.

Returning a nil Output is fine — it produces a successful no-op result and is useful for fire-and-forget handlers (logging, telemetry, ...).

type Config

type Config struct {
	// PreToolUse hooks run before tool execution
	PreToolUse []MatcherConfig `json:"pre_tool_use,omitempty" yaml:"pre_tool_use,omitempty"`

	// PostToolUse hooks run after tool execution
	PostToolUse []MatcherConfig `json:"post_tool_use,omitempty" yaml:"post_tool_use,omitempty"`

	// SessionStart hooks run when a session begins
	SessionStart []Hook `json:"session_start,omitempty" yaml:"session_start,omitempty"`

	// TurnStart hooks run at the start of every agent turn (each model
	// call). AdditionalContext is injected transiently and never
	// persisted to the session.
	TurnStart []Hook `json:"turn_start,omitempty" yaml:"turn_start,omitempty"`

	// BeforeLLMCall hooks run just before each model call (after
	// turn_start). Output is informational; use turn_start to
	// contribute system messages.
	BeforeLLMCall []Hook `json:"before_llm_call,omitempty" yaml:"before_llm_call,omitempty"`

	// AfterLLMCall hooks run just after each successful model call,
	// before the response is recorded and tool calls dispatched.
	AfterLLMCall []Hook `json:"after_llm_call,omitempty" yaml:"after_llm_call,omitempty"`

	// SessionEnd hooks run when a session ends
	SessionEnd []Hook `json:"session_end,omitempty" yaml:"session_end,omitempty"`

	// OnUserInput hooks run when the agent needs user input
	OnUserInput []Hook `json:"on_user_input,omitempty" yaml:"on_user_input,omitempty"`

	// Stop hooks run when the model finishes responding
	Stop []Hook `json:"stop,omitempty" yaml:"stop,omitempty"`

	// Notification hooks run when the agent sends a notification (error, warning) to the user
	Notification []Hook `json:"notification,omitempty" yaml:"notification,omitempty"`

	// OnError hooks run when the runtime hits an error during a turn
	// (model failures, repetitive tool-call loops). Fires alongside
	// Notification with level="error".
	OnError []Hook `json:"on_error,omitempty" yaml:"on_error,omitempty"`

	// OnMaxIterations hooks run when the runtime reaches its configured
	// max_iterations limit. Fires alongside Notification with
	// level="warning".
	OnMaxIterations []Hook `json:"on_max_iterations,omitempty" yaml:"on_max_iterations,omitempty"`
}

Config represents the hooks configuration for an agent

func FromConfig

func FromConfig(cfg *latest.HooksConfig) *Config

FromConfig converts a latest.HooksConfig to a hooks.Config

func (*Config) IsEmpty

func (c *Config) IsEmpty() bool

IsEmpty returns true if no hooks are configured

type Decision

type Decision string

Decision represents a permission decision from a hook

const (
	// DecisionAllow allows the operation to proceed
	DecisionAllow Decision = "allow"

	// DecisionDeny blocks the operation
	DecisionDeny Decision = "deny"

	// DecisionAsk prompts the user for confirmation (PreToolUse only)
	DecisionAsk Decision = "ask"
)

type EventType

type EventType string

EventType represents the type of hook event

const (
	// EventPreToolUse is triggered before a tool call executes.
	// Can allow/deny/modify tool calls; can block with feedback.
	EventPreToolUse EventType = "pre_tool_use"

	// EventPostToolUse is triggered after a tool completes successfully.
	// Can provide validation, feedback, or additional processing.
	EventPostToolUse EventType = "post_tool_use"

	// EventSessionStart is triggered when a session begins or resumes.
	// Can load context, setup environment, install dependencies.
	EventSessionStart EventType = "session_start"

	// EventTurnStart is triggered at the start of every agent turn (each
	// model call), AFTER the persisted messages are read but BEFORE the
	// model is invoked. Its AdditionalContext is appended as transient
	// system messages for that turn only — it is NOT persisted to the
	// session, so per-turn signals (date, environment, prompt files) are
	// recomputed every turn instead of bloating the message history on
	// every resume.
	EventTurnStart EventType = "turn_start"

	// EventBeforeLLMCall is triggered immediately before each model call,
	// AFTER turn_start has assembled the messages slice. Use this for
	// observability, cost guardrails, or auditing without contributing
	// system messages — turn_start is the right event for the latter.
	// The hook output is currently informational; a future extension may
	// honor a deny verdict to short-circuit the call.
	EventBeforeLLMCall EventType = "before_llm_call"

	// EventAfterLLMCall is triggered immediately after a successful model
	// call, BEFORE the response is recorded into the session and before
	// any tool calls are dispatched. Receives the assistant text content
	// in stop_response (matching the stop event). Failed model calls
	// fire EventOnError instead.
	EventAfterLLMCall EventType = "after_llm_call"

	// EventSessionEnd is triggered when a session terminates.
	// Can perform cleanup, logging, persist session state.
	EventSessionEnd EventType = "session_end"

	// EventOnUserInput is triggered when the agent needs input from the user.
	// Can log, notify, or perform actions before user interaction.
	EventOnUserInput EventType = "on_user_input"

	// EventStop is triggered when the model finishes its response and is about
	// to hand control back to the user. Can perform post-response validation,
	// logging, or cleanup.
	EventStop EventType = "stop"

	// EventNotification is triggered when the agent emits a notification to the user,
	// such as errors or warnings. Can send external notifications or log events.
	EventNotification EventType = "notification"

	// EventOnError is triggered specifically when the runtime hits an
	// error during a turn (model failures, repetitive tool-call loops).
	// Fires alongside EventNotification (level="error") so existing
	// notification hooks keep working; on_error gives a structured entry
	// point for users who want to react only to errors.
	EventOnError EventType = "on_error"

	// EventOnMaxIterations is triggered when the runtime reaches its
	// configured max_iterations limit. Fires alongside EventNotification
	// (level="warning") so existing notification hooks keep working;
	// on_max_iterations gives a structured entry point for users who
	// want to react only to that condition (e.g. log to a metrics
	// pipeline rather than a chat channel).
	EventOnMaxIterations EventType = "on_max_iterations"
)

type Executor

type Executor struct {
	// contains filtered or unexported fields
}

Executor handles the execution of hooks.

An Executor resolves each Hook's HookType against a Registry of [HandlerFactory]s. The default executor uses DefaultRegistry, which only knows about the built-in "command" handler; tests and embedders can supply their own registry via NewExecutorWithRegistry to add new handler kinds (in-process Go callbacks, HTTP webhooks, MCP tools, ...) without touching the executor itself.

Public entry points:

  • Executor.Dispatch runs the hooks registered for one EventType and aggregates their verdicts into a single Result.
  • Executor.Has reports whether any hooks are configured for an event, so callers can avoid building the Input when nothing would run.

func NewExecutor

func NewExecutor(config *Config, workingDir string, env []string) *Executor

NewExecutor creates a new hook executor backed by DefaultRegistry.

func NewExecutorWithRegistry added in v1.52.0

func NewExecutorWithRegistry(config *Config, workingDir string, env []string, registry *Registry) *Executor

NewExecutorWithRegistry creates a new hook executor that resolves hook types against the supplied registry.

This is the seam used to extend the hook system: callers register additional [HandlerFactory]s for new HookType values on a private Registry and pass it here.

func (*Executor) Dispatch added in v1.52.0

func (e *Executor) Dispatch(ctx context.Context, event EventType, input *Input) (*Result, error)

Dispatch runs the hooks registered for event and aggregates their verdicts into a single Result.

For tool-matched events (pre/post_tool_use), only hooks whose matcher matches input.ToolName are run. For all other events the hook list is flat. When no hooks would run, a permissive Result{Allowed: true} is returned without serializing the input.

Dispatch sets input.HookEventName to event so handlers can rely on it without the caller having to remember.

func (*Executor) Has added in v1.52.0

func (e *Executor) Has(event EventType) bool

Has reports whether any hooks are configured for event. Callers can use this to skip building the Input when nothing would run.

type Handler added in v1.52.0

type Handler interface {
	Run(ctx context.Context, input []byte) (HandlerResult, error)
}

Handler executes a single hook invocation.

A Handler is built by a HandlerFactory for a single Hook and is invoked at most once. The factory is responsible for digesting the Hook (validating it, copying out the fields the handler needs); Run only receives the JSON-encoded Input on stdin and the timeout-bounded context produced by the Executor.

A Handler MUST NOT apply Hook.GetTimeout itself: the executor wraps ctx with the timeout before calling Run. A Handler should respect ctx cancellation and return promptly.

type HandlerEnv added in v1.52.0

type HandlerEnv struct {
	// WorkingDir is the directory in which the handler should run.
	WorkingDir string
	// Env is the environment to expose to the handler, in os.Environ() form.
	Env []string
}

HandlerEnv is the per-executor context passed to handler factories. It carries everything a handler may need that isn't part of the Hook definition itself. New fields can be added in the future without breaking factory signatures.

type HandlerFactory added in v1.52.0

type HandlerFactory func(env HandlerEnv, hook Hook) (Handler, error)

HandlerFactory builds a Handler for a single hook invocation.

Factories are expected to validate the hook (e.g. that a command handler has a non-empty [Hook.Command]) and return an error if the hook is not runnable. The returned Handler is used for exactly one [Handler.Run] call and may be discarded afterwards.

type HandlerResult added in v1.52.0

type HandlerResult struct {
	// Stdout is the raw standard output produced by the handler.
	Stdout string
	// Stderr is the raw standard error produced by the handler.
	Stderr string
	// ExitCode is the handler's exit code. 0 means success; 2 is a
	// blocking error; any other non-zero value is treated as a
	// non-blocking error by the executor.
	ExitCode int
	// Output, when non-nil, short-circuits Stdout JSON parsing. In-process
	// handlers populate this directly.
	Output *Output
}

HandlerResult is the raw result of a single [Handler.Run] call.

A handler can speak to the executor in either of two ways:

  • The classic process protocol: leave Output nil, write JSON (or plain text) to Stdout, signal blocking with ExitCode == 2, etc. The executor parses Stdout into an Output when ExitCode == 0 and Stdout begins with '{'. This is what command hooks do.

  • The direct protocol: set Output to a pre-parsed Output. In-process handlers use this to skip the JSON round-trip; ExitCode should stay 0 and Stdout/Stderr can be left empty.

type Hook

type Hook struct {
	// Type specifies whether this is a command or prompt hook
	Type HookType `json:"type" yaml:"type"`

	// Command is the shell command to execute (for command hooks)
	Command string `json:"command,omitempty" yaml:"command,omitempty"`

	// Args are arbitrary string arguments passed to the hook handler.
	// They are interpreted by the handler kind: builtin hooks receive
	// them as the args parameter of [BuiltinFunc]; future handler kinds
	// (http, mcp, ...) can adopt the same field.
	Args []string `json:"args,omitempty" yaml:"args,omitempty"`

	// Timeout is the execution timeout in seconds (default: 60)
	Timeout int `json:"timeout,omitempty" yaml:"timeout,omitempty"`
}

Hook represents a single hook configuration

func (*Hook) GetTimeout

func (h *Hook) GetTimeout() time.Duration

GetTimeout returns the timeout duration, defaulting to 60 seconds

type HookSpecificOutput

type HookSpecificOutput struct {
	// HookEventName identifies which event this output is for
	HookEventName EventType `json:"hook_event_name,omitempty"`

	// PreToolUse fields
	PermissionDecision       Decision       `json:"permission_decision,omitempty"`
	PermissionDecisionReason string         `json:"permission_decision_reason,omitempty"`
	UpdatedInput             map[string]any `json:"updated_input,omitempty"`

	// PostToolUse/SessionStart fields
	AdditionalContext string `json:"additional_context,omitempty"`
}

HookSpecificOutput contains event-specific output fields

type HookType

type HookType string

HookType represents the type of hook action

const HookTypeBuiltin HookType = "builtin"

HookTypeBuiltin dispatches to a named, in-process Go function registered via Registry.RegisterBuiltin. The function name is stored in [Hook.Command], so we don't need a new YAML key — a builtin hook is written as `{type: builtin, command: "<name>"}`.

const (
	// HookTypeCommand executes a shell command
	HookTypeCommand HookType = "command"
)

type Input

type Input struct {
	// Common fields for all hooks
	SessionID     string    `json:"session_id"`
	Cwd           string    `json:"cwd"`
	HookEventName EventType `json:"hook_event_name"`

	// Tool-related fields (for PreToolUse and PostToolUse)
	ToolName  string         `json:"tool_name,omitempty"`
	ToolUseID string         `json:"tool_use_id,omitempty"`
	ToolInput map[string]any `json:"tool_input,omitempty"`

	// PostToolUse specific
	ToolResponse any `json:"tool_response,omitempty"`

	// SessionStart specific
	Source string `json:"source,omitempty"` // "startup", "resume", "clear", "compact"

	// SessionEnd specific
	Reason string `json:"reason,omitempty"` // "clear", "logout", "prompt_input_exit", "other"

	// Stop specific
	StopResponse string `json:"stop_response,omitempty"` // The model's final response content

	// Notification specific
	NotificationLevel   string `json:"notification_level,omitempty"`   // "error" or "warning"
	NotificationMessage string `json:"notification_message,omitempty"` // The notification content
}

Input represents the JSON input passed to hooks via stdin

func (*Input) ToJSON

func (i *Input) ToJSON() ([]byte, error)

ToJSON serializes the input to JSON

type MatcherConfig

type MatcherConfig struct {
	// Matcher is a regex pattern to match tool names (e.g., "shell|edit_file")
	// Use "*" to match all tools
	Matcher string `json:"matcher,omitempty" yaml:"matcher,omitempty"`

	// Hooks are the hooks to execute when the matcher matches
	Hooks []Hook `json:"hooks" yaml:"hooks"`
}

MatcherConfig represents a hook matcher with its hooks

type Output

type Output struct {
	// Continue indicates whether to continue execution (default: true)
	Continue *bool `json:"continue,omitempty"`

	// StopReason is the message to show when continue=false
	StopReason string `json:"stop_reason,omitempty"`

	// SuppressOutput hides stdout from transcript
	SuppressOutput bool `json:"suppress_output,omitempty"`

	// SystemMessage is a warning to show the user
	SystemMessage string `json:"system_message,omitempty"`

	// Decision is for blocking operations
	Decision string `json:"decision,omitempty"`

	// Reason is the message explaining the decision
	Reason string `json:"reason,omitempty"`

	// HookSpecificOutput contains event-specific fields
	HookSpecificOutput *HookSpecificOutput `json:"hook_specific_output,omitempty"`
}

Output represents the JSON output from a hook

func (*Output) IsBlocked

func (o *Output) IsBlocked() bool

IsBlocked returns true if the decision is to block

func (*Output) ShouldContinue

func (o *Output) ShouldContinue() bool

ShouldContinue returns whether execution should continue

type Registry added in v1.52.0

type Registry struct {
	// contains filtered or unexported fields
}

Registry maps a HookType to a HandlerFactory, and a builtin name to a BuiltinFunc.

A Registry is safe for concurrent use. NewRegistry returns a registry pre-populated with the two universal handler kinds ("command" and "builtin"); embedders register additional handler types or named builtin Go functions on it before passing it to NewExecutorWithRegistry.

The process-wide DefaultRegistry is one such registry, used by NewExecutor. It is convenient for callers that don't have any runtime-owned builtins of their own; callers that do (e.g. the agent runtime) should construct a private registry to keep their builtins out of any shared global state.

func NewRegistry added in v1.52.0

func NewRegistry() *Registry

NewRegistry returns a registry pre-populated with the two universal handler kinds: HookTypeCommand (shell command hooks) and HookTypeBuiltin (in-process Go callbacks looked up via Registry.RegisterBuiltin).

func (*Registry) Lookup added in v1.52.0

func (r *Registry) Lookup(t HookType) (HandlerFactory, bool)

Lookup returns the factory registered for t, or (nil, false) if none.

func (*Registry) LookupBuiltin added in v1.52.0

func (r *Registry) LookupBuiltin(name string) (BuiltinFunc, bool)

LookupBuiltin returns the function registered as name, or (nil, false). Exported primarily for tests and tooling that want to enumerate or validate builtins without going through the Executor.

func (*Registry) Register added in v1.52.0

func (r *Registry) Register(t HookType, f HandlerFactory)

Register associates a factory with a hook type. A subsequent registration of the same type replaces the previous one.

func (*Registry) RegisterBuiltin added in v1.52.0

func (r *Registry) RegisterBuiltin(name string, fn BuiltinFunc) error

RegisterBuiltin makes fn callable as `{type: builtin, command: name}` on this registry.

Subsequent registrations of the same name replace the previous one, matching the Registry.Register contract. Empty name or nil fn are rejected.

RegisterBuiltin is safe for concurrent use, but typical callers register from package-level setup or a runtime constructor.

type Result

type Result struct {
	// Allowed indicates if the operation should proceed
	Allowed bool

	// Message is feedback to include in the response
	Message string

	// ModifiedInput contains any modifications to tool input (PreToolUse only)
	ModifiedInput map[string]any

	// AdditionalContext is context to add (PostToolUse/SessionStart)
	AdditionalContext string

	// SystemMessage is a warning to show the user
	SystemMessage string

	// ExitCode is the exit code from the hook command (0 = success, 2 = blocking error)
	ExitCode int

	// Stderr contains any error output from the hook
	Stderr string
}

Result represents the result of executing hooks

Jump to

Keyboard shortcuts

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