hook

package
v0.2.0-alpha.13 Latest Latest
Warning

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

Go to latest
Published: Sep 18, 2026 License: Apache-2.0 Imports: 16 Imported by: 0

Documentation

Index

Constants

View Source
const BlockingExitCode = 2

BlockingExitCode is the exit status a command hook can use to deny an action without producing a structured JSON response. Mirrors Claude Code.

View Source
const HTTPBlockingStatus = 422

HTTPBlockingStatus is the HTTP status code an endpoint can use to deny an action without producing a structured JSON response. Picked to match "Unprocessable Entity" semantics; mirrors the spirit of CommandDriver's exit-2 contract.

Variables

This section is empty.

Functions

func NewDriverRegistry

func NewDriverRegistry(deps Deps) map[string]Driver

NewDriverRegistry builds the per-type driver map used by the HookManager. Drivers with external dependencies are included only when their caller is available; dispatch then skips the corresponding configured entries.

Types

type CommandDriver

type CommandDriver struct {
	// contains filtered or unexported fields
}

CommandDriver runs a shell command for each invocation. The HookInput is serialized as JSON and written to the command's stdin. Communication follows the Claude-Code-compatible contract:

  • Exit 0: success. Optional JSON on stdout may select the decision via {"decision":"block","reason":"..."}. Empty or non-JSON stdout = allow.
  • Exit 2: explicit block; stderr text is surfaced as the reason.
  • Other non-zero exit / timeout: fails open (allows) with a warning log.

When the sandbox is active, entry.Command runs through it exactly as the Bash tool's own commands do -- same wrap, same excluded_commands, same scrubbed environment -- so a hook cannot reach what the sandbox exists to contain. Unlike Bash, a hook has no dangerously_disable_sandbox escape hatch: hooks are config-authored automation, not an LLM-chosen call the operator is watching turn by turn, so there is no per-invocation argument for one to opt out with.

func NewCommandDriver

func NewCommandDriver(sandbox agent.SandboxView) *CommandDriver

NewCommandDriver constructs a command driver. sandbox nil is treated as agent.NoopSandbox{} -- no enforcement, matching every other SandboxView consumer's nil-guard.

func (*CommandDriver) Run

Run executes entry.Command with the HookInput JSON on stdin.

func (*CommandDriver) Type

func (*CommandDriver) Type() string

Type satisfies Driver.

type Deps

type Deps struct {
	MCPCaller MCPCaller
	LLMCaller LLMCaller
	// Sandbox gates the command and http drivers the same way it gates the
	// Bash and WebFetch tools, so a hook cannot reach a boundary those tools
	// are confined by. Nil (the zero value) is treated as agent.NoopSandbox{}
	// by NewCommandDriver/NewHTTPDriver, matching WithSandbox's nil-guard
	// elsewhere -- no enforcement, today's pre-sandbox behavior.
	Sandbox agent.SandboxView
}

Deps groups the externals that drivers need to be constructed. It is passed to NewDriverRegistry. Nil fields disable the corresponding driver type (the registry skips it instead of returning a broken implementation).

type Driver

type Driver interface {
	// Type returns the value the manager uses to address this driver. Must
	// match the values produced by corehook.Entry.ResolvedType.
	Type() string
	// Run executes entry against in and returns the resulting decision.
	Run(ctx context.Context, entry corehook.Entry, in agent.HookInput) agent.HookOutput
}

Driver executes one configured hook entry and returns the resulting HookOutput. Implementations are stateless except for cheap caches (e.g. compiled regex). Dispatch concerns — matcher evaluation, entry ordering, first-block-wins aggregation — live in the manager, not here.

A Driver must fail open on internal errors: hook failures should never silently break the agent loop. Returning a blocking HookOutput is reserved for explicit policy decisions surfaced by the hook itself (e.g. command exit 2, HTTP 422, JSON {"decision":"block"}).

type HTTPDriver

