agenthooks

package module
v0.4.1-ordered-mcp-inv... Latest Latest
Warning

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

Go to latest
Published: Aug 6, 2026 License: MIT Imports: 24 Imported by: 0

README


agenthooks

Author coding-agent hooks once in Go; run them on Claude Code, Cursor, OpenAI Codex, Gemini CLI, OpenCode, and Kimi Code.

Go Doc Release Go Report Card
Go Version Platform Support GitHub stars
Built by Speakeasy Software License

One clear interface, zero data-fidelity loss: the library owns the wire (JSON dialects, exit codes, stderr discipline, provider quirks), the consumer owns the logic. See DESIGN.md for the full design and the provider research behind it.

package main

import (
	"context"

	"github.com/speakeasy-api/agenthooks"
)

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 {
			return agenthooks.AskUser("shell commands need confirmation"), nil
		}
		return agenthooks.NoDecision(), nil
	})

	// Fidelity escape hatch: every event, mapped or not, raw payload intact.
	r.OnAny(func(ctx context.Context, e *agenthooks.Event) error {
		agenthooks.Logger(ctx).Info("event", "provider", e.Provider, "native", e.NativeName)
		return nil
	})

	agenthooks.Main(r)
}

Composing handlers

Registration is variadic and stacks: handlers run in order, a neutral decision falls through, and the first conclusive decision wins. Combinators turn handlers into handlers, so compositions register exactly like leaves:

r.OnToolPre(
	// Gate MCP calls; everything else falls through to the next stage.
	agenthooks.When(agenthooks.MatchMCP("*"), gateMCP),
	// Run every check, merge: most restrictive kind wins, contexts append.
	agenthooks.All(auditCommand, agenthooks.When(agenthooks.MatchCanonical(agenthooks.ToolShell), gateShell)),
)

// Middleware wraps the typed pipeline, outermost first: transform the
// normalized projection, short-circuit, or post-process the decision.
r.Use(func(ctx context.Context, typed any, next agenthooks.Next) (agenthooks.Decision, error) {
	return next(ctx, typed)
})
  • Any(hs...) — first conclusive decision wins, short-circuits (identical to stacked registration).
  • All(hs...) — every handler runs; the most restrictive kind wins (deny > ask > allow > neutral), contexts append in order, errors join.
  • When(m, h) — guard a handler by tool identity. Matcher is a one-method interface; MatchTools/MatchMCP/MatchCanonical wrap ToolMatcher, and custom (e.g. CEL-backed) matchers plug in.
  • Walk(fn) — introspect the registered stages in dispatch order. Stage names are the reflected function names, so register named funcs or method values where readable Walk output matters.

Embedding the runner outside a hook process (e.g. server-side)? r.Decide(ctx, event) runs the same pipeline — observers, middleware, handlers — and returns the winning Decision (readable via Kind(), Reason(), Context(), ...) with no wire encoding and no capability degradation; stage errors return as errors.

Packages

Package Purpose
agenthooks Envelope, decisions, capability matrix, policy, runtime (Main/Run), quirk registry
provider/{claudecode,codex,cursor,gemini,opencode,kimicode} Complete typed native structs with unknown-field capture — the fidelity guarantee
install One Go Manifest → correct hooks.json / settings.json / config.toml / plugin scaffolding per provider, workarounds baked in
transcript Best-effort JSONL transcript readers
agenthookstest Fixture corpus, in-process harness, fake-provider spawner
e2e Opt-in end-to-end suite driving real local agent CLIs (AGENTHOOKS_E2E=1 go test ./e2e)
examples/secretguard Working example: detect secrets in tool inputs, prompt the user to accept the risk or block

Installing hooks

m := install.Manifest{
	Command: []string{"/usr/local/bin/myhooks"},
	Hooks: []install.HookSpec{
		{Kind: agenthooks.KindToolPre, Blocking: true, Timeout: 30 * time.Second},
		{Kind: agenthooks.KindStop, Blocking: true},
	},
	Identity: install.Identity{Name: "myhooks", Version: "1.0.0"},
	Fail:     agenthooks.FailClosed,
}
err := install.Install(ctx, m, install.Target{
	Provider: agenthooks.ProviderClaudeCode,
	Scope:    install.ScopeProject,
	Dir:      repoRoot,
})

Generated configs bake in the argv contract (mybinary agenthooks run --provider=...), per-provider timeout units, async workarounds (sync Stop on Claude cowork, backgrounder wrapper on Codex), Cursor failClosed, and Codex trust-hash pre-seeding.

