Skip to main content

How Claude-Mem Uses Hooks: A Lifecycle-Driven Architecture

Core Principle

Observe the main Claude Code session from the outside, process observations in the background, inject context at the right time.

The Big Picture

Claude-Mem is fundamentally a hook-driven system. Every piece of functionality happens in response to lifecycle events:
As of Claude Code 2.1.0 (ultrathink update), SessionStart hooks no longer display user-visible messages. Context is silently injected via hookSpecificOutput.additionalContext.
Key insight: Claude-Mem doesn’t interrupt or modify Claude Code’s behavior. It observes from the outside and provides value through lifecycle hooks.

Why Hooks?

The Non-Invasive Requirement

Claude-Mem had several architectural constraints:
  1. Can’t modify Claude Code: It’s a closed-source binary
  2. Must be fast: Can’t slow down the main session
  3. Must be reliable: Can’t break Claude Code if it fails
  4. Must be portable: Works on any project without configuration
Solution: External command hooks configured via settings.json

The Hook System Advantage

Claude Code’s hook system provides exactly what we need:

Lifecycle Events

SessionStart, UserPromptSubmit, PreToolUse (Read), PostToolUse, Stop, SessionEnd

Non-Blocking

Hooks run in parallel, don’t wait for completion

Context Injection

SessionStart and UserPromptSubmit can add context

Tool Observation

PostToolUse sees all tool inputs and outputs

The Hook Scripts

Claude-Mem uses lifecycle hook scripts across 5 lifecycle events. npx claude-mem install (and npx claude-mem repair) set up the runtime; the Setup hook runs version-check.js, which completes a plugin-marketplace install whose dependencies are missing and records the install marker. SessionStart runs 2 hooks: worker-service start, which is async so session start never waits on it, and the synchronous context hook, which starts the worker itself when needed.

Setup Hook: Version Check

Purpose: Make sure the loaded plugin can start its worker: install the runtime dependencies the plugin marketplace left out, and record the .install-version marker. Note: npx claude-mem install and npx claude-mem repair install Bun, uv and the plugin dependencies up front. The plugin marketplace (including claude plugin update) only extracts files, so the Setup hook completes a missing dependency tree itself. When: Claude Code Setup phase, before every session. What it does:
  1. Checks that every dependency declared in the plugin’s package.json (plus the zod subpaths the worker loads) is present in its node_modules, not just that the folder exists.
  2. If any are missing, runs bun install --production in the plugin root (up to 120s), then checks again. A partial tree is kept, never deleted.
  3. If dependencies are still missing, prints claude-mem: plugin dependencies missing (…) - run: npx claude-mem@latest install, naming the modules (stderr; Codex receives it as SessionStart context).
  4. If the closure is complete and .install-version is missing, unreadable or from another version, writes the marker itself ({version, installedAt, bun}) instead of sending a working install to the installer.
  5. Always exits 0 — never blocks a session.
Configuration (simplified; the shipped command first resolves the plugin root):
Key Features:
  • ✅ Fast when the install is complete: a declared-dependency check, no install
  • ✅ Self-heals marketplace installs: installs missing dependencies and writes the marker, so a working install never sees a “run the installer” message
  • ✅ Names the missing modules when an install cannot be completed
  • ✅ Always exit 0 — never blocks a session
Source: plugin/scripts/version-check.js. npx claude-mem install / npx claude-mem repair do the same work ahead of time — they install Bun + uv globally, run bun install in the plugin cache, and write the .install-version marker — behind a visible clack spinner.

Hook 1: SessionStart - Context Injection

Purpose: Inject relevant context from previous sessions When: Claude Code starts (runs alongside the async worker-start SessionStart entry) What it does:
  1. Extracts project name from current working directory
  2. Queries SQLite for recent session summaries (last 10)
  3. Queries SQLite for recent observations (configurable, default 50)
  4. Formats as progressive disclosure index
  5. Outputs to stdout (automatically injected into context)
Key decisions:
  • ✅ Runs on startup, clear, and compact
  • ✅ 300-second timeout (allows for npm install if needed)
  • ✅ Progressive disclosure format (index, not full details)
  • ✅ Configurable observation count via CLAUDE_MEM_CONTEXT_OBSERVATIONS
