Documentation
¶
Overview ¶
Package hooks loads, trusts and runs Celeste's lifecycle hooks (2.0 F0).
Hooks come from ~/.celeste/hooks.json (global, trusted), .celeste/hooks.json files in the workspace and its ancestors (repo-local, untrusted until the user approves them), and grimoire "## Hooks" sections, which are converted on load to protocol v1.
Index ¶
- Constants
- Variables
- func CheckRepoGrimoire(path string) error
- func CheckRepoSandbox(configPath string) error
- func Decide(store *TrustStore, src Source, approve ApproveFunc) (run bool, why string, err error)
- func DescribeSource(w io.Writer, src Source)
- func DisabledWarning(err error) string
- func Hash(defs []Definition) string
- func IsTerminal(f *os.File) bool
- func MCPServerName(src Source) string
- func SafeText(s string) string
- func SourceFile(src Source) string
- func StopContinuation(out Outcome, continued bool, turnsLeft int, s StopScope) (instr, warning string)
- func TrustPath(home string) string
- func WithWarn(ctx context.Context, warn func(string)) context.Context
- type Answer
- type ApproveFunc
- type Decision
- type Definition
- type Event
- type Options
- type Outcome
- type Runner
- func (r *Runner) Has(ev Event) bool
- func (r *Runner) PostCompact(ctx context.Context, trigger, summary string) Outcome
- func (r *Runner) PostToolUse(ctx context.Context, tool string, input map[string]any, res ToolResponse) Outcome
- func (r *Runner) PreCompact(ctx context.Context, trigger, instructions string) Outcome
- func (r *Runner) PreToolUse(ctx context.Context, tool string, input map[string]any) Outcome
- func (r *Runner) SessionStart(ctx context.Context, source string) Outcome
- func (r *Runner) Stop(ctx context.Context, lastMessage string) Outcome
- func (r *Runner) SubagentStop(ctx context.Context, agentID, lastMessage string) Outcome
- func (r *Runner) ToolHooks() tools.HookRunner
- func (r *Runner) UserPromptSubmit(ctx context.Context, prompt string) Outcome
- type Source
- type SourceKind
- type StopScope
- type ToolResponse
- type TrustStatus
- type TrustStore
Constants ¶
const ( // ProtocolV1 is a grimoire "## Hooks" entry: exit 0 allows, anything // else blocks with the output as the reason. ProtocolV1 = "v1" // ProtocolV2 reads one JSON object from stdout. ProtocolV2 = "v2" // DefaultTimeout and MaxTimeout are in seconds. DefaultTimeout = 30 MaxTimeout = 600 )
Variables ¶
var Events = []Event{ EventPreToolUse, EventPostToolUse, EventSessionStart, EventUserPromptSubmit, EventPreCompact, EventPostCompact, EventStop, EventSubagentStop, }
Events lists every event.
Functions ¶
func CheckRepoGrimoire ¶
CheckRepoGrimoire refuses a repo grimoire file the way Discover (and so `celeste hooks trust`) does: a symlinked file, or one reached through a symlinked directory under its project root, is never trusted or asked about.
func CheckRepoSandbox ¶
CheckRepoSandbox refuses a workspace config that is a symlink or sits in a symlinked .celeste directory, as repo hook files are refused: such a file is never trusted or asked about.
func Decide ¶
func Decide(store *TrustStore, src Source, approve ApproveFunc) (run bool, why string, err error)
Decide is the one trust step every repo source passes (hooks, stream rules, sandbox settings, MCP servers): run reports whether src may run, why says why not ("declined", "not trusted", ...). A Trusted source runs and a Declined one does not, without asking; any other is put to approve (nil in non-interactive runs: it then never runs), and a yes or a no is recorded. err is a failure to record the answer: a yes then holds for this session only, a no still skips the source.
func DescribeSource ¶
DescribeSource prints a source's hooks as the user must see them before approving. Every string from the file is shown Go-quoted, so control and bidi characters appear as escapes instead of acting on the terminal.
func DisabledWarning ¶
DisabledWarning is the one warning every adopter shows when Load fails.
func Hash ¶
func Hash(defs []Definition) string
Hash is the SHA-256 of the normalized definitions: exactly what a user approves. Formatting, key order and the rest of a grimoire don't change it.
func IsTerminal ¶
IsTerminal reports whether f is an interactive terminal. Callers require it for both the input and the output of a prompt. Character devices such as /dev/null and NUL are not accepted; use `celeste hooks trust` there.
func MCPServerName ¶
MCPServerName is the server name of a KindRepoMCP source.
func SafeText ¶
SafeText returns s, or s Go-quoted when showing it could act on or hide text on a terminal. It is termsafe.Line, the one rule for one-line untrusted text celeste shows: hook errors and reasons, the approval prompt and `celeste mcp list`.
func SourceFile ¶
SourceFile is the file a trust source was read from: its path without the "#stream-rules", "#sandbox" or "#mcp:<name>" suffix that keeps it apart from other trust entries for the same file.
func StopContinuation ¶
func StopContinuation(out Outcome, continued bool, turnsLeft int, s StopScope) (instr, warning string)
StopContinuation is the rule the chat, MCP chat and the agent share for a Stop or SubagentStop outcome: a deny continues the work once per unit and only while turns remain; a later deny, or one with no turns left, is reported as a warning and ignored, so a hook cannot keep the work going to the turn cap. instr is what to continue with (the hook's reason, or "Continue." when it gave none), or "" to finish; warning is "" unless a deny was ignored.
Types ¶
type Answer ¶
type Answer int
Answer is a person's reply to a trust question.
const ( // AnswerLater is no answer (EOF, a cancelled prompt, a question queued // for later): the source does not run and nothing is recorded. AnswerLater Answer = iota // AnswerYes approves the source; the approval is recorded. AnswerYes // AnswerNo declines it; the decline is recorded, so it is not asked // about again until it changes (#411). AnswerNo )
type ApproveFunc ¶
type ApproveFunc func(src Source, status TrustStatus) Answer
ApproveFunc asks a person whether an untrusted source may run. Only interactive callers supply one. It is never asked about a Trusted or Declined source.
func PromptApprover ¶
func PromptApprover(in io.Reader, out io.Writer) ApproveFunc
PromptApprover asks on out and reads a y/N answer from in. y or yes approves; anything else on the line (Enter included) declines, and the decline is remembered. EOF is no answer (AnswerLater), also after a partial line other than y or yes.
func PromptApproverNoRemember ¶
func PromptApproverNoRemember(in io.Reader, out io.Writer) ApproveFunc
PromptApproverNoRemember is PromptApprover for a caller that records only a yes (`celeste hooks trust`): the prompt says a no leaves the stored decision as it was instead of claiming it is remembered.
type Definition ¶
type Definition struct {
Event Event `json:"event"`
Matcher string `json:"matcher,omitempty"`
Command string `json:"command"`
Timeout int `json:"timeout,omitempty"`
Protocol string `json:"protocol,omitempty"`
}
Definition is one hook as written in hooks.json.
func FromGrimoire ¶
func FromGrimoire(entries []grimoire.HookEntry) (defs []Definition, skipped []string)
FromGrimoire converts grimoire "## Hooks" entries to protocol v1 definitions. Phases other than PreToolUse and PostToolUse never ran in 1.x; they are reported in skipped, not converted.
func ParseFile ¶
func ParseFile(data []byte) ([]Definition, error)
ParseFile parses a hooks.json document. An unknown field or any invalid entry rejects the whole file, so a typo never silently drops a guard.
type Event ¶
type Event string
Event names a point in a session where hooks run.
const ( EventPreToolUse Event = "PreToolUse" EventPostToolUse Event = "PostToolUse" EventSessionStart Event = "SessionStart" EventUserPromptSubmit Event = "UserPromptSubmit" EventPreCompact Event = "PreCompact" EventPostCompact Event = "PostCompact" EventStop Event = "Stop" EventSubagentStop Event = "SubagentStop" )
type Options ¶
type Options struct {
Workspace string
Home string // "" = os.UserHomeDir()
SessionID string
// Approve is nil in non-interactive modes: untrusted sources are then
// skipped with a warning and never approved.
Approve ApproveFunc
Warn func(string) // nil = stderr
}
Options configures Load.
type Outcome ¶
type Outcome struct {
Decision Decision // Allow when no hook matched
Reason string // as the hook wrote it: escape it to show it on a terminal
AdditionalContext string // returned to the model
UpdatedInput map[string]any // PreToolUse only; nil = unchanged
}
Outcome is the combined verdict of every hook that ran for one event.
type Runner ¶
type Runner struct {
// contains filtered or unexported fields
}
Runner runs a session's hooks. It is immutable after Load and safe for concurrent use. A nil *Runner allows everything.
func Load ¶
Load discovers the workspace's hooks and keeps the ones allowed to run: global sources always, repo sources when trusted or approved now. On error, callers must warn the user and treat hooks as disabled; global guards are not running in that state.
func (*Runner) PostCompact ¶
func (*Runner) PostToolUse ¶
func (*Runner) PreCompact ¶
PreCompact: trigger is "manual" or "auto"; anything but Allow blocks the compaction; AdditionalContext is extra summary instructions.
func (*Runner) PreToolUse ¶
func (*Runner) SessionStart ¶
SessionStart: source is "startup" or "resume".
func (*Runner) SubagentStop ¶
SubagentStop: same rule as Stop, for a subagent's final message.
func (*Runner) ToolHooks ¶
func (r *Runner) ToolHooks() tools.HookRunner
ToolHooks adapts r for tools.Registry.SetHookRunner. It is nil when no PreToolUse or PostToolUse hook is loaded, so the registry skips hook work.
type Source ¶
type Source struct {
// Path is the absolute path where the file was found, symlinks not
// resolved. It is the trust key, so an approval never transfers to a
// different location.
Path string
// Root is the project root the hooks run in: the directory holding
// .celeste/, or the grimoire's directory. "" for global sources, which
// run in the workspace.
Root string
Kind SourceKind
Hooks []Definition // normalized
Hash string // Hash(Hooks); for KindRepoStreamRules, the hash of Rules
// Rules is the "## Stream Rules" text of a KindRepoStreamRules source,
// shown for review before it is trusted.
Rules string
// contains filtered or unexported fields
}
Source is one file that defines hooks.
func Discover ¶
Discover finds every hook source for workspace, in run order: ~/.celeste/hooks.json, ~/.celeste/grimoire.md, .celeste/hooks.json files from the filesystem root down to workspace, then every other grimoire file in grimoire priority order. A file that fails to read or parse (including a symlinked repo file or one over 1 MiB) is skipped with a warning. Sources that define no hooks are left out.
func MCPSource ¶
MCPSource is the trust source for server name in the workspace MCP config at configPath: keyed by the file's path plus "#mcp:<name>", pinned to hash (mcp.ServerConfig.TrustHash), described by summary (mcp.ServerConfig.TrustSummary).
func SandboxSource ¶
SandboxSource is the trust source for one workspace config's "sandbox" object: keyed by the file's path plus "#sandbox", hashed over body (the object's canonical JSON), so any edit asks again.
func SourcesAt ¶
SourcesAt returns what `celeste hooks trust` acts on: everything Discover finds when target is a directory, or the one file when it is a file Discover would load. Anything else is an error.
func StreamRulesSource ¶
StreamRulesSource is the trust source for one grimoire file's "## Stream Rules" section: keyed by the file's path plus "#stream-rules", hashed over the section text, so any edit asks again.
type SourceKind ¶
type SourceKind string
SourceKind says where a hook source lives, which decides whether it is trusted without approval.
const ( KindGlobal SourceKind = "global" // ~/.celeste/hooks.json KindGlobalGrimoire SourceKind = "global-grimoire" // ~/.celeste/grimoire.md (v1) KindRepo SourceKind = "repo" // .celeste/hooks.json in the workspace or an ancestor KindRepoGrimoire SourceKind = "repo-grimoire" // any other grimoire file (v1) )
const KindRepoMCP SourceKind = "repo-mcp"
KindRepoMCP is one server in a workspace's .mcp.json or .celeste/mcp.json. An enabled one would start its command (or connect to its URL) when the chat launches, so it is trusted like a repo hook: by a hash of what it runs, in the same store, with the same `celeste hooks trust` flow.
const KindRepoSandbox SourceKind = "repo-sandbox"
KindRepoSandbox is the "sandbox" object of a workspace's .celeste/config.json (2.0 W4). It runs nothing, but its loosening ("enabled": false, "network": true, extra writable paths) lets every bash command write or connect where the sandbox would stop it, so it is trusted like a repo hook: by content hash, in the same store, with the same `celeste hooks trust` flow.
const KindRepoStreamRules SourceKind = "repo-stream-rules"
KindRepoStreamRules is a repo grimoire's "## Stream Rules" section (2.0 W3). It runs nothing, but its rules can stop replies, force paid re-runs and add reminders the model treats as authoritative, so it is trusted like a repo hook: by content hash, in the same store, with the same `celeste hooks trust` flow. Load never runs or warns about it; the rules package checks its trust (rules.Trusted).
type StopScope ¶
type StopScope struct {
Event, Actor, Unit string
}
StopScope names, for StopContinuation's warnings, the event that fired ("Stop" or "SubagentStop"), who it asked to continue ("the chat", "the agent") and the unit a continuation is counted per ("turn", "call", "run").
type ToolResponse ¶
ToolResponse is a tool's result as PostToolUse hooks see it.
type TrustStatus ¶
type TrustStatus int
TrustStatus is whether a source may run without asking.
const ( Untrusted TrustStatus = iota // never approved or declined Trusted // global, or approved with this exact hash Changed // approved before, but its hooks changed since Declined // declined with this exact hash: never runs, never asked again DeclinedChanged // declined before, but it changed since: asked again )
func (TrustStatus) String ¶
func (s TrustStatus) String() string
type TrustStore ¶
type TrustStore struct {
// contains filtered or unexported fields
}
TrustStore is ~/.celeste/trusted.json: approved and declined repo hook sources, keyed by canonical path, pinned to the hash of the definitions the person saw.
func LoadTrust ¶
func LoadTrust(home string) *TrustStore
LoadTrust reads the store. It never returns nil; check Err.
func (*TrustStore) Approve ¶
func (s *TrustStore) Approve(src Source) error
Approve records src's current hash, replacing any decline. It re-reads the file first so another process's decisions survive, then replaces the file atomically, all under a lock file shared with other processes (trusted.json.lock), so no decision another process stored is lost.
func (*TrustStore) Decline ¶
func (s *TrustStore) Decline(src Source) error
Decline records that the person said no to src's current hash, replacing any approval: it never runs and is not asked about again until it changes (#411). Like Approve, it re-reads the file first.
func (*TrustStore) Err ¶
func (s *TrustStore) Err() error
Err reports why the store could not be read; nil when it is usable.
func (*TrustStore) Forget ¶
func (s *TrustStore) Forget(key string) (forgot bool, err error)
Forget removes any approval or decline stored under key (a Source's Path), so the source is asked about again. forgot is false when nothing was stored; the file is then left untouched.
func (*TrustStore) Status ¶
func (s *TrustStore) Status(src Source) TrustStatus
Status reports whether src may run without asking. A decline of this exact hash wins over any approval.