Semantics worth knowing

  • NoDecision() != Allow(): an empty-opinion response defers to the provider's own permission flow; it never force-allows.
  • Decisions are readable: DecisionKind constants (DecisionDeny, DecisionAsk, ...) with String() for logs, read accessors on every decision type — including Blocks(), true exactly when the decision prevents the action (deny, block-prompt), so boundaries never enumerate kinds — and the common Decision interface returned by Runner.Decide and middleware.
  • Capability degradation is explicit: Policy.Unsupported chooses Degrade (nearest supported intent, logged) or Strict (handler error → Policy.Fail). Check e.Can(agenthooks.CapAsk) when you care.
  • Event.Raw is the verbatim provider payload, always. Unknown fields decode into Extra on every native struct. Normalization is a projection.
  • Provider gaps are backfilled best-effort: Kimi's and Cursor's print modes never fire their prompt-submitted hooks, so the runner synthesizes a reporting-only prompt.submitted before the next event that implies one — once per recovered prompt per session, so resumed headless turns (-p --resume) backfill again. Backfilled events carry Backfilled: true, a nil Raw (nothing is fabricated), no capabilities, and any returned decision is discarded — the prompt already reached the model. To gate on a recovered prompt, record what the PromptSubmitted handler saw and deny in the triggering event's handler: the backfill dispatches in the same process immediately before it. The prompt text is recovered best-effort — from Kimi's session store; on Cursor from the session transcript every hook payload names (covering stdin-piped prompts), falling back to the agent process's own argv. Disable with WithoutBackfill().
  • MCP tool calls carry the target server's transport: MCPCall.URL/Command come from Cursor and Gemini MCP payloads and are otherwise resolved from the provider's own MCP config files (.mcp.json, ~/.claude.json, ~/.codex/config.toml, .cursor/mcp.json, .gemini/settings.json plus extension manifests, .kimi-code/mcp.json, ~/.kimi/mcp.json, opencode.json(c)). OpenCode's shim uses the running server's resolved inventory, including custom, inline, remote, and managed configuration. Codex replays launch -c overrides and profiles into a detached codex mcp list --json warm, keyed by the recovered launch context. On Claude Code, servers absent from config files (plugins, claude.ai connectors) are attributed via claude mcp list, started as a detached SessionStart warm in the launching process's project and configuration context. The first MCP hook waits for that same context's in-flight snapshot only when discovery has not finished. On Claude Code 2.1.214+, launch-only --mcp-config, --strict-mcp-config, --settings, --setting-sources, --plugin-dir, --bare, and --safe-mode semantics are recovered through CLAUDE_PID; older versions fall back to the ordinary on-disk context. Inventories are cached briefly by a secret-safe context digest under the user cache directory and replaced on refresh so removals propagate. Remote --plugin-url servers stay unattributed rather than being refetched with potentially different code during hook dispatch. Config-resolved transport is flagged FromConfig and is best-effort — ambiguous or unrecoverable matches stay empty. Disable with WithoutMCPResolution() (everything) or WithoutMCPListFallback() (provider CLI probes).
  • The quirk registry (agenthooks.Quirks()) is the machine-readable list of provider glue this library hides, and doubles as the conformance-test plan.

Contributing

This repository is maintained by Speakeasy, but we welcome and encourage contributions from the community to help improve its capabilities and stability.

How to Contribute

  1. Discussions: Have a feature request or want to discuss the roadmap? Use GitHub Discussions to share your ideas and engage with the community.

  2. Issues: Found a bug or technical issue? Open a GitHub Issue to report it with details about the problem. Provider-behavior reports are especially valuable — the quirk registry only grows through observation.

  3. Pull Requests: We welcome pull requests! If you'd like to contribute code:

    • Fork the repository
    • Create a new branch for your feature/fix
    • Submit a PR with a clear description of the changes and any related issues
  4. Feedback: Share your experience using the library or suggest improvements.

All contributions, whether they're feature requests, bug reports, or code changes, help make this project better for everyone. Please ensure your contributions adhere to the existing code style and include appropriate tests where applicable.

License

Released under the MIT License.

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

View Source
const DefaultContinuationCap = 5

DefaultContinuationCap bounds ContinueWith loops on providers without a native cap (Cursor Claude-compat mode ships loop_limit: null).

Variables

View Source
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.

View Source
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 Logger

func Logger(ctx context.Context) *slog.Logger

Logger returns the runner's logger from a handler context.

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

func SynthesizeToolID(sessionID, turnID, toolName string, input []byte) string

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 AgentInfo

