hook

package
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Jul 4, 2026 License: MIT Imports: 13 Imported by: 0

Documentation

Overview

Package hook runs user-supplied programs that observe and steer the agent loop at named events, behind the workspace trust gate from D16. A hook is an external process, not an ari plugin format: ari spawns it with a JSON payload on stdin and reads its answer from the exit code and stdout, so a hook is any language a user can spawn (doc 05 sections 12 and 13).

The package holds no ari dependencies. The loop reaches it through a small seam in the agent package, and the runner builds a dispatcher over the discovered config and the trust decision; the permission-steering and session-lifecycle behaviors are wired in the next slice.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Describe

func Describe(c Command) string

Describe renders a one-line summary of a command for doctor and the trust prompt, so a user sees what a workspace's hooks would run before trusting it (doc 05 section 12).

func Known

func Known(e Event) bool

Known reports whether an event name is part of the M1 set, so config can warn on a typo rather than silently ignoring a whole hook.

Types

type Command

type Command struct {
	Event   Event
	Layer   string // "user", "project", or "local"
	If      string
	Command string
	Shell   string
	Timeout time.Duration
	Once    bool
	Async   bool
	// contains filtered or unexported fields
}

Command is one configured hook resolved for a run: the file fields plus the event it fires on, the layer it came from (which gates it against workspace trust), and the compiled matcher.

func (Command) Applies

func (c Command) Applies(tool string) bool

Applies reports whether this command should fire for a tool. A non-tool event ignores the matcher and always applies; a tool event consults it.

type Event

type Event string

Event names one point in the loop where hooks fire. The values match the Claude Code event names so a hook config written for that ecosystem transfers unchanged (doc 05 section 13.1, research A.6).

const (
	// PreToolUse fires after a call is validated and before the permission
	// decision, for each call. A blocking result stops the call.
	PreToolUse Event = "PreToolUse"
	// PostToolUse fires after a tool call completes successfully, with the
	// result available. This is where a linter or formatter runs.
	PostToolUse Event = "PostToolUse"
	// PostToolUseFailure fires after a tool call returns an error, so a hook
	// can react to what failed.
	PostToolUseFailure Event = "PostToolUseFailure"
	// UserPromptSubmit fires when the user submits a prompt, before the model
	// sees it. Behavior lands in the next slice.
	UserPromptSubmit Event = "UserPromptSubmit"
	// SessionStart fires when a session begins. Behavior lands in the next
	// slice.
	SessionStart Event = "SessionStart"
	// SessionEnd fires when a session ends.
	SessionEnd Event = "SessionEnd"
	// Stop fires when the ant is about to finish a turn with no more tool
	// calls. A blocking Stop re-drives the loop; that behavior lands in the
	// next slice under the spiral guard.
	Stop Event = "Stop"
	// PreCompact fires before the loop compacts context.
	PreCompact Event = "PreCompact"
	// PostCompact fires after a compaction completes.
	PostCompact Event = "PostCompact"
)

type HookSpecificOutput

type HookSpecificOutput struct {
	HookEventName            string          `json:"hookEventName,omitempty"`
	PermissionDecision       string          `json:"permissionDecision,omitempty"`
	PermissionDecisionReason string          `json:"permissionDecisionReason,omitempty"`
	UpdatedInput             json.RawMessage `json:"updatedInput,omitempty"`
	AdditionalContext        string          `json:"additionalContext,omitempty"`
}

HookSpecificOutput is the Claude Code per-event control block. The runner normalizes it onto Permission and AdditionalContext so the rest of the package reads one shape.

type Options

type Options struct {
	// Commands is every configured hook across all layers. The runner filters
	// them per event, per matcher, and per trust.
	Commands []Command
	// Trusted is the workspace trust decision (doc 05 section 12). When false,
	// only user-layer hooks run; the repo's own hooks stay silent until the
	// user trusts the workspace.
	Trusted bool
	// Untrusted marks an automation session fed untrusted content (D19). When
	// true, no hook runs at all, even a user hook in a trusted workspace,
	// because a prompt-injected session could otherwise trigger configured
	// commands with attacker-chosen timing.
	Untrusted bool
	// ProjectDir and Session seed the hook environment.
	ProjectDir string
	Session    string
	// contains filtered or unexported fields
}

Options builds a Runner for one session.

type Outcome

type Outcome struct {
	Block        bool
	Message      string
	Context      string
	StopContinue *bool
	StopReason   string

	// Permission is the aggregated steer from PreToolUse hooks, most
	// restrictive wins: any deny denies, else any ask asks, else the first
	// allow (which may narrow the input). Nil when no hook steered the call.
	// A blocking hook also surfaces here as a deny so the pipeline seam can
	// treat exit 2 and an explicit deny the same way.
	Permission *Permission

	Results []Result
}

Outcome aggregates what the hooks for one event decided. Block is set when any hook blocked; Message carries the joined block or warning text; Context carries the joined additional context from exit-0 hooks; StopContinue is false when a hook asked the turn to stop.

type Output

type Output struct {
	// Continue false stops the turn with StopReason, across every event.
	Continue   *bool  `json:"continue,omitempty"`
	StopReason string `json:"stopReason,omitempty"`

	// AdditionalContext is injected for user-prompt, session-start, and
	// post-tool events.
	AdditionalContext string `json:"additionalContext,omitempty"`

	// Permission is ari's flat way for a pre-tool or permission-request hook
	// to steer the pipeline. It is honored alongside the Claude Code
	// hookSpecificOutput shape below; when both are set, Permission wins.
	Permission *Permission `json:"permission,omitempty"`

	// HookSpecificOutput is the Claude Code shape a PreToolUse hook returns to
	// steer permission: permissionDecision allow, deny, or ask, an optional
	// narrowing updatedInput, and a reason. A UserPromptSubmit or SessionStart
	// hook can also carry additionalContext here (doc 05 section 3).
	HookSpecificOutput *HookSpecificOutput `json:"hookSpecificOutput,omitempty"`
}

