Hooks Architecture
Core principle
Observe the main Claude Code session from the outside, process observations in the background, inject context at the right time. Claude Mem is fundamentally a hook-driven system. Every piece of functionality happens in response to lifecycle events fired by Claude Code. The memory system never interrupts or modifies Claude Code’s behavior — it observes from the outside and provides value through lifecycle hooks.As of Claude Code 2.1.0 (ultrathink update), SessionStart hooks no longer display user-visible messages. Context is silently injected via
hookSpecificOutput.additionalContext.Why hooks?
Architectural constraints
Claude Mem had several non-negotiable requirements:- Can’t modify Claude Code — it’s a closed-source binary
- Must be fast — hooks can’t slow down the main session
- Must be reliable — a failure must not break Claude Code
- Must be portable — works on any project without per-project configuration
Lifecycle events
SessionStart, UserPromptSubmit, PostToolUse, Stop, and SessionEnd cover the full session lifecycle.
Non-blocking
Hooks run independently and don’t block Claude from continuing work.
Context injection
SessionStart hooks can add context to Claude’s initial system prompt via
additionalContext.Tool observation
PostToolUse receives all tool names, inputs, and outputs — the raw material for memory.
How hooks.json is structured
All hook registrations live in a single file atplugin/hooks/hooks.json. Claude Code reads this file at startup and registers the commands to run at each lifecycle event.
| Field | Purpose |
|---|---|
matcher | Regex or glob filter on the event subtype. "startup|clear|compact" means SessionStart fires only for those sources. "*" matches any tool name in PostToolUse. Omitting matcher means the hook fires unconditionally. |
type | Always "command" for shell commands. |
command | Shell command to execute. Receives hook data via stdin. |
timeout | Maximum seconds before Claude Code kills the hook process. |
Hook chaining in SessionStart
SessionStart runs three hooks in sequence for thestartup|clear|compact source:
smart-install.js— checks and installs dependencies (up to 300s timeout for first-time npm install)worker-service.cjs start— ensures the Bun worker process is running (60s timeout)worker-service.cjs hook claude-code context— fetches and injects prior context (60s timeout)
The 5 lifecycle hooks + smart-install pre-hook
Pre-hook: smart-install (before SessionStart context)
Smart install is not a lifecycle hook — it is a pre-hook script called via command chaining inhooks.json before the context hook runs.
When: Claude Code starts with source startup, clear, or compact
What it does:
- Checks whether dependencies need installation using a
.install-versionversion marker - Only runs
npm installwhen necessary: first-time setup, or whenpackage.jsonversion changed - Provides Windows-specific error messages for missing build tools
- Triggers Bun worker service startup
scripts/smart-install.js
v5.0.3: Version caching reduced SessionStart time from 2–5 seconds (npm install) to ~10ms on cached runs.
Hook 1: SessionStart — context injection
Purpose: Inject relevant context from previous sessions into Claude’s initial prompt When: Claude Code starts (runs after smart-install) What it does:- Extracts the project name from the current working directory
- Queries SQLite for recent session summaries (last 10)
- Queries SQLite for recent observations (configurable via
CLAUDE_MEM_CONTEXT_OBSERVATIONS, default 50) - Formats the results as a progressive disclosure index — titles and metadata, not full content
- Outputs to stdout as
hookSpecificOutput.additionalContext(silently injected, not shown to user)
src/hooks/context-hook.ts → plugin/scripts/context-hook.js
Hook 2: UserPromptSubmit — new session
Purpose: Initialize session tracking when the user submits a prompt When: Before Claude processes the user’s message What it does:- Reads the user prompt and session ID from stdin
- Creates a new session record in SQLite (idempotent — uses
INSERT OR IGNORE) - Saves the raw user prompt for full-text search (v4.2.0+)
- Starts the Bun worker service if not already running
- Returns immediately — non-blocking
src/hooks/new-hook.ts → plugin/scripts/new-hook.js
Hook 3: PostToolUse — observation capture
Purpose: Capture tool execution data for asynchronous AI compression When: Immediately after any tool completes (matcher:*)
What it does:
- Receives tool name, input, and output from stdin
- Checks the skip list — discards low-value tools (
TodoWrite,AskUserQuestion,ListMcpResourcesTool,SlashCommand,Skill) - Strips privacy tags (
<private>...</private>) from input and response - POSTs the observation to the worker via HTTP (fire-and-forget, 2s timeout)
- Returns immediately — worker handles AI compression asynchronously
src/hooks/save-hook.ts → plugin/scripts/save-hook.js
Hook 4: Stop — summary generation
Purpose: Generate an AI-powered session summary when Claude stops When: When Claude stops responding (user pauses or stops the session) What it does:- Reads the transcript JSONL file to extract the last user message and last assistant message
- Sends a summarization request to the worker via HTTP (fire-and-forget)
- Worker queues summarization for the SDK agent asynchronously
src/hooks/summary-hook.ts → plugin/scripts/summary-hook.js
Hook 5: SessionEnd — cleanup
Purpose: Mark the session as completed when it ends When: Claude Code session closes (not on/clear)
What it does:
- POSTs a completion request to the worker with the session ID and reason
- Worker marks the session as
completedin SQLite - Worker broadcasts the session completion event to SSE clients (viewer UI)
DELETE to the worker, immediately killing any in-progress summary generation. Since v4.1.0 the hook simply marks the session complete, allowing the worker to finish processing naturally:
src/hooks/cleanup-hook.ts → plugin/scripts/cleanup-hook.js
Exit code strategy
Claude Mem hooks use specific exit codes per Claude Code’s hook contract:| Exit code | Meaning | When Claude Mem uses it |
|---|---|---|
0 | Success or graceful shutdown | All normal hook completions; worker/hook errors that should not surface to the user |
1 | Non-blocking error | Stderr shown to user, session continues |
2 | Blocking error | Stderr fed to Claude for processing; session pauses |
Stdin / stdout data flow
Every hook receives structured JSON from Claude Code on stdin and writes its response to stdout.Stdin (hook receives from Claude Code)
session_id and cwd are present for all events.
Stdout (hook sends back to Claude Code)
For most hooks, a simple acknowledgement:Hook timing reference
| Event | Timeout | Blocking | Output handling |
|---|---|---|---|
| SessionStart — smart-install | 300s | No | stderr (log only) |
| SessionStart — worker start | 60s | No | stderr (log only) |
| SessionStart — context | 60s | No | JSON → additionalContext (silent) |
| UserPromptSubmit | 60s | No | stdout → context |
| PostToolUse | 120s | No | Transcript only |
| Stop | 120s | No | Database (async) |
| SessionEnd | 5s | No | Log only |
The worker service: why hooks stay fast
Hooks must return in well under a second. AI compression of a single observation takes 5–30 seconds. The solution is a queue-based architecture:Design patterns
Pattern 1: Fire-and-forget hooks
Pattern 2: Queue-based decoupling
Pattern 3: Graceful degradation
- Database locked → skip observation, log error
- Worker crashed → auto-restart via Bun
- Network issue → retry with exponential backoff
- Disk full → warn user, disable memory
Pattern 4: Progressive disclosure context
Context injection uses an index format, not full observation text:Debugging hooks
Debug mode
Testing hooks manually
Common issues
Hook not executing
Hook not executing
Symptoms: Hook command never runs.
- Open
/hooksin Claude Code — is the hook registered? - Check the
matcherpattern (case-sensitive regex). - Test the command manually:
echo '{}' | node save-hook.js - Verify file permissions (executable bit set?).
Hook times out
Hook times out
Symptoms: Hook execution exceeds timeout, Claude Code kills it.
- Check the
timeoutsetting (default 60s, smart-install has 300s). - Identify the slow operation — is it a database call or network request?
- Move slow operations to the worker process.
- Increase
timeoutinhooks.jsonif necessary.
Context not injecting
Context not injecting
Symptoms: SessionStart hook runs but prior context is missing.
- Check stdout — must be valid JSON, no extra output.
- Verify no stderr output is mixed into stdout (npm install logs polluted this in v4.3.0; fixed in v4.3.1).
- Check exit code (must be 0).
- Confirm
hookSpecificOutput.additionalContextis present in the JSON.
Observations not captured
Observations not captured
Symptoms: PostToolUse hook runs but no observations appear in the viewer.
- Check the observation queue:
sqlite3 ~/.claude-mem/claude-mem.db "SELECT * FROM observation_queue" - Verify the session exists:
SELECT * FROM sdk_sessions - Check worker status:
npm run worker:status - View worker logs:
npm run worker:logs
Hook performance measurements
| Hook | Average | p95 | p99 |
|---|---|---|---|
| SessionStart — smart-install (cached) | 10ms | 20ms | 40ms |
| SessionStart — smart-install (first run) | 2,500ms | 5,000ms | 8,000ms |
| SessionStart — context | 45ms | 120ms | 250ms |
| UserPromptSubmit | 12ms | 25ms | 50ms |
| PostToolUse | 8ms | 15ms | 30ms |
| SessionEnd | 5ms | 10ms | 20ms |
.install-version marker (v5.0.3) ensures it is skipped on all subsequent startups.
Key takeaways
- Hooks are interfaces — they define clean boundaries between Claude Code and the memory system.
- Non-blocking is critical — hooks must return fast; the worker does the heavy lifting.
- Graceful degradation — the memory system can fail silently without breaking Claude Code.
- Queue-based decoupling — capture and processing are independent pipelines.
- Progressive disclosure — context injection uses an index-first approach to minimize token cost.
- Single responsibility — each hook has one clear purpose.