type HTTPDriver struct {
	// Client is the http.Client used for outbound requests. Tests inject
	// their own; production constructs one with the per-entry timeout.
	Client *http.Client
	// contains filtered or unexported fields
}

HTTPDriver posts the HookInput JSON to entry.URL and parses the response.

  • 2xx with no body / non-JSON body: allow.
  • 2xx with JSON {"decision":"block","reason":"..."}: block.
  • HTTPBlockingStatus (422): block with the response body as reason.
  • Any other non-2xx, network error, or timeout: fail open with a warn.

Header values may contain "$VAR" / "${VAR}" placeholders. Only env vars listed in entry.AllowedEnv are substituted — this prevents accidental leakage of arbitrary process env into outbound requests.

When the sandbox is active, entry.URL's host is checked against the same allowed_domains/denied_domains policy WebFetch enforces, before the request is ever built — a hook is a non-bash egress path exactly like WebFetch is, so it honors the same allow-list rather than reaching the network unconstrained.

func NewHTTPDriver

func NewHTTPDriver(sandbox agent.SandboxView) *HTTPDriver

NewHTTPDriver returns a driver with a default client. Per-entry timeout is applied via context, so the client itself does not need a global timeout. sandbox nil is treated as agent.NoopSandbox{}, matching every other SandboxView consumer's nil-guard.

func (*HTTPDriver) Run

Run executes one HTTP hook entry.

func (HTTPDriver) Type

func (HTTPDriver) Type() string

Type satisfies Driver.

type LLMCaller

type LLMCaller interface {
	CompleteHookPrompt(ctx context.Context, model, prompt string) (string, error)
}

LLMCaller runs a single-turn prompt against a model and returns the text content. An empty model means "use the default fast model". Implementations live in agentapp.

type MCPCaller

type MCPCaller interface {
	CallMCPTool(ctx context.Context, server, tool string, input map[string]any) (string, error)
}

MCPCaller invokes a named tool on a named MCP server. Implementations live in agentapp where MCPManager is wired; the MCPDriver depends only on this interface so infra/hook stays free of agentapp imports.

type MCPDriver

type MCPDriver struct {
	// contains filtered or unexported fields
}

MCPDriver invokes a tool on an already-connected MCP server as a hook. The driver delegates the actual MCP call to a MCPCaller (provided by agentapp). Values inside entry.Input may reference fields of the HookInput payload via "${field}" placeholders (e.g. "${tool_args.path}") so the hook script can pass per-event data to the MCP tool.

The tool's text result is parsed as JSON when it looks like a hook output object ({decision, reason}); other text is treated as allow.

func NewMCPDriver

func NewMCPDriver(caller MCPCaller) *MCPDriver

NewMCPDriver returns a driver backed by caller. A nil caller makes the driver fail open with a warning at dispatch time (consistent with other missing-driver scenarios).

func (*MCPDriver) Run

Run invokes the configured MCP tool.

func (MCPDriver) Type

func (MCPDriver) Type() string

Type satisfies Driver.

type PromptDriver

type PromptDriver struct {
	// contains filtered or unexported fields
}

PromptDriver runs a single-turn LLM prompt as a hook. The literal "$ARGUMENTS" inside entry.Prompt is replaced with the HookInput JSON so the model can inspect every field of the event payload. The response is parsed as JSON looking for {decision, reason}; anything else is treated as allow.

Note: PromptDriver makes one LLM call per matching event, which can dominate turn latency if used on hot paths (PreToolUse / PostToolUse). Pair it with a cheap default model in settings.yaml.

func NewPromptDriver

func NewPromptDriver(caller LLMCaller) *PromptDriver

NewPromptDriver returns a driver backed by caller.

func (*PromptDriver) Run

Run renders the configured prompt and asks the LLM for a decision.

func (PromptDriver) Type

func (PromptDriver) Type() string

Type satisfies Driver.

Jump to

Keyboard shortcuts

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