Output is the structured stdout a hook may return on exit 0. Every field is optional; a hook that only wants to block uses exit 2 and no JSON at all (doc 05 section 13.3).

type Payload

type Payload struct {
	Event   Event           `json:"event"`
	Tool    string          `json:"tool,omitempty"`
	Input   json.RawMessage `json:"input,omitempty"`
	Result  string          `json:"result,omitempty"`
	IsError bool            `json:"isError,omitempty"`
	Prompt  string          `json:"prompt,omitempty"`
	Reason  string          `json:"reason,omitempty"` // SessionStart/SessionEnd source
	Session string          `json:"session,omitempty"`
	Cwd     string          `json:"cwd,omitempty"`
}

Payload is the JSON handed to a hook on stdin. It is event-shaped: a tool event carries the tool and its input, a prompt event carries the prompt. Everything a hook needs to decide is here, so a hook never has to reach back into ari (doc 05 section 13.2).

type Permission

type Permission struct {
	Behavior     string          `json:"behavior"` // "allow" or "deny"
	UpdatedInput json.RawMessage `json:"updatedInput,omitempty"`
	AddRules     []string        `json:"addRules,omitempty"`
	Message      string          `json:"message,omitempty"`
	Interrupt    bool            `json:"interrupt,omitempty"` // deny and abort the turn
}

Permission is how a pre-tool or permission-request hook steers the pipeline. The Behavior is a plain string here so the package stays free of ari dependencies; the runner maps it onto the permission pipeline's types.

type Result

type Result struct {
	Event            Event
	ExitCode         int
	Blocking         bool
	NonBlockingError bool
	Message          string // the block message or the warning
	Stdout           string // raw stdout, shown when it is not structured control
	Output           *Output
}

Result is what one command hook returned, interpreted per the exit-code contract: 0 is success (stdout may carry structured control), 2 blocks (stderr is the block message), any other non-zero is a non-blocking error (stderr warns the user, the agent is not blocked).

type Runner

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

Runner dispatches hooks for one session. It owns the trust gate: every fire consults it, and there is no path around it (doc 05 section 12).

func NewRunner

func NewRunner(o Options) *Runner

NewRunner builds a session dispatcher over the configured hooks.

func (*Runner) Any

func (r *Runner) Any() bool

Any reports whether any hook could ever run for this session, so a caller can skip wiring the seam entirely when there is nothing to fire.

func (*Runner) Enabled

func (r *Runner) Enabled() bool

Enabled reports whether hooks may run for this session at all. It is the single gate for the untrusted-content rule: a session fed untrusted content runs no hook regardless of workspace trust (doc 05 sections 12 and 14).

func (*Runner) Fire

func (r *Runner) Fire(ctx context.Context, ev Event, p Payload) Outcome

Fire runs every hook registered for an event that passes the trust gate and the matcher, and aggregates their results. A disabled session or an event with no matching hook returns the zero Outcome, so the caller treats "no hooks" and "hooks all passed" the same way.

type Spec

type Spec struct {
	Matcher string `toml:"matcher"`
	If      string `toml:"if"`
	Command string `toml:"command"`
	Shell   string `toml:"shell"`
	Timeout string `toml:"timeout"`
	Once    bool   `toml:"once"`
	Async   bool   `toml:"async"`
}

Spec is one hook as written in a config file. It is the TOML shape; the event key and the source layer are supplied by the loader, not the file.

func (Spec) Build

func (s Spec) Build(ev Event, layer string) (Command, error)

Build resolves a Spec into a Command for an event and a source layer, applying the default shell and timeout and compiling the matcher. A bad timeout or an uncompilable regex matcher is an error the loader turns into a config warning, so one broken hook never wedges the whole config.

type Trust

type Trust struct {
	Root      string    `json:"root"`
	Trusted   bool      `json:"trusted"`
	DecidedAt time.Time `json:"decided_at"`
	DecidedBy string    `json:"decided_by"`
}

Trust is the per-workspace trust record. Trust is always an explicit user decision, so DecidedBy is always "user"; there is no auto-trust, because the whole value of the gate is that a repo you cloned and have not vouched for cannot run code at you the moment you open it (doc 05 section 12).

type TrustStore

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

TrustStore persists trust decisions keyed by workspace root path in one JSON file in the global nest, so a workspace is trusted once and not once per session. It is safe for concurrent use.

func LoadTrust

func LoadTrust(path string) *TrustStore

LoadTrust reads the trust file at path. A missing file is an empty store, not an error: a fresh install trusts nothing. A corrupt file is also an empty store, because a trust record you cannot parse must fail closed to untrusted, never open to trusted.

func (*TrustStore) IsTrusted

func (s *TrustStore) IsTrusted(root string) bool

IsTrusted reports whether root has been explicitly trusted. An unknown workspace is untrusted, the safe default.

func (*TrustStore) Lookup

func (s *TrustStore) Lookup(root string) (Trust, bool)

Lookup returns the raw record for a workspace, so doctor can report when and by whom trust was decided.

func (*TrustStore) Revoke

func (s *TrustStore) Revoke(root string, at time.Time) error

Revoke records that a workspace is no longer trusted, so a hook config that turned malicious can be shut off without deleting the record.

func (*TrustStore) Trust

func (s *TrustStore) Trust(root string, at time.Time) error

Trust records an explicit user decision to trust a workspace and persists it. at is passed in so the record is deterministic under test.

Jump to

Keyboard shortcuts

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