Documentation
¶
Overview ¶
Package hook implements lifecycle hooks that observe and control the agent loop, in the style of Claude Code hooks.
Index ¶
- Constants
- func Middleware(m *Manager) loop.Middleware
- func PostCompactObserver(m *Manager) func(ctx context.Context, run *loop.Run, reason string, c *loop.Compaction)
- func PreCompactGuard(m *Manager) func(ctx context.Context, run *loop.Run, reason string) bool
- func StopFeedback(event EventType, res *Result) string
- type Agent
- type Async
- type AsyncResult
- type Command
- type Decision
- type Event
- type EventType
- type Func
- type HTTP
- type Hook
- type MCPCaller
- type MCPCallerFunc
- type MCPTool
- type Manager
- func (m *Manager) AddContext(sessionID, text string)
- func (m *Manager) Close()
- func (m *Manager) Dispatch(ctx context.Context, ev *Event) (res *Result, err error)
- func (m *Manager) DropContext(sessionID string)
- func (m *Manager) Has(event EventType) bool
- func (m *Manager) MaxStopBlocks() int
- func (m *Manager) Merge(other *Manager)
- func (m *Manager) On(event EventType, matcher string, h Hook) error
- func (m *Manager) OnFunc(event EventType, matcher string, ...) error
- func (m *Manager) SetPermissionMode(f func(*loop.Run) string)
- func (m *Manager) Wait(ctx context.Context) error
- type Option
- type Prompt
- type Result
- type SettingsOptions
- type Verifier
- type VerifierFunc
Constants ¶
const DefaultStopBlockCap = 8
DefaultStopBlockCap is the default number of consecutive times Stop or SubagentStop hooks may keep the agent working, like Claude Code's 8-consecutive-continuation cap.
Variables ¶
This section is empty.
Functions ¶
func Middleware ¶
func Middleware(m *Manager) loop.Middleware
Middleware returns runtime middleware that dispatches lifecycle events to m. Hook errors are logged and otherwise ignored unless m is FailClosed. Context queued for the run's session with Manager.AddContext, such as the results of Async hooks, is added to the run's input and to tool results.
PreCompact and PostCompact hooks run through PreCompactGuard and PostCompactObserver, SubagentStart and SubagentStop hooks through agent.AsTool, and SessionEnd hooks through agent.Runner.EndSession.
func PostCompactObserver ¶
func PostCompactObserver(m *Manager) func(ctx context.Context, run *loop.Run, reason string, c *loop.Compaction)
PostCompactObserver returns a function that runs the PostCompact hooks of m after a compaction, with the summary in Event.Message, and then its SessionStart hooks with source "compact", as Claude Code does. PostCompact results are ignored; the AdditionalContext of the SessionStart hooks is added to the first user message of the compacted conversation, next to the summary. Errors are logged. Pass it to context.WithCompactObserver.
func PreCompactGuard ¶
PreCompactGuard returns a function that runs the PreCompact hooks of m and reports whether compaction may proceed. Pass it to context.WithCompactGuard.
func StopFeedback ¶
StopFeedback returns the input that continues an agent whose Stop or SubagentStop hooks, as given by event, returned res, or "" if it may stop: the Reason of a Deny and any AdditionalContext, which Claude Code delivers as hook feedback. A Halt lets the agent stop.
Types ¶
type Agent ¶
type Agent struct {
Verifier Verifier
Prompt string
// Model is passed to the verifier; "" selects its default.
Model string
// Timeout defaults to 60 seconds. A negative Timeout disables it.
Timeout time.Duration
}
Agent is a hook that has an agent verify the event with tools, like Claude Code's agent hooks, which are experimental. Prompt is expanded as for a Prompt hook, followed by instructions to end with {"ok": true} or {"ok": false, "reason": "..."}. A block is handled like that of a Prompt hook with ContinueOnBlock set, and "impossible" is ignored.
type Async ¶
Async runs a hook in the background, like Claude Code's "async": true. The action that fired the event does not wait for it, and its decision, Halt and updates have no effect. When it finishes, its AdditionalContext and SystemMessage are queued for its session and added to the conversation at the next tool result or the next run's input, whichever comes first; see Manager.AddContext.
With Rewake, like Claude Code's "asyncRewake", a Deny (exit status 2 of a Command) also queues its Reason, the command's stderr or else its stdout, and sets AsyncResult.Rewake so an OnAsyncResult function can wake an idle session.
The hook's own timeout applies; settings files give async commands no timeout, as Claude Code does not enforce one. Manager.Close cancels the hooks still running.
type AsyncResult ¶
type AsyncResult struct {
Event EventType
SessionID string
RunID string
// Context is what was queued for the session: the hook's
// AdditionalContext and SystemMessage, and for Rewake the Reason of a
// Deny.
Context string
// Rewake is set when a Rewake hook denied (exited with status 2).
Rewake bool
// Err is the hook's error; context.Canceled if Manager.Close
// cancelled it.
Err error
}
AsyncResult is the outcome of an Async hook.
type Command ¶
type Command struct {
// Command is run with "sh -c" (or "cmd /d /s /c" on Windows). When Args
// is non-nil, Command is instead the executable, run directly with Args
// as its arguments; "${CLAUDE_PROJECT_DIR}" in Command and Args is
// replaced by the project directory.
Command string
// Args, when non-nil (even empty), selects the exec form.
Args []string
// Dir is the working directory.
Dir string
// Env is added to the process environment.
Env []string
// Timeout defaults to 10 minutes, to 30 seconds for UserPromptSubmit
// and to 1.5 seconds for SessionEnd (see SessionEndTimeout). A negative
// Timeout disables it.
Timeout time.Duration
}
Command is a hook that runs a command, compatible with Claude Code command hooks.
The event is written to the command's stdin as JSON. It carries Claude Code's fields: hook_event_name, session_id, transcript_path (always empty), cwd (Dir or the process's working directory) and permission_mode (Event.PermissionMode, or "default"); tool_name, tool_use_id and tool_input for tool events; tool_response for PostToolUse; error and is_interrupt for PostToolUseFailure; prompt; source for SessionStart; trigger for PreCompact and PostCompact, with custom_instructions (null) and compact_summary; message, title and notification_type for Notification; stop_hook_active and last_assistant_message for Stop and SubagentStop; agent_id (the sub-agent's run ID) and agent_type for SubagentStart; and agent_id, agent_type and agent_transcript_path (empty) for SubagentStop. tool_name and tool_input are agentx's, and tool_response is {"content": ..., "is_error": ...} rather than the structured output of Claude Code's built-in tools. The agentx fields run_id, agent, turn, reason and data (Event.Data) are added. The environment has CLAUDE_PROJECT_DIR (Dir or the working directory), AGENTX_HOOK_EVENT and AGENTX_SESSION_ID.
Exit status 0 succeeds. If stdout is a JSON object it is parsed as a hook output (see below); otherwise non-empty stdout becomes AdditionalContext for SessionStart and UserPromptSubmit. Exit status 2 denies the action with stderr as the reason; JSON fields on stdout still apply but cannot override the deny. Any other status is a hook error, unless stdout is a JSON object, which then decides alone. Processes the command leaves running in the background do not delay or discard its result for more than a second; on Unix they are also killed once it exits. On Windows they keep running unless the hook times out or its context is cancelled.
JSON output fields: decision ("allow", "approve", "ask", "defer", "deny" or "block"), reason, continue (false halts the run), stopReason, systemMessage (Result.SystemMessage), additionalContext and hookSpecificOutput with permissionDecision, permissionDecisionReason, additionalContext, updatedInput and updatedToolOutput. updatedToolOutput must be a string or an object with a string "content" field; other values are ignored. updatedMCPToolOutput is accepted in its place. suppressOutput has no effect, as in Claude Code. A "defer" decision drops updatedInput and additionalContext. Notification, PreCompact and PostCompact ignore continue and systemMessage.
type Decision ¶
type Decision string
Decision is a hook's verdict on the action that triggered it.
const ( // Continue expresses no opinion. Continue Decision = "" // Allow approves the action and skips further approval. Allow Decision = "allow" // Ask requires approval before the action proceeds. Ask Decision = "ask" // Defer pauses a PreToolUse call so it can be decided later, like // Claude Code's "defer". agentx treats it like Ask: the call needs // approval, so without an Approver the run is interrupted and can be // resumed. It ranks between Ask and Deny. Defer Decision = "defer" // Deny blocks the action. Deny Decision = "deny" )
type Event ¶
type Event struct {
Type EventType `json:"hook_event_name"`
SessionID string `json:"session_id,omitempty"`
RunID string `json:"run_id,omitempty"`
Agent string `json:"agent,omitempty"`
Turn int `json:"turn,omitempty"`
// PermissionMode is the run's permission mode, such as "default" or
// "plan"; see Manager.SetPermissionMode.
PermissionMode string `json:"permission_mode,omitempty"`
// Prompt is the user input for UserPromptSubmit and SessionStart.
Prompt string `json:"prompt,omitempty"`
// Request is the pending model request for PreModel. Hooks may modify it.
Request *model.Request `json:"-"`
// Response is the model response for PostModel.
Response *model.Response `json:"-"`
// ToolCall is set for tool events.
ToolCall *model.ToolCall `json:"-"`
// ToolResult is set for PostToolUse and PostToolUseFailure.
ToolResult *model.ToolResult `json:"-"`
// Messages is the conversation for Stop, SubagentStop, PreCompact and
// PostCompact.
Messages []*model.Message `json:"-"`
// Message is the text of a Notification, the final text for Stop and
// SubagentStop, or the summary for PostCompact.
Message string `json:"message,omitempty"`
// Title is the title of a Notification.
Title string `json:"title,omitempty"`
// Reason explains why the event fired: the source for SessionStart
// ("startup", "resume", "clear", "fork" or "compact"), the reason for
// SessionEnd, "auto" for PreCompact and PostCompact, which fire only for
// automatic compaction, the sub-agent's name for SubagentStart and
// SubagentStop, and "approval" for a Notification that asks for tool
// approval.
Reason string `json:"reason,omitempty"`
// StopHookActive is true when the agent is already continuing because a
// Stop or SubagentStop hook blocked a previous stop in the same run or
// sub-agent call, even after later tool calls. Hooks use it to avoid
// loops.
StopHookActive bool `json:"stop_hook_active,omitempty"`
// Data carries additional values.
Data map[string]any `json:"data,omitempty"`
}
Event is the input to a hook.
type EventType ¶
type EventType string
EventType identifies a lifecycle point at which hooks run.
const ( // SessionStart runs when a session starts, with Reason as Claude // Code's source: "startup" for the first run of a new session, // "resume" when an agent.Runner first runs or resumes an existing // session, "clear" and "fork" for the first run of a session made by // Runner.Clear or Runner.Fork, and "compact" after automatic // compaction. AdditionalContext is added to the conversation: to the // run's input, or after the compaction summary. It cannot block the // session. SessionStart EventType = "SessionStart" // SessionEnd runs when agent.Runner.EndSession ends a session, with // Reason as Claude Code's reason, such as "clear", "logout", // "prompt_input_exit" or "other". It cannot block, and its results are // ignored. Its hooks share a time budget; see SessionEndTimeout. SessionEnd EventType = "SessionEnd" // UserPromptSubmit runs before user input enters the conversation. Deny // blocks the run; AdditionalContext is attached to the input. UserPromptSubmit EventType = "UserPromptSubmit" // PreModel runs before each model call; hooks may modify Event.Request. PreModel EventType = "PreModel" // PostModel runs after each model call with Event.Response. PostModel EventType = "PostModel" // PreToolUse runs before a tool call. Deny rejects the call, Ask and // Defer request approval, Allow skips approval and UpdatedInput // replaces the arguments. PreToolUse EventType = "PreToolUse" // PostToolUse runs after a tool call that succeeded. UpdatedOutput // replaces the result, and AdditionalContext and the Reason of a Deny // are appended to it. PostToolUse EventType = "PostToolUse" // PostToolUseFailure runs instead of PostToolUse after a tool call // that ran and returned an error result. AdditionalContext and the // Reason of a Deny are appended to the result. Calls denied before // they ran fire neither event. PostToolUseFailure EventType = "PostToolUseFailure" // Stop runs when the model finishes without tool calls. Deny with a // Reason, or AdditionalContext, makes the agent continue with them as // new input, up to Manager.MaxStopBlocks times in a row (8 by // default); a turn with tool calls resets the count. Stop EventType = "Stop" // SubagentStop runs when a sub-agent invoked as a tool (agent.AsTool) // finishes. Like Stop, Deny with a Reason, or AdditionalContext, keeps // the sub-agent working with them as new input, under the same cap. SubagentStop EventType = "SubagentStop" // PreCompact runs before the conversation is compacted. Deny skips // automatic compaction. PreCompact EventType = "PreCompact" // PostCompact runs after the conversation was compacted, with the // summary in Message. Its result is ignored. PostCompact EventType = "PostCompact" // Notification carries informational messages such as approval requests. Notification EventType = "Notification" // SubagentStart runs when a sub-agent invoked as a tool (agent.AsTool) // starts a task, before its run. Agent and Reason are the sub-agent's // name, which the matcher matches, RunID is the ID given to its run and // Prompt is the task. It cannot block the sub-agent; AdditionalContext // is added in front of the task. SubagentStart EventType = "SubagentStart" )
type HTTP ¶
type HTTP struct {
URL string
// Headers are added to the request. "$NAME" and "${NAME}" in values are
// replaced by the environment variable NAME if it is listed in
// AllowedEnvVars, and by "" otherwise.
Headers map[string]string
AllowedEnvVars []string
// Timeout defaults to 10 minutes, to 30 seconds for UserPromptSubmit
// and to 1.5 seconds for SessionEnd. A negative Timeout disables it.
Timeout time.Duration
// Client defaults to http.DefaultClient.
Client *http.Client
}
HTTP is a hook that POSTs the event to a URL, compatible with Claude Code HTTP hooks.
The request body is the JSON a Command receives on stdin, with Content-Type application/json. As in Claude Code, a 2xx response with an empty body succeeds without output, and a 2xx response whose body is a JSON object is read like a command's JSON output. Any other body, a non-2xx status, a connection failure and a timeout are hook errors, which do not block the action unless the manager is FailClosed.
type MCPCaller ¶
type MCPCaller interface {
CallTool(ctx context.Context, server, tool string, args json.RawMessage) (text string, isError bool, err error)
}
MCPCaller calls a tool of a connected MCP server for MCPTool hooks and returns the text content of the result and whether the server reported it as an error. mcp.HookCaller returns one for mcp.Client values.
type MCPCallerFunc ¶
type MCPCallerFunc func(ctx context.Context, server, tool string, args json.RawMessage) (string, bool, error)
MCPCallerFunc adapts a function to MCPCaller.
type MCPTool ¶
type MCPTool struct {
Caller MCPCaller
Server string
Tool string
// Input holds the tool arguments. "${path}" in its string values, such
// as "${tool_input.file_path}", is replaced by the value at that
// dotted path in the event's JSON input: strings as they are, other
// values as JSON, and missing values by "".
Input map[string]any
// Timeout defaults to 10 minutes, to 30 seconds for UserPromptSubmit
// and to 1.5 seconds for SessionEnd. A negative Timeout disables it.
Timeout time.Duration
}
MCPTool is a hook that calls a tool of an MCP server, like Claude Code's mcp_tool hooks. The tool's text is read like a command's stdout on exit status 0, and a result marked as an error is a hook error.
type Manager ¶
type Manager struct {
// contains filtered or unexported fields
}
Manager dispatches events to registered hooks.
A nil *Manager is valid and has no hooks. The zero value is an empty manager with the default options.
func LoadSettings ¶
LoadSettings reads Claude Code settings files with SettingsOptions.Load without options, so only command and http handlers run.
func ParseSettings ¶
ParseSettings returns a manager with the hooks configured in the "hooks" block of a Claude Code settings file, such as
{"hooks": {"PreToolUse": [{"matcher": "Bash", "hooks": [
{"type": "command", "command": "./check.sh", "timeout": 30}]}]}}
It is SettingsOptions.Parse without options, so only command and http handlers run.
func (*Manager) AddContext ¶
AddContext queues text for a session's conversation. The hook middleware adds it, as a system reminder, to the next tool result or the next run's input of the session, whichever comes first. Async hooks and agent.Runner, for the SessionStart hooks of a resumed session, use it. Empty text is ignored.
func (*Manager) Close ¶
func (m *Manager) Close()
Close cancels the Async hooks running in the background and waits for them to finish, like Claude Code at the end of a non-interactive session. The manager remains usable.
func (*Manager) Dispatch ¶
Dispatch runs the matching hooks and merges their results. It always returns a non-nil result.
As in Claude Code, the matching hooks run in parallel (see Parallelism), each with its own copy of ev, and identical Command, HTTP, MCPTool, Prompt and Agent hooks registered more than once run once. The results are merged in registration order, whatever order the hooks finish in: the most restrictive decision wins (deny > defer > ask > allow), with the Reason of the first hook that returned it; AdditionalContext and SystemMessage accumulate; the last UpdatedInput and UpdatedOutput in registration order win. For PreToolUse the merged UpdatedInput is then applied to ev.ToolCall. PreModel hooks, which may modify ev.Request, run one at a time in registration order on ev itself, and so does a lone hook.
Async hooks start in the background and do not contribute to the result. SessionEnd hooks share a time budget; see SessionEndTimeout. Hook errors and panics are returned joined; with FailClosed they also produce Deny. If ctx is done, hooks that have not started are skipped and its error is returned.
Inside a run traced by middleware.Tracing, hooks that run get a "hook {event}" span.
func (*Manager) DropContext ¶
DropContext discards the context queued for a session, for example when it ends.
func (*Manager) MaxStopBlocks ¶
MaxStopBlocks returns how many consecutive times Stop or SubagentStop hooks may keep the agent working; zero means no limit. See StopBlockCap.
func (*Manager) Merge ¶
Merge registers all hooks from other into m. FailClosed carries over, and so do the other options of other that m does not set.
func (*Manager) On ¶
On registers a hook for an event.
matcher follows Claude Code's rules. An empty matcher or "*" matches everything. A matcher made only of letters, digits, '_', '-', spaces, ',' and '|' lists exact names separated by '|' or ','; for example "Edit|Write" or "Edit, Write". Any other matcher is a regular expression (Go RE2 syntax) that may match anywhere in the value, so "^Edit$" matches only Edit while "Edit.*" also matches NotebookEdit.
For tool events the value is the tool name. Claude Code's built-in tool names also select the agentx built-ins with the same role: Bash (bash), Read (read_file), Write (write_file), Edit and MultiEdit (edit_file), Glob (glob), Grep (grep), WebFetch (web_fetch) and TodoWrite (todo_write); Edit and Write also select apply_patch. For Notification the value is Event.Reason or Claude Code's notification type, such as permission_prompt for "approval"; for other events it is Event.Reason.
func (*Manager) OnFunc ¶
func (m *Manager) OnFunc(event EventType, matcher string, f func(context.Context, *Event) (*Result, error)) error
OnFunc registers a function hook.
func (*Manager) SetPermissionMode ¶
SetPermissionMode sets how the manager learns a run's permission mode, which events report as Event.PermissionMode and command hooks receive as permission_mode. agent.New sets it from a middleware with a PermissionMode(*loop.Run) string method, such as permission.Policy. Without it the mode is "default".
type Option ¶
type Option func(*Manager)
Option configures a Manager.
func FailClosed ¶
func FailClosed() Option
FailClosed makes hook errors deny the action instead of being ignored. Use it when hooks enforce security policy.
func OnAsyncResult ¶
func OnAsyncResult(f func(AsyncResult)) Option
OnAsyncResult registers a function called when an Async hook finishes, after its context was queued for the session. Use it to wake an idle session when AsyncResult.Rewake is set, for example by starting a run; the queued context is added to that run's input. f runs on the hook's goroutine.
func Parallelism ¶
Parallelism bounds how many hooks of one event run at once. Zero, the default, runs all matching hooks at once, as Claude Code does, except that at most 12 Prompt and Agent hooks run at a time. 1 runs them one at a time in registration order.
func SessionEndTimeout ¶
SessionEndTimeout sets the time budget of the SessionEnd hooks of one dispatch, which is also the timeout of each Command, HTTP and MCPTool hook without its own, like Claude Code's CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS. By default the budget is 1.5 seconds, raised to the longest timeout set on a SessionEnd hook, up to a minute, and hooks without their own timeout get 1.5 seconds.
func StopBlockCap ¶
StopBlockCap sets how many consecutive times Stop or SubagentStop hooks may keep the agent working before the stop is let through anyway, like Claude Code's CLAUDE_CODE_STOP_HOOK_BLOCK_CAP. Zero disables the cap; a negative n restores the default, DefaultStopBlockCap.
type Prompt ¶
type Prompt struct {
Model model.Model
Prompt string
// ModelName, if set, overrides the model name of the request.
ModelName string
// Timeout defaults to 30 seconds. A negative Timeout disables it.
Timeout time.Duration
ContinueOnBlock bool
}
Prompt is a hook that asks a model to evaluate the event, like Claude Code's prompt hooks.
The model receives Prompt with "$ARGUMENTS" replaced by the event's JSON input, the input a Command receives; without "$ARGUMENTS" the input is appended. "\$" stands for a literal "$". It must answer with a JSON object: {"ok": true} lets the action proceed, and {"ok": false, "reason": "..."} blocks it, which depends on the event as in Claude Code:
- Stop and SubagentStop: the agent continues with the reason, unless the answer also has "impossible": true, which lets it stop.
- PreToolUse: the call is denied with the reason; unless ContinueOnBlock is set, the run also halts after the step, with the reason as a warning.
- PostToolUse: unless ContinueOnBlock is set, the run halts with the reason as a warning; with it, the reason is added to the result.
- PostToolUseFailure: the reason is added to the result.
- UserPromptSubmit and other events: Deny with the reason.
An answer that is not such an object is a hook error.
type Result ¶
type Result struct {
Decision Decision
// Reason explains the decision. For Deny it is shown to the model.
Reason string
// AdditionalContext is added to the conversation; where depends on the
// event type.
AdditionalContext string
// UpdatedInput replaces the tool arguments for PreToolUse.
UpdatedInput json.RawMessage
// UpdatedOutput replaces the tool result content for PostToolUse.
UpdatedOutput *string
// Halt stops the run after the current step.
Halt bool
// HaltReason explains Halt.
HaltReason string
// SystemMessage is a warning for the user, not the model. The hook
// middleware logs it.
SystemMessage string
}
Result is the output of a hook. A nil result means Continue.
type SettingsOptions ¶
type SettingsOptions struct {
// Model evaluates "prompt" handlers. Their "model" field becomes
// Prompt.ModelName.
Model model.Model
// Verifier runs "agent" handlers; see agent.HookVerifier.
Verifier Verifier
// MCP calls the tools of "mcp_tool" handlers; see mcp.HookCaller.
MCP MCPCaller
// HTTPClient sends "http" handlers; nil uses http.DefaultClient.
HTTPClient *http.Client
}
SettingsOptions supplies what the hooks of a settings file need beyond commands. Handlers whose dependency is missing are skipped with a warning.
func (SettingsOptions) Load ¶
func (o SettingsOptions) Load(paths ...string) (*Manager, []string, error)
Load reads Claude Code settings files with Parse and returns one manager with all their hooks. Pass the files in increasing precedence, for example ~/.claude/settings.json, .claude/settings.json and .claude/settings.local.json: the last disableAllHooks wins. Files that do not exist are skipped. A handler configured identically in more than one file runs once, as in Claude Code.
func (SettingsOptions) Parse ¶
func (o SettingsOptions) Parse(data []byte) (*Manager, []string, error)
Parse returns a manager with the hooks configured in the "hooks" block of a Claude Code settings file.
Handlers become Command, HTTP, MCPTool, Prompt and Agent hooks: timeout is in seconds, args selects a command's exec form, async and asyncRewake commands are wrapped in Async (async commands get no timeout), and onFailure "block" makes a failing hook deny the action as exit status 2 would, except on Stop and SubagentStop and for async hooks. statusMessage and once are ignored. "disableAllHooks": true yields an empty manager. As in Claude Code, the CLAUDE_CODE_STOP_HOOK_BLOCK_CAP environment variable sets the manager's StopBlockCap, and CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS its SessionEndTimeout in milliseconds.
Handlers agentx cannot run as configured are skipped and described in the returned warnings: prompt, agent and mcp_tool handlers without the matching option, handler types an event does not support in Claude Code (SessionStart runs only command and mcp_tool handlers; prompt and agent handlers run only for UserPromptSubmit, PreToolUse, PostToolUse, PostToolUseFailure, Stop and SubagentStop), the if field, shell "powershell", events agentx does not dispatch, and invalid matchers. The error reports malformed JSON.