Output format:
Source: src/hooks/context-hook.ts → plugin/scripts/context-hook.js

Hook 2: UserPromptSubmit (New Session Hook)

Purpose: Initialize session tracking when user submits a prompt When: Before Claude processes the user’s message What it does:
  1. Reads user prompt and session ID from stdin
  2. Creates new session record in SQLite
  3. Saves raw user prompt for full-text search (v4.2.0+)
  4. Starts Bun worker service if not running
  5. Returns immediately (non-blocking)
Configuration:
Key decisions:
  • ✅ No matcher (runs for all prompts)
  • ✅ Creates session record immediately
  • ✅ Stores raw prompts for search (privacy note: local SQLite only)
  • ✅ Auto-starts worker service
  • ✅ Suppresses output (suppressOutput: true)
Database operations:
Source: src/hooks/new-hook.ts → plugin/scripts/new-hook.js

Hook 3: PostToolUse (Save Observation Hook)

Purpose: Capture tool execution observations for later processing When: Immediately after any tool completes successfully What it does:
  1. Receives tool name, input, output from stdin
  2. Finds active session for current project
  3. Enqueues observation in observation_queue table
  4. Returns immediately (processing happens in worker)
Configuration:
Key decisions:
  • ✅ Matcher: * (captures all tools)
  • ✅ Non-blocking (just enqueues, doesn’t process)
  • ✅ Worker processes observations asynchronously
  • ✅ Parallel execution safe (each hook gets own stdin)
Database operations:
What gets queued:
Source: src/hooks/save-hook.ts → plugin/scripts/save-hook.js

Hook 4: Stop Hook (Summary Generation)

Purpose: Generate AI-powered session summaries during the session When: When Claude stops (triggered by Stop lifecycle event) What it does:
  1. Gathers session observations from database
  2. Sends to Claude Agent SDK for summarization
  3. Processes response and extracts structured summary
  4. Stores in session_summaries table
Configuration:
Key decisions:
  • ✅ Triggered by Stop lifecycle event
  • ✅ Multiple summaries per session (v4.2.0+)
  • ✅ Summaries are checkpoints, not endings
  • ✅ Uses Claude Agent SDK for AI compression
Summary structure:
Source: src/hooks/summary-hook.ts → plugin/scripts/summary-hook.js

Hook 5: SessionEnd (Cleanup Hook)

Purpose: Mark sessions as completed when they end When: Claude Code session ends (not on /clear) What it does:
  1. Marks session as completed in database
  2. Allows worker to finish processing
  3. Performs graceful cleanup
Configuration:
Key decisions:
  • ✅ Graceful completion (v4.1.0+)
  • ✅ No longer sends DELETE to workers
  • ✅ Skips cleanup on /clear commands
  • ✅ Preserves ongoing sessions
Why graceful cleanup? Old approach (v3):
Problems:
  • Interrupted summary generation
  • Lost pending observations
  • Race conditions
New approach (v4.1.0+):
Benefits:
  • Worker finishes important operations
  • Summaries complete successfully
  • Clean state transitions
Source: src/hooks/cleanup-hook.ts → plugin/scripts/cleanup-hook.js

Hook Execution Flow

Session Lifecycle

Hook Timing

As of Claude Code 2.1.0 (ultrathink update), SessionStart hooks no longer display user-visible messages. Context is silently injected via hookSpecificOutput.additionalContext.

The Worker Service Architecture

Why a Background Worker?

Problem: Hooks must be fast (< 1 second) Reality: AI compression takes 5-30 seconds per observation Solution: Hooks enqueue observations, worker processes async

Bun Process Management

Technology: Bun (JavaScript runtime and process manager) Why Bun:
  • Auto-restart on failure
  • Fast startup and low memory footprint
  • Built-in TypeScript support
  • Cross-platform (works on macOS, Linux, Windows)
  • No separate process manager needed
Worker lifecycle:

Worker HTTP API

Technology: Express.js REST API on the worker’s per-user port (default 37700 + (uid % 100), override via CLAUDE_MEM_WORKER_PORT) Endpoints: Why HTTP API?
  • Language-agnostic (hooks can be any language)
  • Easy debugging (curl commands)
  • Standard error handling
  • Proper async handling