type AgentInfo struct {
	ID   string
	Type string // provider's subagent type name
}

AgentInfo describes the subagent context, when the event fired inside one.

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

func Capabilities(p Provider, v Variant, k EventKind) CapSet

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 CompactEvent struct {
	Event
	Trigger      string
	Instructions string
}

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

func EventOf(typed any) *Event

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.

func (*Event) RawField

func (e *Event) RawField(path string) json.RawMessage

RawField resolves a dot-separated path (object keys and array indexes) into the raw payload. It returns nil when the path does not resolve.

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 FailMode

type FailMode int

FailMode governs what a handler error, panic, or timeout means.

const (
	FailOpen   FailMode = iota // handler error/timeout -> NoDecision
	FailClosed                 // handler error/timeout -> Deny (where possible)
)

type FileEditedEvent

type FileEditedEvent struct {
	Event
	Path string
}

type Interceptor added in v0.4.1

type Interceptor func(ctx context.Context, typed any, next Next) (Decision, error)

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

func ParseMCPName(name string) *MCPCall

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

type MCPInventoryEvent struct {
	Event
	Servers []MCPServer
}

MCPInventoryEvent reports the effective MCP server snapshot independently from any provider-native session event.

type MCPServer added in v0.5.0

type MCPServer struct {
	Name    string
	URL     string
	Command string
}

MCPServer is one server in the provider's effective MCP configuration.

type Matcher added in v0.4.1

type Matcher interface {
	Matches(ToolCall) bool
}

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

type Next func(ctx context.Context, typed any) (Decision, error)

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 NotificationEvent struct {
	Event
	Message string
}

type Option

type Option func(*Runner)

Option configures a Runner.

func WithDedupDir

func WithDedupDir(dir string) Option

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

func WithLogger(l *slog.Logger) Option

WithLogger replaces the default logger (stderr, warn level).

func WithPolicy

func WithPolicy(p Policy) Option

WithPolicy sets a static policy for every event.

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 PermissionEvent struct {
	Event
	Tool ToolCall
}

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

type PolicyFunc func(*Event) Policy

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 PromptEvent struct {
	Event
	Prompt string
}

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.

func Quirks

func Quirks() []Quirk

Quirks returns the full registry, ordered by ID.

func QuirksFor

func QuirksFor(p Provider) []Quirk

QuirksFor filters the registry by provider.

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 New

func New(opts ...Option) *Runner

New builds a Runner. Default policy: FailOpen, Degrade, FallbackNoDecision.

func (*Runner) Decide added in v0.4.1

func (r *Runner) Decide(ctx context.Context, typed any) (Decision, error)

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

func (r *Runner) OnAny(fn func(context.Context, *Event) error)

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

func (r *Runner) OnOther(nativeName string, fn func(context.Context, *Event) error)

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) OnStop

func (r *Runner) OnStop(hs ...func(context.Context, *StopEvent) (StopDecision, error))

func (*Runner) OnSubagentStart

func (r *Runner) OnSubagentStart(hs ...func(context.Context, *SubagentStartEvent) error)

func (*Runner) OnSubagentStop

func (r *Runner) OnSubagentStop(hs ...func(context.Context, *StopEvent) (StopDecision, error))

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

func (r *Runner) Walk(fn func(StageInfo) error) error

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 SessionEndEvent struct {
	Event
	Reason string
}

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

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 (SessionStartDecision) WithSystemMessage

func (d SessionStartDecision) WithSystemMessage(s string) SessionStartDecision

type SessionStartEvent

type SessionStartEvent struct {
	Event
	Source string // "startup", "resume", ... (provider-specific vocabulary)
}

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.

const (
	StageMiddleware StageType = "middleware" // installed with Use
	StageObserver   StageType = "observer"   // OnAny / OnOther
	StageHandler    StageType = "handler"    // typed On<Kind> registration
)

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 Finish

func Finish() StopDecision

Finish lets the agent stop.

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 Observed

func Observed() ToolPostDecision

Observed acknowledges the event with no opinion.

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 ToolPreEvent struct {
	Event
	Tool ToolCall
}

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".

type Variant

type Variant string

Variant refines Provider where runtime behavior genuinely differs.

const (
	VariantUnknown Variant = ""
	VariantCLI     Variant = "cli"
	VariantIDE     Variant = "ide"
	VariantCloud   Variant = "cloud"
	VariantCowork  Variant = "cowork"
	VariantRemote  Variant = "remote"
)

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).

Jump to

Keyboard shortcuts

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