Documentation
¶
Overview ¶
Package agenthooks lets you author coding-agent hooks once and run them on Claude Code, Cursor, OpenAI Codex, Gemini CLI, OpenCode, and Kimi Code.
A hook program is a normal Go binary: register typed handlers on a Runner and hand control to Main. The library detects the invoking provider, decodes stdin, dispatches, and encodes the response in the provider's dialect — including exit code and stderr discipline. Consumers never see the wire; they see typed events and return typed decisions.
func main() {
r := agenthooks.New(agenthooks.WithPolicy(agenthooks.Policy{
Fail: agenthooks.FailClosed,
AskFallback: agenthooks.FallbackDeny,
}))
r.OnToolPre(func(ctx context.Context, e *agenthooks.ToolPreEvent) (agenthooks.ToolPreDecision, error) {
if e.Tool.Canonical == agenthooks.ToolShell && isDestructive(e.Tool) {
return agenthooks.Deny("blocked by policy"), nil
}
return agenthooks.NoDecision(), nil
})
agenthooks.Main(r)
}
Index ¶
- Constants
- Variables
- func All[E any, D decision](hs ...func(context.Context, *E) (D, error)) func(context.Context, *E) (D, error)
- func Any[E any, D decision](hs ...func(context.Context, *E) (D, error)) func(context.Context, *E) (D, error)
- func CompileMatcher(p Provider, m ToolMatcher) (expr string, ok bool)
- func Logger(ctx context.Context) *slog.Logger
- func Main(r *Runner)
- func SynthesizeToolID(sessionID, turnID, toolName string, input []byte) string
- func When[E any, D decision](m Matcher, h func(context.Context, *E) (D, error)) func(context.Context, *E) (D, error)
- type AgentInfo
- type AskFallback
- type CanonicalTool
- type CapSet
- type Capability
- type CompactEvent
- type Decision
- type DecisionKind
- type DegradeMode
- type DetectionConfidence
- type Event
- type EventKind
- type FailMode
- type FileEditedEvent
- type Interceptor
- type MCPCall
- type MCPInventoryEvent
- type MCPServer
- type Matcher
- type ModelEvent
- type Next
- type NotificationEvent
- type Option
- type PermissionEvent
- type Policy
- type PolicyFunc
- type PromptDecision
- func (d PromptDecision) Blocks() bool
- func (d PromptDecision) Context() []string
- func (d PromptDecision) Kind() DecisionKind
- func (d PromptDecision) Reason() string
- func (d PromptDecision) StopAgent(reason string) PromptDecision
- func (d PromptDecision) StopsAgent() (string, bool)
- func (d PromptDecision) SystemMessage() string
- func (d PromptDecision) WithBlockReason(candidates ...string) PromptDecision
- func (d PromptDecision) WithContext(s string) PromptDecision
- func (d PromptDecision) WithSystemMessage(s string) PromptDecision
- type PromptEvent
- type Provider
- type Quirk
- type Runner
- func (r *Runner) Decide(ctx context.Context, typed any) (Decision, error)
- func (r *Runner) OnAny(fn func(context.Context, *Event) error)
- func (r *Runner) OnCompactPost(hs ...func(context.Context, *CompactEvent) error)
- func (r *Runner) OnCompactPre(hs ...func(context.Context, *CompactEvent) error)
- func (r *Runner) OnFileEdited(hs ...func(context.Context, *FileEditedEvent) error)
- func (r *Runner) OnMCPInventory(hs ...func(context.Context, *MCPInventoryEvent) error)
- func (r *Runner) OnModelRequest(hs ...func(context.Context, *ModelEvent) error)
- func (r *Runner) OnModelResponse(hs ...func(context.Context, *ModelEvent) error)
- func (r *Runner) OnNotification(hs ...func(context.Context, *NotificationEvent) error)
- func (r *Runner) OnOther(nativeName string, fn func(context.Context, *Event) error)
- func (r *Runner) OnPermission(hs ...func(context.Context, *PermissionEvent) (ToolPreDecision, error))
- func (r *Runner) OnPromptSubmitted(hs ...func(context.Context, *PromptEvent) (PromptDecision, error))
- func (r *Runner) OnSessionEnd(hs ...func(context.Context, *SessionEndEvent) error)
- func (r *Runner) OnSessionStart(hs ...func(context.Context, *SessionStartEvent) (SessionStartDecision, error))
- func (r *Runner) OnStop(hs ...func(context.Context, *StopEvent) (StopDecision, error))
- func (r *Runner) OnSubagentStart(hs ...func(context.Context, *SubagentStartEvent) error)
- func (r *Runner) OnSubagentStop(hs ...func(context.Context, *StopEvent) (StopDecision, error))
- func (r *Runner) OnToolError(hs ...func(context.Context, *ToolPostEvent) (ToolPostDecision, error))
- func (r *Runner) OnToolPost(hs ...func(context.Context, *ToolPostEvent) (ToolPostDecision, error))
- func (r *Runner) OnToolPre(hs ...func(context.Context, *ToolPreEvent) (ToolPreDecision, error))
- func (r *Runner) Run(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) int
- func (r *Runner) Use(i Interceptor)
- func (r *Runner) Walk(fn func(StageInfo) error) error
- type SessionEndEvent
- type SessionInfo
- type SessionStartDecision
- func (d SessionStartDecision) Blocks() bool
- func (d SessionStartDecision) Context() []string
- func (d SessionStartDecision) Kind() DecisionKind
- func (d SessionStartDecision) Reason() string
- func (d SessionStartDecision) StopsAgent() (string, bool)
- func (d SessionStartDecision) SystemMessage() string
- func (d SessionStartDecision) WithContext(s string) SessionStartDecision
- func (d SessionStartDecision) WithSystemMessage(s string) SessionStartDecision
- type SessionStartEvent
- type StageInfo
- type StageType
- type StopDecision
- func (d StopDecision) Blocks() bool
- func (d StopDecision) Context() []string
- func (d StopDecision) Instruction() string
- func (d StopDecision) Kind() DecisionKind
- func (d StopDecision) Reason() string
- func (d StopDecision) StopsAgent() (string, bool)
- func (d StopDecision) SystemMessage() string
- func (d StopDecision) WithSystemMessage(s string) StopDecision
- type StopEvent
- type SubagentStartEvent
- type ToolCall
- type ToolMatcher
- type ToolPostDecision
- func (d ToolPostDecision) Blocks() bool
- func (d ToolPostDecision) Context() []string
- func (d ToolPostDecision) Kind() DecisionKind
- func (d ToolPostDecision) Reason() string
- func (d ToolPostDecision) ReplacedOutput() (any, bool)
- func (d ToolPostDecision) StopAgent(reason string) ToolPostDecision
- func (d ToolPostDecision) StopsAgent() (string, bool)
- func (d ToolPostDecision) SystemMessage() string
- func (d ToolPostDecision) WithContext(s string) ToolPostDecision
- func (d ToolPostDecision) WithSystemMessage(s string) ToolPostDecision
- type ToolPostEvent
- type ToolPreDecision
- func (d ToolPreDecision) Blocks() bool
- func (d ToolPreDecision) Context() []string
- func (d ToolPreDecision) Kind() DecisionKind
- func (d ToolPreDecision) Reason() string
- func (d ToolPreDecision) StopAgent(reason string) ToolPreDecision
- func (d ToolPreDecision) StopsAgent() (string, bool)
- func (d ToolPreDecision) SystemMessage() string
- func (d ToolPreDecision) UpdatedInput() (any, bool)
- func (d ToolPreDecision) WithBlockReason(candidates ...string) ToolPreDecision
- func (d ToolPreDecision) WithContext(s string) ToolPreDecision
- func (d ToolPreDecision) WithSystemMessage(s string) ToolPreDecision
- func (d ToolPreDecision) WithUpdatedInput(v any) ToolPreDecision
- type ToolPreEvent
- type Usage
- type Variant
Constants ¶
const DefaultContinuationCap = 5
DefaultContinuationCap bounds ContinueWith loops on providers without a native cap (Cursor Claude-compat mode ships loop_limit: null).
Variables ¶
var ErrLossyUpdate = errors.New("agenthooks: updated input removes keys; provider merge is shallow (lossy)")
ErrLossyUpdate is returned when an input rewrite removes keys on a provider whose update mechanism is a shallow merge (Gemini): the removal cannot be expressed, so honoring the rewrite would silently corrupt the tool call.
var ErrUnsupportedDecision = errors.New("agenthooks: decision unsupported by provider")
ErrUnsupportedDecision is returned (under Policy.Unsupported == Strict) when a handler decision cannot be expressed on the invoking provider.
Functions ¶
func All ¶ added in v0.4.1
func All[E any, D decision](hs ...func(context.Context, *E) (D, error)) func(context.Context, *E) (D, error)
All composes handlers into one handler that runs every handler — no short-circuit, so all findings and side effects are recorded — and then merges: the most restrictive kind wins (deny > ask > allow > neutral, ties to the earliest), Context appends from all decisions in order, the winner's other fields are taken wholesale, and StopAgent is sticky. Errors: every handler still runs, the errors are joined, and any error aborts the combinator.
func Any ¶ added in v0.4.1
func Any[E any, D decision](hs ...func(context.Context, *E) (D, error)) func(context.Context, *E) (D, error)
Any composes handlers into one handler with the same semantics as stacked registration — one rule everywhere: handlers run in order, the first conclusive decision wins and short-circuits the rest, so order is priority. A handler error aborts immediately.
func CompileMatcher ¶
func CompileMatcher(p Provider, m ToolMatcher) (expr string, ok bool)
CompileMatcher renders the matcher in the provider's dialect. ok is false when the dialect can't express it (e.g. Cursor has no matchers), in which case config generation matches broadly and the runner filters in-process.
func Main ¶
func Main(r *Runner)
Main parses argv/stdin, runs the registered handlers, writes the response, and exits with the dialect-correct code. It also redirects the process's stdout so stray handler prints can't corrupt the wire (Gemini parses stderr as the decision when stdout is empty; Codex rejects unknown JSON).
When argv carries --async (rendered by install for telemetry events on providers without native async hooks, quirk #10), Main hands the payload to a detached copy of this process and exits immediately; the child runs the normal path without the flag. Detaching is process management, so it lives here rather than in the testable Run.
func SynthesizeToolID ¶
SynthesizeToolID derives a stable id for providers that omit one: hash(session|turn|tool|input) -> hook_synth_<16 hex>.
func When ¶ added in v0.4.1
func When[E any, D decision](m Matcher, h func(context.Context, *E) (D, error)) func(context.Context, *E) (D, error)
When guards a handler: h runs only when the event carries a tool call the matcher matches; otherwise the stage is neutral. Events without a tool call (prompts, stops, ...) never match.
Types ¶
type AskFallback ¶
type AskFallback int
AskFallback selects the degradation target when Ask is unsupported.
const ( FallbackNoDecision AskFallback = iota FallbackDeny )
type CanonicalTool ¶
type CanonicalTool string
CanonicalTool classifies native tools into a small cross-provider taxonomy.
const ( ToolShell CanonicalTool = "shell" ToolFileRead CanonicalTool = "file.read" ToolFileWrite CanonicalTool = "file.write" ToolFileEdit CanonicalTool = "file.edit" ToolSearch CanonicalTool = "search" ToolFetch CanonicalTool = "fetch" ToolTask CanonicalTool = "task" ToolMCP CanonicalTool = "mcp" ToolOther CanonicalTool = "other" )
func CanonicalToolFor ¶
func CanonicalToolFor(name string) CanonicalTool
CanonicalToolFor classifies a native tool name. MCP names (any dialect) classify as ToolMCP.
type CapSet ¶
type CapSet map[Capability]bool
CapSet is the set of capabilities available on one provider+event pair.
func Capabilities ¶
Capabilities reports what a decision can express for the given provider/variant/event combination.
func (CapSet) Has ¶
func (s CapSet) Has(c Capability) bool
type Capability ¶
type Capability string
Capability names one thing a decision can express. The matrix below encodes which providers honor it on which events, so degradation is explicit and typed instead of silent.
const ( CapDeny Capability = "deny" CapAsk Capability = "ask" CapAllow Capability = "allow" CapUpdateInput Capability = "update-input" CapAddContext Capability = "add-context" CapReplaceOutput Capability = "replace-output" CapContinueAgent Capability = "continue-agent" CapStopAgent Capability = "stop-agent" CapSystemMessage Capability = "system-message" )
type CompactEvent ¶
type Decision ¶ added in v0.4.1
type Decision interface {
Kind() DecisionKind
Reason() string
SystemMessage() string
Context() []string
// Blocks reports whether the decision prevents the gated action: true
// exactly for the kinds whose intent is "the action is prevented" —
// DecisionDeny and DecisionBlockPrompt today. An ask is not blocking
// (it defers to a human), and every observe/continue kind is false.
// Consumers branch on this instead of enumerating kinds; a blocking
// kind added later will return true here without call-site changes.
Blocks() bool
// StopsAgent reports whether the decision requests the agent stop (the
// StopAgent modifier) and, if so, the reason it carries. ok is false for
// decisions that did not request a stop, in which case reason is empty.
// The modifier changes edge behavior, so Runner.Decide callers need it to
// reconstruct the full intent.
StopsAgent() (reason string, ok bool)
// contains filtered or unexported methods
}
Decision is the read-only view every decision type satisfies — the common surface Runner.Decide and middleware Next return. Consumers type-assert to the concrete decision type (ToolPreDecision, StopDecision, ...) when they need kind-specific fields such as Instruction or UpdatedInput. The interface is sealed: only decision values built by this package implement it, so every Decision can be carried back through the pipeline losslessly.
type DecisionKind ¶ added in v0.4.1
type DecisionKind int
DecisionKind identifies the outcome a decision carries. The values are plain ints with append-only ordering: new kinds are only ever added at the end, so persisted values never change meaning.
const ( // DecisionNoDecision defers to the provider's normal flow (NEVER a // forced allow). It is the zero value: a zero-value decision is neutral. DecisionNoDecision DecisionKind = iota DecisionAllow DecisionDeny DecisionAsk DecisionAcceptPrompt DecisionBlockPrompt DecisionFinish DecisionContinue DecisionObserved DecisionFlagOutput DecisionReplaceOutput DecisionContinueSession )
func (DecisionKind) String ¶ added in v0.4.1
func (k DecisionKind) String() string
String returns a stable lower-case name for logs.
type DegradeMode ¶
type DegradeMode int
DegradeMode governs unsupported decisions (e.g. Ask on Codex).
const ( // Degrade maps to the nearest supported intent (Ask->Deny or // Ask->NoDecision per AskFallback) and logs the downgrade. Degrade DegradeMode = iota // Strict treats an unsupported decision as a handler error, which then // follows Policy.Fail. Strict )
type DetectionConfidence ¶
type DetectionConfidence string
DetectionConfidence records how the invoking provider was identified.
const ( DetectionConfig DetectionConfidence = "config" // --provider flag from generated config DetectionEnv DetectionConfidence = "env" // environment variable sniffing DetectionShape DetectionConfidence = "shape" // payload shape sniffing )
type Event ¶
type Event struct {
Provider Provider
Variant Variant
NativeName string // "PreToolUse", "beforeShellExecution", "BeforeTool", ...
Kind EventKind // normalized; KindOther when unmapped
Time time.Time // library receive time; Gemini also supplies its own
Session SessionInfo
Agent *AgentInfo // non-nil inside a subagent context
DetectionConfidence DetectionConfidence
// Backfilled marks an event the provider never sent: some providers skip
// events in some modes (quirks #30, #31), and the runner synthesizes the
// miss on the next delivered event, best-effort. Backfilled events are
// reporting-only — Raw is nil, Can() reports no capabilities, and any
// decision returned by the handler is discarded.
Backfilled bool
// Raw is the verbatim provider payload. Never normalized, never trimmed.
Raw json.RawMessage
// Ext carries embedder-defined extension data keyed by embedder-chosen
// names. The library never reads, writes, or propagates it: it exists for
// applications that construct typed events themselves (e.g. a server-side
// ingest path re-projecting stored payloads) to stamp app-specific
// context their own handlers read back. Nil on every event the library
// decodes.
Ext map[string]any
}
Event is the unified envelope. Raw is always the verbatim provider payload; normalization is a projection over it, never a replacement.
func EventOf ¶
EventOf returns the embedded envelope of any typed event returned by the decoder, or nil if the value is not an agenthooks event. Consumers that dispatch on the concrete types (rather than registering handlers) use it to reach the shared envelope fields.
func (*Event) Can ¶
func (e *Event) Can(c Capability) bool
Can reports whether the event's provider honors the capability. Backfilled events are reporting-only: the provider never sent them, so nothing can be gated or mutated.
type EventKind ¶
type EventKind string
EventKind is the unified, Claude-shaped event taxonomy. The set is open: any native event with no mapping is delivered as KindOther with the native name and raw payload intact.
const ( KindSessionStart EventKind = "session.start" KindSessionEnd EventKind = "session.end" KindMCPInventory EventKind = "mcp.inventory" KindPromptSubmitted EventKind = "prompt.submitted" KindToolPre EventKind = "tool.pre" // gate/rewrite before execution KindToolPost EventKind = "tool.post" KindToolError EventKind = "tool.error" KindPermission EventKind = "permission.request" KindStop EventKind = "agent.stop" // turn finished KindSubagentStart EventKind = "subagent.start" KindSubagentStop EventKind = "subagent.stop" KindCompactPre EventKind = "compact.pre" KindCompactPost EventKind = "compact.post" KindNotification EventKind = "notification" KindFileEdited EventKind = "file.edited" // post-hoc file-change reports KindModelRequest EventKind = "model.request" // gemini BeforeModel, opencode chat.params KindModelResponse EventKind = "model.response" KindOther EventKind = "other" // any unmapped native event )
type FileEditedEvent ¶
type Interceptor ¶ added in v0.4.1
Interceptor is router middleware. It receives the typed event (e.g. *ToolPreEvent) and the rest of the pipeline as next. It may transform the normalized projection in place (Tool.Input, prompt text — Raw and RawInput stay verbatim per the fidelity model), short-circuit by returning without calling next, or post-process next's decision. Interceptors call next at most once.
type MCPCall ¶
type MCPCall struct {
Server string // decoded from mcp__s__t / mcp_s_t / MCP:t + context; "" if undecodable
Tool string // tool name as the MCP server knows it
URL string // remote transport: payload-borne or config-resolved
Command string // stdio transport: command plus args, space-joined
// FromConfig marks URL/Command as resolved from the provider's config
// files rather than carried by the event payload. Config-resolved
// transport is best-effort: treat it as advisory, not authoritative.
FromConfig bool
}
MCPCall carries the decoded MCP tool identity plus the server's transport. Cursor and Gemini MCP events ship url/command in the hook payload; elsewhere the runner resolves them from provider config (quirk #25) and flags the result FromConfig.
func ParseMCPName ¶
ParseMCPName decodes the three MCP tool-name dialects: mcp__server__tool (Claude/Codex), mcp_server_tool (Gemini, best-effort since "_" is ambiguous), and MCP:tool (Cursor, server unknown). It returns nil when the name is not MCP-shaped.
type MCPInventoryEvent ¶ added in v0.5.0
MCPInventoryEvent reports the effective MCP server snapshot independently from any provider-native session event.
type MCPServer ¶ added in v0.5.0
MCPServer is one server in the provider's effective MCP configuration.
type Matcher ¶ added in v0.4.1
Matcher guards handlers by tool identity (see When). ToolMatcher satisfies it; custom implementations (e.g. CEL-backed) plug in without library dependencies.
type ModelEvent ¶
type ModelEvent struct {
Event
}
ModelEvent covers the experimental model.request/model.response kinds (Gemini BeforeModel/AfterModel, OpenCode chat.params). Observe-only in v1.
type Next ¶ added in v0.4.1
Next continues the pipeline from an interceptor: it runs the remaining interceptors and the typed handlers for the event and returns the winning decision.
type NotificationEvent ¶
type Option ¶
type Option func(*Runner)
Option configures a Runner.
func WithDedupDir ¶
WithDedupDir relocates the on-disk cross-process state: the dedup markers used for Cursor's duplicate tool events (quirk #2), launch-context MCP inventory caches (quirks #26, #32), and the prompt-backfill markers (quirks #30, #31). Without this option, MCP inventories use os.UserCacheDir when available; other state uses os.TempDir.
func WithLogger ¶
WithLogger replaces the default logger (stderr, warn level).
func WithPolicyFunc ¶
func WithPolicyFunc(f PolicyFunc) Option
WithPolicyFunc resolves policy per event (ratchets, per-tool strictness, ...).
func WithoutBackfill ¶
func WithoutBackfill() Option
WithoutBackfill disables synthesizing reporting-only events for provider misses (the prompt.submitted backfill for Kimi/Cursor print modes, quirks #30 and #31).
func WithoutDedup ¶
func WithoutDedup() Option
WithoutDedup disables Cursor duplicate-event suppression.
func WithoutMCPListFallback ¶
func WithoutMCPListFallback() Option
WithoutMCPListFallback disables provider CLI inventory probes (`claude mcp list`, `codex mcp list --json`). Direct config-file resolution stays active; launch overrides that cannot be safely reproduced stay unattributed.
func WithoutMCPResolution ¶
func WithoutMCPResolution() Option
WithoutMCPResolution disables filling MCPCall.URL/Command when the payload carries no transport info — both the config-file fast path (quirk #25) and the `claude mcp list` fallback (quirk #26).
type PermissionEvent ¶
type Policy ¶
type Policy struct {
Fail FailMode
Unsupported DegradeMode
AskFallback AskFallback
// ContinuationCap caps ContinueWith loops; 0 means DefaultContinuationCap.
ContinuationCap int
// Timeout bounds handler execution. 0 derives a deadline from the
// --timeout flag in the generated config (90% of it) or defaultDeadline.
Timeout time.Duration
}
Policy declares how the runner behaves when a handler fails or asks for something the provider can't do. FailClosed is enforced with the provider's real mechanism per event; on events with no blocking mechanism it downgrades to logging (see Can(CapDeny)).
type PolicyFunc ¶
PolicyFunc resolves policy per event, enabling consumer patterns like a ratchet (fail-open until first success, then fail-closed).
type PromptDecision ¶
type PromptDecision struct {
// contains filtered or unexported fields
}
PromptDecision gates prompt.submitted events.
func AcceptPrompt ¶
func AcceptPrompt() PromptDecision
func BlockPrompt ¶
func BlockPrompt(reason string) PromptDecision
func (PromptDecision) Blocks ¶ added in v0.4.1
func (d PromptDecision) Blocks() bool
Blocks reports whether the decision prevents the gated action.
func (PromptDecision) Context ¶ added in v0.4.1
func (d PromptDecision) Context() []string
Context reports the context strings attached with WithContext.
func (PromptDecision) Kind ¶ added in v0.4.1
func (d PromptDecision) Kind() DecisionKind
Kind reports the decision outcome.
func (PromptDecision) Reason ¶ added in v0.4.1
func (d PromptDecision) Reason() string
Reason reports the reason the decision was constructed with.
func (PromptDecision) StopAgent ¶
func (d PromptDecision) StopAgent(reason string) PromptDecision
func (PromptDecision) StopsAgent ¶ added in v0.4.1
func (d PromptDecision) StopsAgent() (string, bool)
StopsAgent reports whether StopAgent was requested and the reason.
func (PromptDecision) SystemMessage ¶ added in v0.4.1
func (d PromptDecision) SystemMessage() string
SystemMessage reports the note attached with WithSystemMessage.
func (PromptDecision) WithBlockReason ¶ added in v0.4.1
func (d PromptDecision) WithBlockReason(candidates ...string) PromptDecision
WithBlockReason attaches the user-facing message for a blocking decision, chosen as the first non-empty candidate; see ToolPreDecision.WithBlockReason for the full semantics.
func (PromptDecision) WithContext ¶
func (d PromptDecision) WithContext(s string) PromptDecision
func (PromptDecision) WithSystemMessage ¶
func (d PromptDecision) WithSystemMessage(s string) PromptDecision
type PromptEvent ¶
type Provider ¶
type Provider string
Provider identifies the coding agent that invoked the hook.
const ( ProviderClaudeCode Provider = "claude-code" // incl. cowork/desktop/web variants ProviderCursor Provider = "cursor" // incl. cursor-agent CLI, cloud agents ProviderCodex Provider = "codex" ProviderGemini Provider = "gemini" ProviderOpenCode Provider = "opencode" ProviderKimi Provider = "kimi-code" // Kimi Code CLI ("kimi" accepted as a flag alias) )
type Quirk ¶
type Quirk struct {
ID int
Provider Provider
Versions string // affected version range, free-form ("all", ">=2.0.64", ...)
Event EventKind
Capability Capability // "" when not capability-scoped
Behavior string
Mitigation string
Reference string // upstream issue/doc pointer
}
Quirk is one entry in the machine-readable registry of provider glue this library hides. The registry doubles as the conformance-test plan: every quirk gets a fixture. Docs markers (⚠) are generated from this table.
type Runner ¶
type Runner struct {
// contains filtered or unexported fields
}
Runner holds registered handlers and policy. Handlers per unified event kind are ordered stages: registration appends, dispatch runs them in order and the first conclusive decision wins (see Any). OnAny is additive (observe-only) and runs regardless. Typed handlers gate/mutate; OnAny never does. Middleware installed with Use wraps the typed pipeline.
func (*Runner) Decide ¶ added in v0.4.1
Decide runs the router pipeline for a typed event — OnAny/OnOther observers, middleware, then the registered handlers — and returns the winning decision. It is the server-side entry point: no wire encoding, no capability degradation (applyPolicy is an edge/wire concern — degrading an ask before the caller's boundary can render it would collapse it), and no MCP transport resolution. A stage error (panics included, converted to errors) returns as an error with a nil decision; the caller owns failure semantics. A neutral outcome is the zero decision: Kind() == DecisionNoDecision.
The context deadline is honored even against handlers that ignore ctx; no default deadline is applied.
func (*Runner) OnAny ¶
OnAny receives EVERY event, mapped or not, with the raw payload — the fidelity escape hatch. Observe-only: errors are logged, never gate.
func (*Runner) OnCompactPost ¶
func (r *Runner) OnCompactPost(hs ...func(context.Context, *CompactEvent) error)
func (*Runner) OnCompactPre ¶
func (r *Runner) OnCompactPre(hs ...func(context.Context, *CompactEvent) error)
func (*Runner) OnFileEdited ¶
func (r *Runner) OnFileEdited(hs ...func(context.Context, *FileEditedEvent) error)
func (*Runner) OnMCPInventory ¶ added in v0.5.0
func (r *Runner) OnMCPInventory(hs ...func(context.Context, *MCPInventoryEvent) error)
func (*Runner) OnModelRequest ¶
func (r *Runner) OnModelRequest(hs ...func(context.Context, *ModelEvent) error)
func (*Runner) OnModelResponse ¶
func (r *Runner) OnModelResponse(hs ...func(context.Context, *ModelEvent) error)
func (*Runner) OnNotification ¶
func (r *Runner) OnNotification(hs ...func(context.Context, *NotificationEvent) error)
func (*Runner) OnOther ¶
OnOther receives events whose native name matches, typically ones with no unified mapping (Kind == KindOther). Observe-only.
func (*Runner) OnPermission ¶
func (r *Runner) OnPermission(hs ...func(context.Context, *PermissionEvent) (ToolPreDecision, error))
func (*Runner) OnPromptSubmitted ¶
func (r *Runner) OnPromptSubmitted(hs ...func(context.Context, *PromptEvent) (PromptDecision, error))
func (*Runner) OnSessionEnd ¶
func (r *Runner) OnSessionEnd(hs ...func(context.Context, *SessionEndEvent) error)
func (*Runner) OnSessionStart ¶
func (r *Runner) OnSessionStart(hs ...func(context.Context, *SessionStartEvent) (SessionStartDecision, error))
func (*Runner) OnSubagentStart ¶
func (r *Runner) OnSubagentStart(hs ...func(context.Context, *SubagentStartEvent) error)
func (*Runner) OnSubagentStop ¶
func (*Runner) OnToolError ¶
func (r *Runner) OnToolError(hs ...func(context.Context, *ToolPostEvent) (ToolPostDecision, error))
func (*Runner) OnToolPost ¶
func (r *Runner) OnToolPost(hs ...func(context.Context, *ToolPostEvent) (ToolPostDecision, error))
func (*Runner) OnToolPre ¶
func (r *Runner) OnToolPre(hs ...func(context.Context, *ToolPreEvent) (ToolPreDecision, error))
func (*Runner) Run ¶
func (r *Runner) Run(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) int
Run is the testable core of Main: explicit streams, returns the exit code.
func (*Runner) Use ¶ added in v0.4.1
func (r *Runner) Use(i Interceptor)
Use installs middleware around the typed-handler pipeline, outermost first: the first interceptor installed sees the event first and the decision last. Middleware wraps typed handlers only — OnAny/OnOther observers run before it and never gate.
func (*Runner) Walk ¶ added in v0.4.1
Walk visits every registered top-level stage in dispatch order: OnAny observers, OnOther observers (grouped by native name, sorted), middleware (outermost first), then typed handlers grouped by event kind in registration order. It stops at the first error and returns it.
Combinator internals are opaque: a composition (Any/All/When) registered as one handler visits as one stage — bare funcs carry no metadata, and stage names are always the reflected function names. Register named funcs or method values where readable Walk output matters.
type SessionEndEvent ¶
type SessionInfo ¶
type SessionInfo struct {
ID string // claude/codex/gemini session_id; cursor conversation_id
TurnID string // codex turn_id, cursor generation_id, claude prompt_id ("" if absent)
CWD string
WorkspaceRoots []string // cursor multi-root; others: [CWD] or project dir
TranscriptPath string // "" if unavailable; format is provider-specific (see transcript pkg)
Model string // "" if not reported
PermissionMode string // claude/codex permission_mode; "" elsewhere
UserEmail string // cursor user_email; "" elsewhere
}
SessionInfo carries the normalized session identity fields.
type SessionStartDecision ¶
type SessionStartDecision struct {
// contains filtered or unexported fields
}
SessionStartDecision responds to session.start.
func ContinueSession ¶
func ContinueSession() SessionStartDecision
func (SessionStartDecision) Blocks ¶ added in v0.4.1
func (d SessionStartDecision) Blocks() bool
Blocks reports whether the decision prevents the gated action.
func (SessionStartDecision) Context ¶ added in v0.4.1
func (d SessionStartDecision) Context() []string
Context reports the context strings attached with WithContext.
func (SessionStartDecision) Kind ¶ added in v0.4.1
func (d SessionStartDecision) Kind() DecisionKind
Kind reports the decision outcome.
func (SessionStartDecision) Reason ¶ added in v0.4.1
func (d SessionStartDecision) Reason() string
Reason reports the reason the decision was constructed with.
func (SessionStartDecision) StopsAgent ¶ added in v0.4.1
func (d SessionStartDecision) StopsAgent() (string, bool)
StopsAgent reports whether StopAgent was requested and the reason. Session start decisions never set the modifier, so ok is always false.
func (SessionStartDecision) SystemMessage ¶ added in v0.4.1
func (d SessionStartDecision) SystemMessage() string
SystemMessage reports the note attached with WithSystemMessage.
func (SessionStartDecision) WithContext ¶
func (d SessionStartDecision) WithContext(s string) SessionStartDecision
func (SessionStartDecision) WithSystemMessage ¶
func (d SessionStartDecision) WithSystemMessage(s string) SessionStartDecision
type SessionStartEvent ¶
type StageInfo ¶ added in v0.4.1
type StageInfo struct {
// Kind is the event kind the stage is registered for. It is empty for
// middleware and OnAny observers (they see every event) and KindOther
// for OnOther observers.
Kind EventKind
Type StageType
// Native is the native event name an OnOther observer is registered for.
// It is empty for every other stage (OnOther stages all report
// Kind == KindOther, so the native name is the only way to tell them
// apart in run-order output).
Native string
// Name is the reflected function name — useful for named funcs and
// method values; anonymous closures report their compiler-assigned
// closure names.
Name string
// Pos is the zero-based registration position within the stage's group.
Pos int
}
StageInfo describes one registered top-level stage.
type StageType ¶ added in v0.4.1
type StageType string
StageType classifies a stage visited by Walk.
type StopDecision ¶
type StopDecision struct {
// contains filtered or unexported fields
}
StopDecision responds to agent.stop / subagent.stop.
func ContinueWith ¶
func ContinueWith(instruction string) StopDecision
ContinueWith keeps the agent working: claude decision:block+reason, cursor followup_message, codex continuation prompt, gemini retry. The runner enforces Policy.ContinuationCap so consumers can't build infinite loops on providers without native caps.
func (StopDecision) Blocks ¶ added in v0.4.1
func (d StopDecision) Blocks() bool
Blocks reports whether the decision prevents the gated action.
func (StopDecision) Context ¶ added in v0.4.1
func (d StopDecision) Context() []string
Context reports the context strings attached to the decision.
func (StopDecision) Instruction ¶ added in v0.4.1
func (d StopDecision) Instruction() string
Instruction reports the ContinueWith payload.
func (StopDecision) Kind ¶ added in v0.4.1
func (d StopDecision) Kind() DecisionKind
Kind reports the decision outcome.
func (StopDecision) Reason ¶ added in v0.4.1
func (d StopDecision) Reason() string
Reason reports the reason the decision was constructed with.
func (StopDecision) StopsAgent ¶ added in v0.4.1
func (d StopDecision) StopsAgent() (string, bool)
StopsAgent reports whether StopAgent was requested and the reason. Stop decisions never set the modifier, so ok is always false.
func (StopDecision) SystemMessage ¶ added in v0.4.1
func (d StopDecision) SystemMessage() string
SystemMessage reports the note attached with WithSystemMessage.
func (StopDecision) WithSystemMessage ¶
func (d StopDecision) WithSystemMessage(s string) StopDecision
type StopEvent ¶
type StopEvent struct {
Event
// PreviouslyContinued surfaces stop_hook_active / loop_count uniformly.
PreviouslyContinued bool
// LoopCount is the provider-reported continuation count when available,
// or 1 when only a boolean guard (stop_hook_active) exists.
LoopCount int
// FinalMessage is the last assistant message of the turn when the provider
// includes it on the stop event (Claude/Codex last_assistant_message).
FinalMessage string
// Usage carries end-of-turn token/cost totals when the provider reports
// them on stop (Cursor), nil otherwise.
Usage *Usage
}
type SubagentStartEvent ¶
type SubagentStartEvent struct {
Event
}
type ToolCall ¶
type ToolCall struct {
ID string // native id, or synthesized (see Synthesized)
Synthesized bool // true when the provider omitted an id (Cursor MCP cases)
Name string // native tool name, verbatim
Canonical CanonicalTool // cross-provider classification
MCP *MCPCall // non-nil when the call targets an MCP tool
// Input is ALWAYS a JSON object (the library un-stringifies Cursor's
// string form and wraps non-object inputs as {"value": ...}).
Input json.RawMessage
// RawInput is the input exactly as the provider sent it (nil when the
// provider sent none and Input was constructed from other fields).
RawInput json.RawMessage
}
ToolCall is the normalized view of a tool invocation.
type ToolMatcher ¶
type ToolMatcher struct {
Names []string // exact native tool names
Canonical []CanonicalTool // canonical classes
MCP []string // "server/*" or "server/tool" globs
}
ToolMatcher is the unified tool matcher used both by config generation (compiled to each provider's matcher dialect where expressible) and by the runner's in-process filter (--filter flag) where not.
func MatchCanonical ¶ added in v0.4.1
func MatchCanonical(classes ...CanonicalTool) ToolMatcher
MatchCanonical matches canonical tool classes.
func MatchMCP ¶ added in v0.4.1
func MatchMCP(globs ...string) ToolMatcher
MatchMCP matches MCP tools by "server/tool" glob: "server" alone means "server/*", and "*" matches any MCP tool.
func MatchTools ¶ added in v0.4.1
func MatchTools(names ...string) ToolMatcher
MatchTools matches exact native tool names (case-insensitive). With no arguments it matches every tool (ToolMatcher's empty-matcher semantics).
func ParseToolMatcher ¶
func ParseToolMatcher(s string) (ToolMatcher, error)
ParseToolMatcher parses the --filter flag format produced by Encode.
func (ToolMatcher) Encode ¶
func (m ToolMatcher) Encode() string
Encode serializes the matcher for the --filter flag baked into generated configs whose provider dialect can't express it.
func (ToolMatcher) IsEmpty ¶
func (m ToolMatcher) IsEmpty() bool
IsEmpty reports whether the matcher matches everything.
func (ToolMatcher) Matches ¶
func (m ToolMatcher) Matches(t ToolCall) bool
Matches reports whether the tool call satisfies the matcher. An empty matcher matches every tool.
type ToolPostDecision ¶
type ToolPostDecision struct {
// contains filtered or unexported fields
}
ToolPostDecision responds to tool.post / tool.error.
func FlagOutput ¶
func FlagOutput(reason string) ToolPostDecision
FlagOutput sends feedback about the tool output to the model.
func ReplaceOutput ¶
func ReplaceOutput(v any) ToolPostDecision
ReplaceOutput substitutes the tool output where supported: claude updatedToolOutput, cursor updated_mcp_tool_output (MCP only), gemini tool_output, opencode output mutation.
func (ToolPostDecision) Blocks ¶ added in v0.4.1
func (d ToolPostDecision) Blocks() bool
Blocks reports whether the decision prevents the gated action.
func (ToolPostDecision) Context ¶ added in v0.4.1
func (d ToolPostDecision) Context() []string
Context reports the context strings attached with WithContext.
func (ToolPostDecision) Kind ¶ added in v0.4.1
func (d ToolPostDecision) Kind() DecisionKind
Kind reports the decision outcome.
func (ToolPostDecision) Reason ¶ added in v0.4.1
func (d ToolPostDecision) Reason() string
Reason reports the reason the decision was constructed with.
func (ToolPostDecision) ReplacedOutput ¶ added in v0.4.1
func (d ToolPostDecision) ReplacedOutput() (any, bool)
ReplacedOutput reports the substituted tool output and whether the decision was constructed with ReplaceOutput.
func (ToolPostDecision) StopAgent ¶
func (d ToolPostDecision) StopAgent(reason string) ToolPostDecision
func (ToolPostDecision) StopsAgent ¶ added in v0.4.1
func (d ToolPostDecision) StopsAgent() (string, bool)
StopsAgent reports whether StopAgent was requested and the reason.
func (ToolPostDecision) SystemMessage ¶ added in v0.4.1
func (d ToolPostDecision) SystemMessage() string
SystemMessage reports the note attached with WithSystemMessage.
func (ToolPostDecision) WithContext ¶
func (d ToolPostDecision) WithContext(s string) ToolPostDecision
func (ToolPostDecision) WithSystemMessage ¶
func (d ToolPostDecision) WithSystemMessage(s string) ToolPostDecision
type ToolPostEvent ¶
type ToolPostEvent struct {
Event
Tool ToolCall
Output json.RawMessage // provider's tool_response/output, verbatim
Failed bool // true on tool.error and error-carrying tool.post
Error string
// DurationMS is the tool execution time in milliseconds when the provider
// reports one (Cursor duration/duration_ms), nil otherwise.
DurationMS *float64
}
type ToolPreDecision ¶
type ToolPreDecision struct {
// contains filtered or unexported fields
}
ToolPreDecision gates tool.pre and permission.request events.
func Allow ¶
func Allow() ToolPreDecision
Allow skips the permission prompt where supported. It never loosens policy: provider-side deny rules still apply.
func AskUser ¶
func AskUser(reason string) ToolPreDecision
AskUser forces a confirmation prompt where the provider supports one. Behavior on providers without ask is governed by Policy.AskFallback.
func Deny ¶
func Deny(reason string) ToolPreDecision
Deny blocks the tool call with feedback for the model.
func NoDecision ¶
func NoDecision() ToolPreDecision
NoDecision defers to the provider's normal permission flow. It is NEVER a forced allow: the codecs emit each provider's correct "no opinion" form.
func (ToolPreDecision) Blocks ¶ added in v0.4.1
func (d ToolPreDecision) Blocks() bool
Blocks reports whether the decision prevents the gated action.
func (ToolPreDecision) Context ¶ added in v0.4.1
func (d ToolPreDecision) Context() []string
Context reports the context strings attached with WithContext.
func (ToolPreDecision) Kind ¶ added in v0.4.1
func (d ToolPreDecision) Kind() DecisionKind
Kind reports the decision outcome.
func (ToolPreDecision) Reason ¶ added in v0.4.1
func (d ToolPreDecision) Reason() string
Reason reports the reason the decision was constructed with.
func (ToolPreDecision) StopAgent ¶
func (d ToolPreDecision) StopAgent(reason string) ToolPreDecision
StopAgent requests continue:false where supported.
func (ToolPreDecision) StopsAgent ¶ added in v0.4.1
func (d ToolPreDecision) StopsAgent() (string, bool)
StopsAgent reports whether StopAgent was requested and the reason.
func (ToolPreDecision) SystemMessage ¶ added in v0.4.1
func (d ToolPreDecision) SystemMessage() string
SystemMessage reports the note attached with WithSystemMessage.
func (ToolPreDecision) UpdatedInput ¶ added in v0.4.1
func (d ToolPreDecision) UpdatedInput() (any, bool)
UpdatedInput reports the rewritten tool arguments and whether a rewrite was attached with WithUpdatedInput.
func (ToolPreDecision) WithBlockReason ¶ added in v0.4.1
func (d ToolPreDecision) WithBlockReason(candidates ...string) ToolPreDecision
WithBlockReason attaches the user-facing message for a blocking decision, chosen as the first non-empty candidate. Callers pass ordered fallbacks — e.g. a policy's custom user-facing message followed by the audit reason the decision was constructed with — and the library picks the one to surface. Candidates are compared verbatim: only the empty string is skipped, so a whitespace-only candidate counts as present. When every candidate is empty the decision is returned unchanged. The chosen message occupies the same channel as WithSystemMessage and is read back with SystemMessage.
func (ToolPreDecision) WithContext ¶
func (d ToolPreDecision) WithContext(s string) ToolPreDecision
WithContext injects context for the model where supported.
func (ToolPreDecision) WithSystemMessage ¶
func (d ToolPreDecision) WithSystemMessage(s string) ToolPreDecision
WithSystemMessage attaches a user-facing note where supported.
func (ToolPreDecision) WithUpdatedInput ¶
func (d ToolPreDecision) WithUpdatedInput(v any) ToolPreDecision
WithUpdatedInput rewrites the tool arguments before execution.
type ToolPreEvent ¶
type Usage ¶
type Usage struct {
InputTokens *int
OutputTokens *int
CacheReadTokens *int
CacheWriteTokens *int
Cost *float64
LoopCount *int
Status string
}
Usage carries the token and cost totals a provider reports at the end of a turn. Pointer fields distinguish "reported as zero" from "not reported".
Source Files
¶
- agenthooks.go
- backfill.go
- capability.go
- claudelaunch.go
- codec.go
- codec_claudecode.go
- codec_codex.go
- codec_cursor.go
- codec_gemini.go
- codec_kimi.go
- codec_opencode.go
- codexlaunch.go
- decision.go
- dedup.go
- detach.go
- detach_unix.go
- detect.go
- event.go
- log.go
- matcher.go
- mcpinventory.go
- mcplistcache.go
- mcplock_unix.go
- mcpresolve.go
- policy.go
- promptargv.go
- promptargv_linux.go
- quirks.go
- router.go
- serve.go
Directories
¶
| Path | Synopsis |
|---|---|
|
Package agenthookstest provides the golden fixture corpus and a fake-provider harness so hook binaries built on agenthooks can be integration-tested in CI without the actual agents (DESIGN.md §10).
|
Package agenthookstest provides the golden fixture corpus and a fake-provider harness so hook binaries built on agenthooks can be integration-tested in CI without the actual agents (DESIGN.md §10). |
|
Package e2e exercises agenthooks against real, locally installed coding agents: it renders hook configuration with the install package, points it at a recorder binary built from testdata/recorder, drives one headless agent turn, and asserts on the events that actually arrived and on decision enforcement (a deny must block the tool).
|
Package e2e exercises agenthooks against real, locally installed coding agents: it renders hook configuration with the install package, points it at a recorder binary built from testdata/recorder, drives one headless agent turn, and asserts on the events that actually arrived and on decision enforcement (a deny must block the tool). |
|
examples
|
|
|
secretguard
command
Command secretguard is an example agenthooks consumer: it scans every tool call's input for credential-shaped strings before execution.
|
Command secretguard is an example agenthooks consumer: it scans every tool call's input for credential-shaped strings before execution. |
|
Package install renders and installs provider hook configurations from a single Go Manifest: correct hooks.json / settings.json / config.toml / plugin scaffolding per provider, with the per-provider timing/async/ fail-mode workarounds baked in (DESIGN.md §7).
|
Package install renders and installs provider hook configurations from a single Go Manifest: correct hooks.json / settings.json / config.toml / plugin scaffolding per provider, with the per-provider timing/async/ fail-mode workarounds baked in (DESIGN.md §7). |
|
internal
|
|
|
jsonx
Package jsonx provides JSON decoding with unknown-field capture.
|
Package jsonx provides JSON decoding with unknown-field capture. |
|
transcriptio
Package transcriptio is the transcript-parsing core shared by the public transcript package and the runner's prompt backfill (the root package cannot import transcript without a cycle).
|
Package transcriptio is the transcript-parsing core shared by the public transcript package and the runner's prompt backfill (the root package cannot import transcript without a cycle). |
|
provider
|
|
|
claudecode
Package claudecode provides complete typed views over native Claude Code hook payloads.
|
Package claudecode provides complete typed views over native Claude Code hook payloads. |
|
codex
Package codex provides typed views over native Codex hook payloads.
|
Package codex provides typed views over native Codex hook payloads. |
|
cursor
Package cursor provides typed views over native Cursor hook payloads (IDE, cursor-agent CLI, and cloud agents).
|
Package cursor provides typed views over native Cursor hook payloads (IDE, cursor-agent CLI, and cloud agents). |
|
gemini
Package gemini provides typed views over native Gemini CLI hook payloads.
|
Package gemini provides typed views over native Gemini CLI hook payloads. |
|
kimicode
Package kimicode provides complete typed views over native Kimi Code CLI hook payloads.
|
Package kimicode provides complete typed views over native Kimi Code CLI hook payloads. |
|
opencode
Package opencode provides typed views over the NDJSON frames the generated agenthooks OpenCode shim plugin proxies from OpenCode's in-process plugin hooks (DESIGN.md §8).
|
Package opencode provides typed views over the NDJSON frames the generated agenthooks OpenCode shim plugin proxies from OpenCode's in-process plugin hooks (DESIGN.md §8). |
|
Package transcript provides best-effort JSONL readers for provider transcript files (Event.Session.TranscriptPath).
|
Package transcript provides best-effort JSONL readers for provider transcript files (Event.Session.TranscriptPath). |