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 ¶
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.
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 (*Runner) Any ¶
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 ¶
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).
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 ¶
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.