hooks

package
v0.9.3 Latest Latest
Warning

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

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

Documentation

Overview

Package hooks implements jcode's user-configurable hook system: external commands fired at key points of the agent loop (before/after a tool runs, on session start, when the agent is about to stop, etc.).

The package is a dependency-free leaf: internal/agent, internal/runner and the command surfaces depend on it, never the other way around. This keeps the hook dispatcher transport-agnostic so TUI, Web and ACP all share one implementation.

The on-disk schema and the stdin/stdout protocol mirror Claude Code / Qoder so users can reuse familiar configs. See internal-doc/hooks-design.md.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func IsPreApproved

func IsPreApproved(ctx context.Context) bool

IsPreApproved reports whether a PreToolUse hook already authorized this call.

func WithDispatcher

func WithDispatcher(ctx context.Context, d Dispatcher) context.Context

WithDispatcher stores the session's Dispatcher on the context so the tool hook middleware and the runner's continuation loop can reach it without threading it through every signature. Command surfaces (TUI/Web/ACP) inject it into the ctx they hand to the agent run.

func WithPreApproved

func WithPreApproved(ctx context.Context) context.Context

WithPreApproved marks the context so a downstream approval gate skips its user prompt. The PreToolUse hook middleware sets this when a hook returns permissionDecision=allow; the approval layer reads it via IsPreApproved.

It is intentionally per-call (context.WithValue, not persisted across interrupt/resume): a pre-approval only applies to the single tool invocation whose ctx carries it.

Types

type Config

type Config struct {
	Hooks map[string][]HookGroup `json:"hooks"`
}

Config is one parsed hooks.json file: event name → matcher groups.

func Load

func Load(configDir, workDir string, trustProject bool) (Config, []string)

Load reads and merges the hooks.json layers.

configDir    — the ~/.jcode directory (config.ConfigDir()).
workDir      — the project working directory; its .jcode/ holds project layers.
trustProject — whether to load the project layers (.jcode/hooks.json and
               .jcode/hooks.local.json).

SECURITY: only the user layer (~/.jcode/hooks.json) is trusted by default. A hook runs arbitrary commands the moment its event fires (SessionStart fires on startup) and can auto-approve tools, so honoring project-provided hooks would be arbitrary code execution from an untrusted clone. Project layers therefore load only when trustProject is true — a stand-in until trust-on-first-use (per-hook hash confirmation, see internal-doc/hooks-design.md §7) lands.

Missing files are skipped silently. Malformed files are skipped with a warning (returned, not fatal) so one broken layer cannot brick the agent.

func (Config) Empty

func (c Config) Empty() bool

Empty reports whether the config defines no hooks at all.

type Decision

type Decision struct {
	// Permission is the folded PreToolUse verdict. PermDeny wins over everything.
	Permission Permission
	// Block is true when a blockable event decided to halt (exit 2 / decision=block).
	Block bool
	// Reason explains a deny/block, surfaced to the agent or user.
	Reason string
	// UpdatedInput, if non-nil, replaces tool_input before execution (PreToolUse).
	UpdatedInput json.RawMessage
	// ModifiedResult, if non-nil, replaces tool_response after execution (PostToolUse).
	ModifiedResult *string
	// AdditionalContext is text to inject into the model context.
	AdditionalContext string
	// SystemMessage is a non-blocking note surfaced to the user.
	SystemMessage string
}

Decision is the folded outcome of firing all of an event's matching hooks. The zero value means "no hook had any opinion — proceed normally".

func (Decision) Denied

func (d Decision) Denied() bool

Denied reports whether the decision blocks the operation.

type Dispatcher

type Dispatcher interface {
	// Fire runs every matching hook for event with the given payload and returns
	// the folded decision. The zero Decision means "no opinion — proceed".
	Fire(ctx context.Context, event Event, p Payload) Decision
	// Configured reports whether any hook is registered for event, letting hot
	// paths skip payload construction when hooks are unused.
	Configured(event Event) bool
}

Dispatcher fires the hooks configured for an event and folds their outcomes into a single Decision. Implementations must be safe for concurrent use.

func DispatcherFromContext

func DispatcherFromContext(ctx context.Context) Dispatcher

DispatcherFromContext returns the Dispatcher on the context, or a no-op dispatcher when none was injected, so callers never need a nil check.

func NewDispatcher

func NewDispatcher(cfg Config, opts Options) Dispatcher

NewDispatcher builds a Dispatcher from a merged Config. When the config defines no hooks it returns a no-op dispatcher whose Fire is free, so callers can wire it unconditionally.

func NewSessionDispatcher

func NewSessionDispatcher(configDir, cwd, sessionID string, logf func(string, ...any)) Dispatcher

NewSessionDispatcher loads the hook config for a session and builds a Dispatcher, honoring the project-trust env gate. Shared by all command surfaces (TUI/Web/ACP) so hooks behave identically everywhere.

configDir — the ~/.jcode directory (config.ConfigDir()).
cwd       — the session working directory.
sessionID — the session UUID (for the hook payload).

type Event

type Event string

Event is a hook trigger point.

const (
	SessionStart       Event = "SessionStart"
	UserPromptSubmit   Event = "UserPromptSubmit"
	PreToolUse         Event = "PreToolUse"
	PostToolUse        Event = "PostToolUse"
	PostToolUseFailure Event = "PostToolUseFailure"
	Stop               Event = "Stop"
)

func (Event) Blockable

func (e Event) Blockable() bool

Blockable reports whether an event's hooks may block/deny (via exit code 2 or a structured decision). Non-blockable events can still inject context or modify results, but cannot halt the operation.

type HookGroup

type HookGroup struct {
	Matcher string     `json:"matcher,omitempty"`
	Hooks   []HookSpec `json:"hooks"`
}

HookGroup binds a matcher to a set of hook commands.

type HookSpec

type HookSpec struct {
	Type    string `json:"type"`              // v1 supports "command"
	Command string `json:"command"`           // shell command (run via `sh -c`)
	Timeout int    `json:"timeout,omitempty"` // seconds; 0 → dispatcher default
	Async   bool   `json:"async,omitempty"`   // fire-and-forget (non-blockable events only)
}

HookSpec is a single configured hook command.

type Options

type Options struct {
	CWD            string
	SessionID      string
	TranscriptPath string
	Env            []string // base env; nil → os.Environ()
	Logf           func(string, ...any)
}

Options parameterize a Dispatcher with per-session context and hooks.

type Payload

type Payload struct {
	SessionID      string          `json:"session_id,omitempty"`
	TranscriptPath string          `json:"transcript_path,omitempty"`
	CWD            string          `json:"cwd,omitempty"`
	HookEventName  string          `json:"hook_event_name"`
	ToolName       string          `json:"tool_name,omitempty"`
	ToolInput      json.RawMessage `json:"tool_input,omitempty"`
	ToolResponse   string          `json:"tool_response,omitempty"`
	Prompt         string          `json:"prompt,omitempty"`
	StopHookActive bool            `json:"stop_hook_active,omitempty"`
}

Payload is the JSON handed to a hook process over stdin. Field names match the Claude Code contract so existing hook scripts work unchanged.

type Permission

type Permission string

Permission is a PreToolUse decision.

const (
	PermNone  Permission = "" // hook expressed no opinion
	PermAllow Permission = "allow"
	PermDeny  Permission = "deny"
	PermAsk   Permission = "ask"
)

Jump to

Keyboard shortcuts

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