docs/public/hooks-architecture.mdx
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:
┌─────────────────────────────────────────────────────────┐
│ CLAUDE CODE SESSION │
│ (Main session - user interacting with Claude) │
│ │
│ SessionStart → UserPromptSubmit → Tool Use → Stop │
│ ↓ ↓ ↓ ↓ ↓ ↓ │
│ [3 Hooks] [Hook] [Hook] [Hook] │
└─────────────────────────────────────────────────────────┘
↓ ↓ ↓ ↓ ↓ ↓
┌─────────────────────────────────────────────────────────┐
│ CLAUDE-MEM SYSTEM │
│ │
│ Smart Worker Context New Obs │
│ Install Start Inject Session Capture │
└─────────────────────────────────────────────────────────┘
Key insight: Claude-Mem doesn't interrupt or modify Claude Code's behavior. It observes from the outside and provides value through lifecycle hooks.
Claude-Mem had several architectural constraints:
Solution: External command hooks configured via settings.json
Claude Code's hook system provides exactly what we need:
<CardGroup cols={2}> <Card title="Lifecycle Events" icon="clock"> SessionStart, UserPromptSubmit, PreToolUse (Read), PostToolUse, Stop, SessionEnd </Card> <Card title="Non-Blocking" icon="forward"> Hooks run in parallel, don't wait for completion </Card> <Card title="Context Injection" icon="upload"> SessionStart and UserPromptSubmit can add context </Card> <Card title="Tool Observation" icon="eye"> PostToolUse sees all tool inputs and outputs </Card> </CardGroup>Claude-Mem uses lifecycle hook scripts across 5 lifecycle events. Runtime setup is handled out-of-band by npx claude-mem install (and npx claude-mem repair); the Setup hook only runs a sub-100ms version-check.js to flag stale installs. SessionStart runs 2 hooks in sequence: worker-service start, then context-hook.
Purpose: Detect stale installs caused by external plugin upgrades and prompt the user to repair.
Note: Runtime installation (Bun, uv, bun install in the plugin cache) is performed by npx claude-mem install and npx claude-mem repair — the Setup hook itself never installs anything.
When: Claude Code Setup phase, before every session.
What it does:
.install-version marker written by the npx installer.run: npx claude-mem repair to stderr.Configuration:
{
"hooks": {
"Setup": [{
"hooks": [{
"type": "command",
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/version-check.js",
"timeout": 60
}]
}]
}
}
Key Features:
claude plugin update)Source: scripts/version-check.js. The matching installer logic lives in npx claude-mem install / npx claude-mem repair, which install Bun + uv globally, run bun install in the plugin cache, and write the .install-version marker — all behind a visible clack spinner.
Purpose: Inject relevant context from previous sessions
When: Claude Code starts (runs after the worker-start SessionStart entry)
What it does:
Key decisions:
CLAUDE_MEM_CONTEXT_OBSERVATIONSOutput format:
# [claude-mem] recent context
**Legend:** 🎯 session-request | 🔴 gotcha | 🟡 problem-solution ...
### Oct 26, 2025
**General**
| ID | Time | T | Title | Tokens |
|----|------|---|-------|--------|
| #2586 | 12:58 AM | 🔵 | Context hook file empty | ~51 |
*Use MCP search tools to access full details*
Source: src/hooks/context-hook.ts → plugin/scripts/context-hook.js
Purpose: Initialize session tracking when user submits a prompt
When: Before Claude processes the user's message
What it does:
Configuration:
{
"hooks": {
"UserPromptSubmit": [{
"hooks": [{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/new-hook.js"
}]
}]
}
}
Key decisions:
suppressOutput: true)Database operations:
INSERT INTO sdk_sessions (claude_session_id, project, user_prompt, ...)
VALUES (?, ?, ?, ...)
INSERT INTO user_prompts (session_id, prompt, prompt_number, ...)
VALUES (?, ?, ?, ...)
Source: src/hooks/new-hook.ts → plugin/scripts/new-hook.js
Purpose: Capture tool execution observations for later processing
When: Immediately after any tool completes successfully
What it does:
Configuration:
{
"hooks": {
"PostToolUse": [{
"matcher": "*",
"hooks": [{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/save-hook.js"
}]
}]
}
}
Key decisions:
* (captures all tools)Database operations:
INSERT INTO observation_queue (session_id, tool_name, tool_input, tool_output, ...)
VALUES (?, ?, ?, ?, ...)
What gets queued:
{
"session_id": "abc123",
"tool_name": "Edit",
"tool_input": {
"file_path": "/path/to/file.ts",
"old_string": "...",
"new_string": "..."
},
"tool_output": {
"success": true,
"linesChanged": 5
},
"created_at_epoch": 1698765432
}
Source: src/hooks/save-hook.ts → plugin/scripts/save-hook.js
Purpose: Generate AI-powered session summaries during the session
When: When Claude stops (triggered by Stop lifecycle event)
What it does:
Configuration:
{
"hooks": {
"Stop": [{
"hooks": [{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/summary-hook.js"
}]
}]
}
}
Key decisions:
Summary structure:
<summary>
<request>User's original request</request>
<investigated>What was examined</investigated>
<learned>Key discoveries</learned>
<completed>Work finished</completed>
<next_steps>Remaining tasks</next_steps>
<files_read>
<file>path/to/file1.ts</file>
<file>path/to/file2.ts</file>
</files_read>
<files_modified>
<file>path/to/file3.ts</file>
</files_modified>
<notes>Additional context</notes>
</summary>
Source: src/hooks/summary-hook.ts → plugin/scripts/summary-hook.js
Purpose: Mark sessions as completed when they end
When: Claude Code session ends (not on /clear)
What it does:
Configuration:
{
"hooks": {
"SessionEnd": [{
"hooks": [{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/cleanup-hook.js"
}]
}]
}
}
Key decisions:
/clear commandsWhy graceful cleanup?
Old approach (v3):
// ❌ Aggressive cleanup
SessionEnd → DELETE /worker/session → Worker stops immediately
Problems:
New approach (v4.1.0+):
// ✅ Graceful completion
SessionEnd → UPDATE sessions SET completed_at = NOW()
Worker sees completion → Finishes processing → Exits naturally
Benefits:
Source: src/hooks/cleanup-hook.ts → plugin/scripts/cleanup-hook.js
sequenceDiagram
participant User
participant Claude
participant Hooks
participant Worker
participant DB
User->>Claude: Start Claude Code
Claude->>Hooks: SessionStart hook
Hooks->>DB: Query recent context
DB-->>Hooks: Session summaries + observations
Hooks-->>Claude: Inject context
Note over Claude: Context available for session
User->>Claude: Submit prompt
Claude->>Hooks: UserPromptSubmit hook
Hooks->>DB: Create session record
Hooks->>Worker: Start worker (if not running)
Worker-->>DB: Ready to process
Claude->>Claude: Execute tools
Claude->>Hooks: PostToolUse (multiple times)
Hooks->>DB: Queue observations
Note over Worker: Polls queue, processes observations
Worker->>Worker: AI compression
Worker->>DB: Store compressed observations
Worker->>Hooks: Trigger summary hook
Hooks->>DB: Store session summary
User->>Claude: Finish
Claude->>Hooks: SessionEnd hook
Hooks->>DB: Mark session complete
Worker->>DB: Check completion
Worker->>Worker: Finish processing
Worker->>Worker: Exit gracefully
| Event | Timing | Blocking | Timeout | Output Handling |
|---|---|---|---|---|
| Setup (version-check) | Before session | No | 60s | stderr hint on stale install (always exit 0) |
| SessionStart (worker-start) | Before session | No | 60s | stderr (log only) |
| SessionStart (context) | Before session | No | 60s | JSON → additionalContext (silent) |
| UserPromptSubmit | Before processing | No | 60s | stdout → context |
| PostToolUse | After tool | No | 120s | Transcript only |
| Summary | Worker triggered | No | 120s | Database |
| SessionEnd | On exit | No | 120s | Log only |
Problem: Hooks must be fast (< 1 second)
Reality: AI compression takes 5-30 seconds per observation
Solution: Hooks enqueue observations, worker processes async
┌─────────────────────────────────────────────────────────┐
│ HOOK (Fast) │
│ 1. Read stdin (< 1ms) │
│ 2. Insert into queue (< 10ms) │
│ 3. Return success (< 20ms total) │
└─────────────────────────────────────────────────────────┘
↓ (queue)
┌─────────────────────────────────────────────────────────┐
│ WORKER (Slow) │
│ 1. Poll queue every 1s │
│ 2. Process observation via Claude SDK (5-30s) │
│ 3. Parse and store results │
│ 4. Mark observation processed │
└─────────────────────────────────────────────────────────┘
Technology: Bun (JavaScript runtime and process manager)
Why Bun:
Worker lifecycle:
# Started by hooks automatically (if not running)
npm run worker:start
# Status check
npm run worker:status
# View logs
npm run worker:logs
# Restart
npm run worker:restart
# Stop
npm run worker:stop
Technology: Express.js REST API on the worker's per-user port (default 37700 + (uid % 100), override via CLAUDE_MEM_WORKER_PORT)
Endpoints:
| Endpoint | Method | Purpose |
|---|---|---|
/health | GET | Health check |
/sessions | POST | Create session |
/sessions/:id | GET | Get session status |
/sessions/:id | PATCH | Update session |
/observations | POST | Enqueue observation |
/observations/:id | GET | Get observation |
Why HTTP API?
Principle: Hooks should return immediately, not wait for completion
// ❌ Bad: Hook waits for processing
export async function saveHook(stdin: HookInput) {
const observation = parseInput(stdin);
await processObservation(observation); // BLOCKS!
return success();
}
// ✅ Good: Hook enqueues and returns
export async function saveHook(stdin: HookInput) {
const observation = parseInput(stdin);
await enqueueObservation(observation); // Fast
return success(); // Immediate
}
Principle: Decouple capture from processing
Hook (capture) → Queue (buffer) → Worker (process)
Benefits:
Principle: Memory system failure shouldn't break Claude Code
try {
await captureObservation();
} catch (error) {
// Log error, but don't throw
console.error('Memory capture failed:', error);
return { continue: true, suppressOutput: true };
}
Failure modes:
Principle: Core functionality works without memory, memory enhances it
Without memory: Claude Code works normally
With memory: Claude Code + context from past sessions
Memory broken: Falls back to working normally
Enable detailed hook execution logs:
claude --debug
Output:
[DEBUG] Executing hooks for PostToolUse:Write
[DEBUG] Getting matching hook commands for PostToolUse with query: Write
[DEBUG] Found 1 hook matchers in settings
[DEBUG] Matched 1 hooks for query "Write"
[DEBUG] Found 1 hook commands to execute
[DEBUG] Executing hook command: ${CLAUDE_PLUGIN_ROOT}/scripts/save-hook.js with timeout 60000ms
[DEBUG] Hook command completed with status 0: {"continue":true,"suppressOutput":true}
**Debugging:**
1. Check `/hooks` menu - is hook registered?
2. Verify matcher pattern (case-sensitive!)
3. Test command manually: `echo '{}' | node save-hook.js`
4. Check file permissions (executable?)
**Debugging:**
1. Check timeout setting (default 60s)
2. Identify slow operation (database? network?)
3. Move slow operation to worker
4. Increase timeout if necessary
**Debugging:**
1. Check stdout (must be valid JSON or plain text)
2. Verify no stderr output (pollutes JSON)
3. Check exit code (must be 0)
4. Look for npm install output (v4.3.1 fix)
**Debugging:**
1. Check database: `sqlite3 ~/.claude-mem/claude-mem.db "SELECT * FROM observation_queue"`
2. Verify session exists: `SELECT * FROM sdk_sessions`
3. Check worker status: `npm run worker:status`
4. View worker logs: `npm run worker:logs`
# Test context hook
echo '{
"session_id": "test123",
"cwd": "/Users/alex/projects/my-app",
"hook_event_name": "SessionStart",
"source": "startup"
}' | node plugin/scripts/context-hook.js
# Test save hook
echo '{
"session_id": "test123",
"tool_name": "Edit",
"tool_input": {"file_path": "test.ts"},
"tool_output": {"success": true}
}' | node plugin/scripts/save-hook.js
# Test with actual Claude Code
claude --debug
/hooks # View registered hooks
# Submit prompt and watch debug output
Target: < 100ms per hook
Actual measurements:
| Hook | Average | p95 | p99 |
|---|---|---|---|
| Setup (version-check, marker matches) | 8ms | 20ms | 40ms |
| Setup (version-check, marker mismatch — stderr hint, still non-blocking) | 10ms | 25ms | 50ms |
| SessionStart (context) | 45ms | 120ms | 250ms |
| SessionStart (user-message) | 5ms | 10ms | 15ms |
| UserPromptSubmit | 12ms | 25ms | 50ms |
| PostToolUse | 8ms | 15ms | 30ms |
| SessionEnd | 5ms | 10ms | 20ms |
Why the Setup hook stays fast:
.install-version marker — no npm install, no spawned subprocesses.bun install inside the plugin cache) happens in npx claude-mem install / npx claude-mem repair, which run with a visible clack spinner outside the session lifecycle.run: npx claude-mem repair hint to stderr and exits 0; the user opts into the slow path explicitly.Schema optimizations:
project, created_at_epoch, claude_session_idQuery patterns:
-- Fast: Uses index on (project, created_at_epoch)
SELECT * FROM session_summaries
WHERE project = ?
ORDER BY created_at_epoch DESC
LIMIT 10
-- Fast: Uses index on claude_session_id
SELECT * FROM sdk_sessions
WHERE claude_session_id = ?
LIMIT 1
-- Fast: FTS5 full-text search
SELECT * FROM observations_fts
WHERE observations_fts MATCH ?
ORDER BY rank
LIMIT 20
Bottleneck: Claude API latency (5-30s per observation)
Mitigation:
Future optimization:
Risk: Hooks execute arbitrary commands with user permissions
Mitigations:
/hooks menu shows changes, requires approval${CLAUDE_PLUGIN_ROOT} prevents path traversalWhat gets stored:
Privacy guarantees:
~/.claude-mem/claude-mem.dbConfiguration:
~/.anthropic/api_key or ANTHROPIC_API_KEY env varThe hook-driven architecture enables Claude-Mem to be both powerful and invisible. Users never notice the memory system working - it just makes Claude smarter over time.