Documentation
¶
Overview ¶
Package hooks implements evva's lifecycle extension system.
Hooks are user-authored shell commands or HTTP webhooks that fire at six well-defined moments in the agent loop: SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, Stop, Notification. The package is pure (no logging, no filesystem) except for loader.go (settings.json I/O) and runner.go / http.go (subprocess + HTTP I/O at fire time).
Hooks compose with permissions. PreToolUse runs BEFORE the permission gate and may return a permissionDecision (allow/deny/ask) that overrides the gate, or an updatedInput that mutates the tool's args before the gate sees them. PostToolUse is non-blocking — it can only append additionalContext to the tool's result for the LLM's next turn.
Index ¶
- func Load(workdir, evvaHome string) (*Registry, []Warning)
- type BaseFactory
- type BasePayload
- type Command
- type CommandType
- type Config
- type Decision
- type Dispatcher
- func (d *Dispatcher) FireNotification(ctx context.Context, message, title, ntype string)
- func (d *Dispatcher) FirePostToolUse(ctx context.Context, toolName string, toolInput []byte, toolResponse string, ...) (string, error)
- func (d *Dispatcher) FirePreToolUse(ctx context.Context, toolName string, toolInput []byte, toolUseID string) (*PreToolUseDecision, error)
- func (d *Dispatcher) FireSessionStart(ctx context.Context, source, model string) (initialUserMessage, additionalContext string, err error)
- func (d *Dispatcher) FireStop(ctx context.Context, lastMessage string, stopHookActive bool) (blocked bool, reason string, err error)
- func (d *Dispatcher) FireUserPromptSubmit(ctx context.Context, prompt string) (additionalContext string, blocked bool, blockReason string, err error)
- func (d *Dispatcher) Has(e Event) bool
- type Event
- type NotificationPayload
- type PostToolUsePayload
- type PreToolUseDecision
- type PreToolUsePayload
- type Registry
- type SessionStartPayload
- type StopPayload
- type UserPromptSubmitPayload
- type Warning
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Load ¶
Load reads hooks from <workdir>/.evva/settings.json and <evvaHome>/settings.json. Missing files are not errors. Malformed entries become Warnings; the rest of the file still loads.
Merge order: project hooks first, then user hooks → project hooks fire first in the dispatcher's sequential walk and may short-circuit user hooks via continue:false.
Types ¶
type BaseFactory ¶
type BaseFactory func() BasePayload
BaseFactory builds a fresh BasePayload each time a hook fires. Live fields (PermissionMode in particular) can change mid-session, so the dispatcher rebuilds the base every fire instead of caching it.
type BasePayload ¶
type BasePayload struct {
SessionID string `json:"session_id"`
TranscriptPath string `json:"transcript_path,omitempty"`
Cwd string `json:"cwd"`
PermissionMode string `json:"permission_mode,omitempty"`
AgentID string `json:"agent_id,omitempty"`
AgentType string `json:"agent_type,omitempty"`
HookEventName string `json:"hook_event_name"`
}
BasePayload is the common envelope shared by every hook event. Field names use snake_case so the JSON shipped to hook commands matches Claude Code's settings-file consumers verbatim.
type Command ¶
type Command struct {
Type CommandType
Command string // for TypeCommand: the shell command
URL string // for TypeHTTP: the endpoint
Method string // for TypeHTTP: HTTP method (default POST)
Headers map[string]string // for TypeHTTP
Timeout int // seconds, 0 = use event default
Async bool // fire-and-forget when true (default true for http)
}
Command is one hook entry — either a shell command or an HTTP webhook invocation. Fields are normalized at load time so the dispatcher can treat both kinds uniformly until it dispatches.
type CommandType ¶
type CommandType string
CommandType discriminates the two hook execution backends. v1 supports shell subprocess and HTTP webhook.
const ( TypeCommand CommandType = "command" TypeHTTP CommandType = "http" )
type Config ¶
Config groups a matcher pattern with the hooks that fire when the matcher matches. Matcher is empty for events that don't carry a tool name (SessionStart, Stop, Notification) — those run unconditionally.
type Decision ¶
type Decision struct {
Continue *bool // pointer so we distinguish unset
Decision string // "" | "approve" | "block"
Reason string
SystemMessage string
HookSpecificOutput map[string]any // raw — fields pulled per event
}
Decision is the parsed shape of a hook command's stdout JSON. Fields are optional; an empty Decision means "no opinion, pass through."
The dispatcher interprets these per event:
- PreToolUse: PermissionDecision and HookSpecificOutput.UpdatedInput influence the gate. Decision="block" or Continue=false blocks the tool.
- PostToolUse: HookSpecificOutput.AdditionalContext is appended to the tool result. Block/Continue ignored.
- UserPromptSubmit: AdditionalContext appended to prompt; Decision="block" or Continue=false drops the prompt.
- Stop: Decision="block" or Continue=false re-enters the loop once.
- SessionStart: AdditionalContext and HookSpecificOutput.InitialUserMessage prepended to the first prompt.
- Notification: stdout ignored.
type Dispatcher ¶
type Dispatcher struct {
// contains filtered or unexported fields
}
Dispatcher is the per-agent surface for firing hooks. It holds a reference to the shared *Registry plus per-agent context (logger, base-payload factory). Subagents construct their own Dispatcher from the parent's Registry so agent_id / agent_type are baked into payloads.
A nil *Dispatcher is safe — the Fire methods all noop. Callers don't need to nil-guard.
func NewDispatcher ¶
func NewDispatcher(reg *Registry, logger *slog.Logger, baseFn BaseFactory, projectDir string) *Dispatcher
NewDispatcher builds a Dispatcher. baseFn is called once per fire to snapshot the live envelope.
func (*Dispatcher) FireNotification ¶
func (d *Dispatcher) FireNotification(ctx context.Context, message, title, ntype string)
FireNotification fires every Notification hook. Async by default (hooks default to fire-and-forget for HTTP, the dispatcher honors each hook's Async flag). Always returns nil — notifications can't fail-loud since they're a side channel.
func (*Dispatcher) FirePostToolUse ¶
func (d *Dispatcher) FirePostToolUse(ctx context.Context, toolName string, toolInput []byte, toolResponse string, toolUseID string, isError bool) (string, error)
FirePostToolUse runs every PostToolUse hook whose matcher matches the tool name. Non-blocking — the only side effect is the returned additionalContext string, which the agent loop appends to the tool's result content for the LLM's next turn.
func (*Dispatcher) FirePreToolUse ¶
func (d *Dispatcher) FirePreToolUse(ctx context.Context, toolName string, toolInput []byte, toolUseID string) (*PreToolUseDecision, error)
FirePreToolUse runs every PreToolUse hook whose matcher matches the tool name, sequentially, threading updatedInput forward. Returns the final decision the agent loop applies before the permission gate.
Returns (nil, nil) when no hooks are configured — caller falls through to the gate as normal.
func (*Dispatcher) FireSessionStart ¶
func (d *Dispatcher) FireSessionStart(ctx context.Context, source, model string) (initialUserMessage, additionalContext string, err error)
FireSessionStart fires every SessionStart hook. Returns initialUserMessage (a synthetic user prompt prepended to the session) and additionalContext (appended to the first real prompt).
func (*Dispatcher) FireStop ¶
func (d *Dispatcher) FireStop(ctx context.Context, lastMessage string, stopHookActive bool) (blocked bool, reason string, err error)
FireStop runs every Stop hook. Returns blocked=true if the agent should re-enter the loop with the given reason as a synthetic user message. stopHookActive=true on the re-entry pass guarantees the hook is consulted but its block is no longer honored — prevents infinite loops.
func (*Dispatcher) FireUserPromptSubmit ¶
func (d *Dispatcher) FireUserPromptSubmit(ctx context.Context, prompt string) (additionalContext string, blocked bool, blockReason string, err error)
FireUserPromptSubmit runs every UserPromptSubmit hook. Returns the concatenated additionalContext (appended to the prompt), a blocked flag, and a reason. When blocked the caller should drop the prompt.
func (*Dispatcher) Has ¶
func (d *Dispatcher) Has(e Event) bool
Has reports whether any hooks are configured for e. The agent loop uses this to skip payload-building when no hook would fire.
type NotificationPayload ¶
type NotificationPayload struct {
BasePayload
Message string `json:"message"`
Title string `json:"title,omitempty"`
NType string `json:"notification_type,omitempty"`
}
NotificationPayload fires for out-of-band events: iteration limit, internal errors, approval-needed. NType is a short tag so hooks can route on it (e.g. only Slack-ping on approval_needed).
type PostToolUsePayload ¶
type PostToolUsePayload struct {
BasePayload
ToolName string `json:"tool_name"`
ToolInput json.RawMessage `json:"tool_input"`
ToolResponse string `json:"tool_response"`
IsError bool `json:"is_error"`
ToolUseID string `json:"tool_use_id"`
}
PostToolUsePayload fires after the tool returns. ToolResponse is the tool's serialized result content; IsError mirrors result.IsError.
type PreToolUseDecision ¶
type PreToolUseDecision struct {
PermissionDecision string // "" | "allow" | "deny" | "ask"
Reason string
UpdatedInput []byte // raw JSON of the new tool input, nil if unchanged
AdditionalContext string
Blocked bool
BlockReason string
}
PreToolUseDecision is the resolved verdict from PreToolUse hooks, as consumed by the agent loop. Built by the dispatcher after running all matched hooks sequentially.
type PreToolUsePayload ¶
type PreToolUsePayload struct {
BasePayload
ToolName string `json:"tool_name"`
ToolInput json.RawMessage `json:"tool_input"`
ToolUseID string `json:"tool_use_id"`
}
PreToolUsePayload fires before the permission gate runs. ToolInput is the raw JSON the LLM emitted; a hook can return an updatedInput to mutate it before the tool sees it.
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry holds the configured hooks for the current session. Project + user scopes are merged at load time and stored under one event-keyed map. Project hooks come first in the slice so the dispatcher fires them ahead of user hooks (project hooks may short-circuit user hooks via continue:false).
Safe for concurrent reads; writes (Reload) take the write lock.
func NewRegistry ¶
func NewRegistry() *Registry
NewRegistry returns an empty Registry. The loader populates it; tests can hand-build one for unit coverage of the dispatcher.
func (*Registry) For ¶
For returns the matcher configs for the given event in fire order: project first, then user. Returns nil if no hooks are configured.
The slice is owned by the registry; callers must NOT mutate it.
func (*Registry) HasAny ¶
HasAny reports whether any hooks are configured for e. Useful for fast-path checks so the agent loop can skip building a payload when no hook would fire anyway.
func (*Registry) ReplaceAll ¶
ReplaceAll atomically swaps the registry contents. Used by Load so a settings-file reload doesn't expose a partially-written intermediate state.
type SessionStartPayload ¶
type SessionStartPayload struct {
BasePayload
Source string `json:"source"`
Model string `json:"model,omitempty"`
}
SessionStartPayload fires when an agent first runs. Source is "startup" (initial Run) — "resume" / "clear" / "compact" reserved for later phases.
type StopPayload ¶
type StopPayload struct {
BasePayload
StopHookActive bool `json:"stop_hook_active"`
LastAssistantMessage string `json:"last_assistant_message,omitempty"`
}
StopPayload fires when the main agent reaches a terminal turn (no more tool calls). LastAssistantMessage carries the model's last reply so hooks can summarize / log. StopHookActive is true on a re-entry pass after a previous Stop hook blocked — used to prevent infinite loops.
type UserPromptSubmitPayload ¶
type UserPromptSubmitPayload struct {
BasePayload
Prompt string `json:"prompt"`
}
UserPromptSubmitPayload fires once per user prompt before it's appended to the session.