hooks

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: May 21, 2026 License: MIT Imports: 16 Imported by: 0

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

Constants

This section is empty.

Variables

This section is empty.

Functions

func Load

func Load(workdir, evvaHome string) (*Registry, []Warning)

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

type Config struct {
	Matcher string // tool-name glob; empty = match-all
	Hooks   []Command
}

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 Event

type Event string

Event identifies one of the six hook fire points.

const (
	EventSessionStart     Event = "SessionStart"
	EventUserPromptSubmit Event = "UserPromptSubmit"
	EventPreToolUse       Event = "PreToolUse"
	EventPostToolUse      Event = "PostToolUse"
	EventStop             Event = "Stop"
	EventNotification     Event = "Notification"
)

func (Event) Valid

func (e Event) Valid() bool

Valid reports whether e is a known event name.

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

func (r *Registry) For(e Event) []Config

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

func (r *Registry) HasAny(e Event) bool

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

func (r *Registry) ReplaceAll(byEvent map[Event][]Config)

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.

type Warning

type Warning struct {
	Path string
	Err  error
}

Warning is a non-fatal load error. The caller surfaces these on stderr like permission warnings; the agent still starts so a malformed settings.json doesn't brick the session.

func (Warning) Error

func (w Warning) Error() string

Jump to

Keyboard shortcuts

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