Hooks are how Orion enforces policy, injects context, and reacts to lifecycle events without modifying the core SDK. They are NOT a custom system — Orion uses the Claude Agent SDK’s native 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

TypeWhat it doesInputs / outputs
commandShell command; receives JSON on stdin, writes JSON on stdoutFull env access — powerful but UNSAFE from untrusted sources
promptLLM-evaluated (SDK uses Haiku internally); $ARGUMENTS substitutionSafe enough that mergeSettings() allows from user settings
agentDispatched subagent (Task tool semantics)E.g. "Verify tests pass: $ARGUMENTS"
httpHTTP webhook to external serviceThird-party policy server
✓ VERIFIED via SDK CLI source (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.
✓ VERIFIED.

All 22 programmatic hooks Orion ships

createSessionStartHook — reads continuity cache, returns &lt;session-continuity&gt;...&lt;/session-continuity&gt; block summarizing prior session if cache <24h old. createPreCompactHook — writes {summary, files, activeProject, sessionType, turnCount} to ~/.cache/orion/continuity/&lt;conversationId&gt;.json (24h TTL). createStopHook — increments in-memory _turnCounters. Every 5th turn writes {turns, filesModified, ts} to session-activity cache. Evicts oldest after 100 conversations. createSessionEndHookcleanupStaleCacheFiles() removes files >7 days old from continuity/, session-activity/, memory-enrichment/. Rate-limited via in-memory _lastCleanupTs + marker file.
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, &lt;system&gt;) + emphasis-word overload. Never denies — emits additionalContext warning only.
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 &lt;workflow-hint&gt; advisory.
createSubagentStartHook — injects &lt;project-context&gt;Active PARA project: ...&lt;/project-context&gt; if active project. Uses basename(projectPath) for cross-platform safety. createSubagentStopHook — log-only; SDK type system has NO hookSpecificOutput for SubagentStop per inline comment.
createCcwStopHook (Stop), createCcwRecoveryHook (PreCompact), createCcwContinuationHook (SessionStart) — CCW-mode handlers in src-tauri/sidecar/ccw/hooks/.
createMediaTracingHook (L565-648) — orion media &lt;type&gt; &lt;action&gt; 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

MatcherHookTimeout
(any)para-context-hook.mjs10s
(any)morning-trigger-hook.mjs5s
(any)automation-intent-hook.mjs5s
(any)initiative-intent-hook.mjs5s
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)

{
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "tool_input": { "command": "...", "file_path": "...", "pattern": "..." },
  "tool_use_id": "...",
  "tool_result": { "output": "...", "is_error": false },
  "prompt": "user message text",
  "session_id": "...",
  "conversation_id": "...",
  "cwd": "...",
  "agent_id": "...",
  "stop_hook_active": false,
  "compact_summary": "..."
}

Output variants (hookSpecificOutput)

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "reason": "Blocked: sensitive file"
  }
}
Used by 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 in options.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.mjs parallelizes.
  • 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":&lt;seconds&gt; in settings.json. Programmatic hooks have NO declared SDK-level timeout — they rely on internal AbortSignal.timeout() for fetch calls (e.g. capture-evaluator = 12s, failure-evaluator = 8s).
  • Blocking semantics — only PreToolUse can deny/mutate. All other events are advisory.
  • Stop loop guards — Stop hooks check input.stop_hook_active || input.stopHookActive and return {ok:true} if set, preventing infinite Stop→respond→Stop cycles.

Open questions / honest gaps

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.
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.
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.
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.
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.
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

Hooks literal passed to query() at L2789-2940. settingSources at L2727. CLAUDE_CONFIG_DIR at L2733. Inline factories: createEnvIsolationHook L532-555, createMediaTracingHook L565-648.
mergeSettings() at L540-581. Security carve-out filtering user-side type:command at L553-565.
All 22 programmatic hook files. Largest: para-context-hook.mjs (757 LOC). Most critical: protection-hook.mjs, document-guard.mjs, lifecycle-hooks.mjs.
Orion’s runtime declarative hooks (15 hook entries, all type:"command").
CCW-mode hooks: stop-handler, recovery-handler, continuation-hook, keyword-detector. Wired only when ccwModeRegistry is non-null.
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?