Design Patterns

Pattern 1: Fire-and-Forget Hooks

Principle: Hooks should return immediately, not wait for completion

Pattern 2: Queue-Based Processing

Principle: Decouple capture from processing
Benefits:
  • Parallel hook execution safe
  • Worker failure doesn’t affect hooks
  • Retry logic centralized
  • Backpressure handling

Pattern 3: Graceful Degradation

Principle: Memory system failure shouldn’t break Claude Code
Failure modes:
  • 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 Enhancement

Principle: Core functionality works without memory, memory enhances it

Hook Debugging

Debug Mode

Enable detailed hook execution logs:
Output:

Common Issues

Symptoms: Hook command never runsDebugging:
  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?)
Symptoms: Hook execution exceeds timeoutDebugging:
  1. Check timeout setting (default 60s)
  2. Identify slow operation (database? network?)
  3. Move slow operation to worker
  4. Increase timeout if necessary
Symptoms: SessionStart hook runs but context missingDebugging:
  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)
Symptoms: PostToolUse hook runs but observations missingDebugging:
  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

Testing Hooks Manually


Performance Considerations

Hook Execution Time

Target: < 100ms per hook Actual measurements: When the Setup hook is fast, and when it is not:
  • With a complete dependency tree it only checks that the declared modules resolve, and rewrites the .install-version marker if it is missing or from another version — no install.
  • Only an incomplete tree — a plugin-marketplace install or claude plugin update whose dependencies were never installed — runs bun install (up to 120s, inside the Setup hook’s 300s timeout). Setup runs once per Claude Code launch, so this never lands on the per-prompt path.
  • npx claude-mem install / npx claude-mem repair do the heavy lifting (Bun + uv, bun install inside the plugin cache) up front with a visible clack spinner, so installs made that way never pay it at session start.

Database Performance

Schema optimizations:
  • Indexes on project, created_at_epoch, claude_session_id
  • FTS5 virtual tables for full-text search
  • WAL mode for concurrent reads/writes
Query patterns:

Worker Throughput

Bottleneck: Claude API latency (5-30s per observation) Mitigation:
  • Process observations sequentially (simpler, more predictable)
  • Skip low-value observations (TodoWrite, ListMcpResourcesTool)
  • Batch summaries (generate every N observations, not every observation)
Future optimization:
  • Parallel processing (multiple workers)
  • Smart batching (combine related observations)
  • Lazy summarization (summarize only when needed)

Security Considerations

Hook Command Safety

Risk: Hooks execute arbitrary commands with user permissions Mitigations:
  1. Frozen at startup: Hook configuration captured at start, changes require review
  2. User review required: /hooks menu shows changes, requires approval
  3. Plugin isolation: ${CLAUDE_PLUGIN_ROOT} prevents path traversal
  4. Input validation: Hooks validate stdin schema before processing

Data Privacy

What gets stored:
  • User prompts (raw text) - v4.2.0+
  • Tool inputs and outputs
  • File paths read/modified
  • Session summaries
Privacy guarantees:
  • All data stored locally in ~/.claude-mem/claude-mem.db
  • No cloud uploads (API calls only for AI compression). Local installs contact cmem.ai once, at signup, to create the sign-in link, with a numbers-only usage summary (observation counts and token totals, no prompts, paths, or project names); nothing else is sent to cmem.ai.
  • SQLite file permissions: user-only read/write
  • Product telemetry is opt-out and never carries prompts, code, or paths — see Telemetry

API Key Protection

Configuration:
  • Anthropic API key in ~/.anthropic/api_key or ANTHROPIC_API_KEY env var
  • Worker inherits environment from Claude Code
  • Never logged or stored in database

Key Takeaways

  1. Hooks are interfaces: They define clean boundaries between systems
  2. Non-blocking is critical: Hooks must return fast, workers do the heavy lifting
  3. Graceful degradation: Memory system can fail without breaking Claude Code
  4. Queue-based decoupling: Capture and processing happen independently
  5. Progressive disclosure: Context injection uses index-first approach
  6. Lifecycle alignment: Each hook has a clear, single purpose

Further Reading


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