query({hooks}) option and settingSources discovery. The novelty is two parallel sources merge at runtime: declarative hooks in prompts/settings.json AND programmatic HookCallback closures in handler.mjs.
The two sources
Four SDK declarative hook types
| Type | What it does | Inputs / outputs |
|---|---|---|
| command | Shell command; receives JSON on stdin, writes JSON on stdout | Full env access — powerful but UNSAFE from untrusted sources |
| prompt | LLM-evaluated (SDK uses Haiku internally); $ARGUMENTS substitution | Safe enough that mergeSettings() allows from user settings |
| agent | Dispatched subagent (Task tool semantics) | E.g. "Verify tests pass: $ARGUMENTS" |
| http | HTTP webhook to external service | Third-party policy server |
node_modules/@anthropic-ai/claude-agent-sdk/cli.js:7300,7306) and Orion’s mergeSettings() carve-out.
Orion’s own
prompts/settings.json uses ONLY type:"command" today. The other three are accepted by the SDK and reachable via vault user settings — subject to the security filter below.Security carve-out: vault settings dropped to type:“prompt” only
mergeSettings() in session-config.mjs:540-581 concatenates built-in + vault user hooks per event into the per-session settings.json BEFORE the SDK reads it. Filter (L553-565):
- Only
type:"prompt"survives from user-side settings. type:"command"from user is dropped + warning logged.- Rationale: shell commands from untrusted vault content could compromise the host.
All 22 programmatic hooks Orion ships
Lifecycle (lifecycle-hooks.mjs, 447 LOC)
Lifecycle (lifecycle-hooks.mjs, 447 LOC)
createSessionStartHook — reads continuity cache, returns <session-continuity>...</session-continuity> block summarizing prior session if cache <24h old.
createPreCompactHook — writes {summary, files, activeProject, sessionType, turnCount} to ~/.cache/orion/continuity/<conversationId>.json (24h TTL).
createStopHook — increments in-memory _turnCounters. Every 5th turn writes {turns, filesModified, ts} to session-activity cache. Evicts oldest after 100 conversations.
createSessionEndHook — cleanupStaleCacheFiles() removes files >7 days old from continuity/, session-activity/, memory-enrichment/. Rate-limited via in-memory _lastCleanupTs + marker file.Tool gates (PreToolUse)
Tool gates (PreToolUse)
createEnvIsolationHook (handler.mjs:532-555) — prepends unset ANTHROPIC_BASE_URL ... (the canonical ORION_INTERNAL_ENV_VARS strip list) to every Bash command. Always returns permissionDecision:"allow" + updatedInput.command.
createProtectionHook (protection-hook.mjs) — DENY on sensitive file writes (matches EXTRA_BLOCKED_FILE_PATTERNS or getDangerLevel=high), system-dir Bash; advisory warnings on lockfile edits / high-danger Bash.
createDocumentGuardHook (document-guard.mjs) — reads first 2KB of .md/.txt/.html/etc., scans for prompt-injection patterns (ignore previous instructions, <system>) + emphasis-word overload. Never denies — emits additionalContext warning only.Project / PARA detection (UserPromptSubmit + PostToolUse)
Project / PARA detection (UserPromptSubmit + PostToolUse)
createProjectAutoDetectHook — first user message keyword-matches against PARA project names (30s cache). High-confidence match → emits projectContextChange IPC. Tracks assignedConversations Set to avoid re-firing.
createProjectDetectHook (PostToolUse) — Write/Edit creating _meta.yaml in ~/Orion/Projects/ OR Bash running orion para create-entity --type project → emits projectContextChange.
createKeywordContextHook (UserPromptSubmit) — message contains workflow keyword (tdd/plan/analyze/deepsearch) → injects <workflow-hint> advisory.Subagent lifecycle
Subagent lifecycle
createSubagentStartHook — injects <project-context>Active PARA project: ...</project-context> if active project. Uses basename(projectPath) for cross-platform safety.
createSubagentStopHook — log-only; SDK type system has NO hookSpecificOutput for SubagentStop per inline comment.CCW (declared only when ccwModeRegistry non-null)
CCW (declared only when ccwModeRegistry non-null)
createCcwStopHook (Stop), createCcwRecoveryHook (PreCompact), createCcwContinuationHook (SessionStart) — CCW-mode handlers in src-tauri/sidecar/ccw/hooks/.Inline closures in handler.mjs
Inline closures in handler.mjs
createMediaTracingHook (L565-648) — orion media <type> <action> Bash patterns → Langfuse tool spans keyed by tool_use_id. Side-effect only.
PostCompact closure (L2832-2857) — emits contextSummaryUpdated IPC with the compact summary, sliced to 4000 chars.
Notification closure (L2858-2884) — forwards SDK notification → frontend IPC event {title, message, notificationType}.
SessionEnd CCW closure (L2911-2935) — dispatches to createSessionEndHandler({projectPath, log}) only when CCW modes are active.Orion’s settings.json wiring
- UserPromptSubmit (4 hooks)
- PreToolUse (2 hooks)
- PostToolUse (3 hooks)
- Stop / End (3 hooks)
- SessionStart (2 hooks)
- PostToolUseFailure (1 hook)
| Matcher | Hook | Timeout |
|---|---|---|
| (any) | para-context-hook.mjs | 10s |
| (any) | morning-trigger-hook.mjs | 5s |
| (any) | automation-intent-hook.mjs | 5s |
| (any) | initiative-intent-hook.mjs | 5s |
para-context-hook.mjs is the largest hook in the codebase (757 LOC). It injects ambient PARA awareness — date/time, snapshot of active project, ~65 tokens — into every user turn, plus keyword classification and conditional reminders. Modular: pulls from hooks/context/*.mjs modules.Hook IPC contract
Input (stdin for declarative, function arg for programmatic)
Output variants (hookSpecificOutput)
- Deny tool call
- Allow + mutate input
- Inject context
- System message
- Stop / Failure response
protection-hook, para-system-guard-hook, para-cli-redirect-hook.PreToolUse decision flow
Execution model
- Per-event ordering — within a single event, array order = execution order. For
PreToolUse: env-isolation FIRST, then protection, then document-guard, then media-tracing. - Declarative + programmatic merge — the SDK merges hooks from
settingSources(settings.json) with hooks inoptions.hooks. Per the inline handler.mjs:1541-1545 comment: “The SDK merges both sources. No custom hook adapter needed.” - Serial within event — ? INFERRED. The SDK appears to invoke hooks for a single event serially. No code in
handler.mjsparallelizes. - Error handling — EVERY shipped hook wraps body in
try { ... } catch { ... }and returns{}or{ok:true}on error. Fail-open is the convention — a broken hook never blocks a query. - Timeouts — declarative hooks honor
"timeout":<seconds>in settings.json. Programmatic hooks have NO declared SDK-level timeout — they rely on internalAbortSignal.timeout()for fetch calls (e.g. capture-evaluator = 12s, failure-evaluator = 8s). - Blocking semantics — only
PreToolUsecan deny/mutate. All other events are advisory. - Stop loop guards — Stop hooks check
input.stop_hook_active || input.stopHookActiveand return{ok:true}if set, preventing infinite Stop→respond→Stop cycles.
Open questions / honest gaps
Q1: prompt/agent/http hook types unused
Q1: prompt/agent/http hook types unused
Orion ships ZERO declarative hooks of types
prompt/agent/http. SDK supports them and the user-settings merge filter even allows prompt from vault settings — but no Orion documentation or skill guides users to write them.Q2: type:prompt instability history
Q2: type:prompt instability history
Both
capture-evaluator-hook.mjs and failure-evaluator-hook.mjs file headers say they “replace type:'prompt' hooks to avoid Stream closed crashes.” The type:'prompt' mode is functional in the SDK but produced sidecar instability in Orion. Solution was to re-implement LLM evaluation as type:"command" hooks that make their own Anthropic API calls via the proxy.Q3: Pi engine hook adapter
Q3: Pi engine hook adapter
handler.mjs:2443 references
engine/pi-hooks.mjs / pi-hook-adapter.mjs for Pi sessions. Parallel hook system exists. Out of scope for this audit — Pi’s hook execution semantics may differ.Q4: Programmatic hook timeouts
Q4: Programmatic hook timeouts
Only declarative hooks have explicit
timeout fields. Programmatic hooks rely on internal AbortSignal.timeout() for fetches; long-running hooks (memory enrichment, file scans) could theoretically block the SDK indefinitely.Q5: SubagentStop limitation
Q5: SubagentStop limitation
Per inline comment in
subagent-lifecycle-hook.mjs:62-64, SubagentStop has NO hookSpecificOutput in the SDK type system. Subagent completion hooks are log-only.Q6: No dedicated memory-extract hook
Q6: No dedicated memory-extract hook
The closest things to “memory extraction” are: (a)
memoryEnrichmentMap / triggerMemoryEnrichment in handler.mjs:758-783, (b) capture-evaluator-hook.mjs (Stop) which extracts DECISION/CONTACT/PREFERENCE/TASK signals and emits instructions for Claude to call the para MCP tool. No dedicated memory-extract-hook.mjs file exists.Key files
src-tauri/sidecar/query/handler.mjs
src-tauri/sidecar/query/handler.mjs
Hooks literal passed to
query() at L2789-2940. settingSources at L2727. CLAUDE_CONFIG_DIR at L2733. Inline factories: createEnvIsolationHook L532-555, createMediaTracingHook L565-648.src-tauri/sidecar/query/session-config.mjs
src-tauri/sidecar/query/session-config.mjs
mergeSettings() at L540-581. Security carve-out filtering user-side type:command at L553-565.src-tauri/sidecar/hooks/
src-tauri/sidecar/hooks/
All 22 programmatic hook files. Largest:
para-context-hook.mjs (757 LOC). Most critical: protection-hook.mjs, document-guard.mjs, lifecycle-hooks.mjs.prompts/settings.json
prompts/settings.json
Orion’s runtime declarative hooks (15 hook entries, all
type:"command").src-tauri/sidecar/ccw/hooks/
src-tauri/sidecar/ccw/hooks/
CCW-mode hooks: stop-handler, recovery-handler, continuation-hook, keyword-detector. Wired only when
ccwModeRegistry is non-null.src-tauri/sidecar/query/message-processor.mjs
src-tauri/sidecar/query/message-processor.mjs
Handles
hook_started / hook_progress / hook_response IPC at L907-928 (SDK 0.2.76+ lifecycle).Next
Agent Loops
Where in the iteration cycle each hook event fires.
Skills
Hooks vs skills — when does each fire?