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.Why Hooks?
The Non-Invasive Requirement
Claude-Mem had several architectural constraints:- Can’t modify Claude Code: It’s a closed-source binary
- Must be fast: Can’t slow down the main session
- Must be reliable: Can’t break Claude Code if it fails
- Must be portable: Works on any project without configuration
The Hook System Advantage
Claude Code’s hook system provides exactly what we need:Lifecycle Events
SessionStart, UserPromptSubmit, PreToolUse (Read), PostToolUse, PostToolUseFailure, 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 registers hooks for 7 lifecycle events (SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, PostToolUseFailure, Stop, SessionEnd) plus a Setup hook.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 worker-service start asynchronously on startup, resume, clear, and compact. The synchronous context hook runs on startup, clear, and compact and starts the worker itself when needed. Continue and resume restore the existing conversation without printing or injecting a new timeline, preserving its prompt cache. A third, startup-only SessionStart entry keeps the hook launcher on PATH.
How Every Hook Is Launched
Each runtime hook is one short exec-form command:command + args, with no shell key. Claude Code resolves claude-mem on PATH and spawns it directly, with no bash, no shell string and no per-call shell startup. On Windows that also means no Git Bash requirement and no console window from a bash → node chain.
session-init, file-context, observation for PostToolUseFailure, summarize, session-end). See plugin/hooks/hooks.json for the full file; it is generated by scripts/build-hooks.js, and the build rejects any runtime entry that has a shell key or a command other than claude-mem.
The claude-mem launcher:
- A small compiled binary (
bun build --compileofplugin/scripts/claude-mem-launcher.cjs), placed bynpx claude-mem install/updateat~/.local/bin/claude-memon macOS and Linux, or%LOCALAPPDATA%\claude-mem\bin\claude-mem.exeon Windows, and added to PATH (shell rc file on POSIX, user Path on Windows). - Its only job: find the installed plugin root (
CLAUDE_PLUGIN_ROOT, then the newest plugin cache version, then the marketplace copy), runworker-service.cjswith the same arguments under Bun, pass the hook’s stdin through, and exit 0 on any failure with oneclaude-mem: <reason>, continuing without memoryline on stderr. It never exits 2, so it never blocks a prompt or a tool. - It is version-stable: it carries no claude-mem logic, so an update keeps working with the binary already on PATH, whether it is claude-mem’s own auto-update,
npx claude-mem updateor a marketplace update.claude-mem --versionprints its protocol number. - It is not the npm package’s
claude-memCLI. A global npmclaude-memearlier on PATH shadows it;npx claude-mem doctorreports that.
bin/ only on the Bash tool’s PATH, not on the hook PATH. The startup-only SessionStart entry, node ${CLAUDE_PLUGIN_ROOT}/scripts/ensure-launcher.cjs, compiles and places the launcher when it is missing or older than the plugin needs. It is synchronous and always exits 0. When the launcher is current it prints nothing; when the memory hooks cannot work it prints one {"systemMessage": "..."} line to stdout, which Claude Code shows to the user (see below). It runs alongside the other SessionStart hooks, so if the launcher was missing, the other hooks in that first session fail with Executable not found in $PATH: "claude-mem", and that session can miss memory context.
When the launcher is missing. Claude Code reports a missing hook executable as a non-blocking error on every hook call (Executable not found in $PATH: "claude-mem") and the session carries on without memory. When the launcher cannot be made active, session start shows:
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.
When: Only when Claude Code is started with claude --init, claude --init-only or claude -p --maintenance. Claude Code does not fire Setup on plugin install, plugin update or a normal session start, so for marketplace installs it is a manual repair path, not an automatic one.
What it does:
- Checks that every dependency declared in the plugin’s
package.json(plus thezodsubpaths the worker loads) is present in itsnode_modules, not just that the folder exists. - If any are missing, runs
bun install --productionin the plugin root (up to 120s), then checks again. A partial tree is kept, never deleted. - 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). - If the closure is complete and
.install-versionis missing, unreadable or from another version, writes the marker itself ({version, installedAt, bun}) instead of sending a working install to the installer. - Always exits 0 — never blocks a session.
"shell": "bash", which first resolves the plugin root and Node):
- ✅ Fast when the install is complete: a declared-dependency check, no install
- ✅ Repairs marketplace installs on
--init/--maintenance: installs missing dependencies and writes the marker - ✅ Names the missing modules when an install cannot be completed
- ✅ Always exit 0 — never blocks a session
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: A new Claude Code session starts, or after clear or compact (runs alongside the async worker-start SessionStart entry). Skipped on continue and resume. What it does:- Extracts project name from current working directory
- Queries SQLite for recent session summaries (last 10)
- Queries SQLite for recent observations (configurable, default 50)
- Formats as progressive disclosure index
- Outputs to stdout (automatically injected into context)
- ✅ 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 - ✅ The block is the memory timeline. It opens with one line,
Access Claude-Mem Rules + Tools: ~/.claude-mem/RULES-AND-TOOLS.md, then any outage warning and the open work-state lists, then the timeline. The newest timeline entry ends the block, with no footer, so later updates can append rows to it. - ✅ How to read the timeline (format, legend, mode, how to fetch details) and how to use claude-mem’s tools (work state, memory search, notes) are in
~/.claude-mem/RULES-AND-TOOLS.md, written whenever the block is rendered and its text changed. The tool guidance is also the MCP server’s instructions. - ✅ The terminal banner opens with the trial lines and the same Rules + Tools line, then the timeline. A blank line after the newest entry, then the closing line: the viewer link and how many timeline lines are shown (
View Observations Live @ http://localhost:37777 · 50 lines).
SessionStart (source compact). Codex never sends SessionStart to a subagent thread, and PostCompact output cannot carry context, so the PostCompact hook marks the thread (~/.claude-mem/state/codex-compaction-pending/) and the thread’s next PostToolUse returns the timeline as additionalContext, once. A thread whose SessionStart already re-injected the timeline does not get it again.
Codex subagents: Codex starts a subagent it spawns with SubagentStart, not SessionStart, so the SubagentStart hook gives the subagent the timeline. A forked subagent starts from a copy of its parent’s conversation, timeline included, so the hook skips it: it reads forked_from_id from the first line of the subagent’s rollout (transcript_path). A subagent whose rollout can’t be read gets the timeline.
Trusting new Codex hooks: Codex runs a plugin hook only after you trust it, and it treats a new or changed hook as untrusted. After an update that adds or changes a claude-mem hook, Codex says at startup that hooks need review. Trust them, or those hooks won’t run. codex exec can’t ask, so trust them once in interactive Codex first.
Output format:
src/cli/handlers/context.ts, run as claude-mem hook claude-code context
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:- Reads user prompt and session ID from stdin
- Creates new session record in SQLite
- Saves raw user prompt for full-text search (v4.2.0+)
- Starts Bun worker service if not running
- Returns immediately (non-blocking)
- ✅ 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)
src/cli/handlers/session-init.ts, run as claude-mem hook claude-code session-init
Hook 3: PostToolUse (Save Observation Hook)
Purpose: Capture tool execution observations for later processing When: Immediately after any tool completes successfully. A failed call (e.g. Bash exiting non-zero) arrives onPostToolUseFailure instead, which runs the same handler and records { error, is_interrupt } as the tool output, so memory also sees attempts that failed.
What it does:
- Receives tool name, input, output from stdin
- Finds active session for current project
- Enqueues observation in observation_queue table
- Returns immediately (processing happens in worker)
- ✅ Matcher:
*(captures all tools) - ✅ Non-blocking (just enqueues, doesn’t process)
- ✅ Worker processes observations asynchronously
- ✅ Parallel execution safe (each hook gets own stdin)
src/cli/handlers/observation.ts, run as claude-mem hook claude-code observation
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:- Gathers session observations from database
- Sends to Claude Agent SDK for summarization
- Processes response and extracts structured summary
- Stores in session_summaries table
- Copies the turn’s deliverables (a deck, a report, a page the agent made for you) into
~/.claude-mem/deliverables/<project>/and records them, so My Work links them on the request’s card
- ✅ Triggered by Stop lifecycle event
- ✅ Multiple summaries per session (v4.2.0+)
- ✅ Summaries are checkpoints, not endings
- ✅ Uses Claude Agent SDK for AI compression
- ✅ Deliverables are copied by the hook itself, from the transcript’s final turn (files its Write/Edit calls made outside the source tree, and files a
work_state_writeattached), before the worktree or scratch folder they live in can be deleted; the worker only records the copies.CLAUDE_MEM_DELIVERABLES_ENABLED=falseturns it off.
src/cli/handlers/summarize.ts, run as claude-mem hook claude-code summarize
Hook 5: SessionEnd (Cleanup Hook)
Purpose: Mark sessions as completed when they end When: Claude Code session ends (not on/clear)
What it does:
- Marks session as completed in database
- Allows worker to finish processing
- Performs graceful cleanup
- ✅ Graceful completion (v4.1.0+)
- ✅ No longer sends DELETE to workers
- ✅ Skips cleanup on
/clearcommands - ✅ Preserves ongoing sessions
- Interrupted summary generation
- Lost pending observations
- Race conditions
- Worker finishes important operations
- Summaries complete successfully
- Clean state transitions
src/cli/handlers/session-end.ts, run as claude-mem hook claude-code session-end
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 asyncBun 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 HTTP API
Technology: Express.js REST API on the worker’s per-user port (default37700 + (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 completionPattern 2: Queue-Based Processing
Principle: Decouple capture from processing- 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- Launcher can’t find the plugin or Bun → One stderr line, exit 0, session continues without memory
claude-memnot on PATH → Claude Code shows a non-blocking hook error and continues- 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 itHook Debugging
Debug Mode
Enable detailed hook execution logs:Common Issues
Hook not executing
Hook not executing
Symptoms: Hook command never runsDebugging:
- Check
/hooksmenu - is hook registered? - Verify matcher pattern (case-sensitive!)
- Check the launcher is on PATH:
command -v claude-mem && claude-mem --version(prints a protocol number), or runnpx claude-mem doctor - Look for
Executable not found in $PATH: "claude-mem"inclaude --debugoutput; see Troubleshooting
Hook times out
Hook times out
Symptoms: Hook execution exceeds timeoutDebugging:
- Check timeout setting (default 60s)
- Identify slow operation (database? network?)
- Move slow operation to worker
- Increase timeout if necessary
Context not injecting
Context not injecting
Symptoms: SessionStart hook runs but context missingDebugging:
- Check stdout (must be valid JSON or plain text)
- Verify no stderr output (pollutes JSON)
- Check exit code (must be 0)
- Look for npm install output (v4.3.1 fix)
Observations not captured
Observations not captured
Symptoms: PostToolUse hook runs but observations missingDebugging:
- Check database:
sqlite3 ~/.claude-mem/claude-mem.db "SELECT * FROM observation_queue" - Verify session exists:
SELECT * FROM sdk_sessions - Check worker status:
npm run worker:status - View worker logs:
npm run worker:logs
Testing Hooks Manually
Performance Considerations
Hook Execution Time
Target: < 100ms per hook Actual measurements:
Launcher overhead: the compiled
claude-mem launcher starts in about 10ms, and runtime hooks spawn no shell.
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-versionmarker if it is missing or from another version — no install. - Only an incomplete tree — a plugin-marketplace install or
claude plugin updatewhose dependencies were never installed — runsbun install(up to 120s, inside the Setup hook’s 300s timeout). Setup only fires forclaude --init/--maintenance, so this never lands on a normal session or the per-prompt path. npx claude-mem install/npx claude-mem repairdo the heavy lifting (Bun + uv,bun installinside 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
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)
- 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:- Frozen at startup: Hook configuration captured at start, changes require review
- User review required:
/hooksmenu shows changes, requires approval - Plugin isolation:
${CLAUDE_PLUGIN_ROOT}prevents path traversal - 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
- All data stored locally in
~/.claude-mem/claude-mem.db - No cloud uploads (API calls only for AI compression). Local installs send cmem.ai only your email and optional note (with the claude-mem version and OS) if you share them when an interactive terminal install asks, and a numbers-only usage summary (observation counts and token totals, no prompts, paths, or project names) when setup creates a sign-in link; nothing else is sent to cmem.ai unless you choose to upload your prompts during browser setup.
- 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_keyorANTHROPIC_API_KEYenv var - Worker inherits environment from Claude Code
- Never logged or stored in database
Key Takeaways
- Hooks are interfaces: They define clean boundaries between systems
- Non-blocking is critical: Hooks must return fast, workers do the heavy lifting
- Graceful degradation: Memory system can fail without breaking Claude Code
- Queue-based decoupling: Capture and processing happen independently
- Progressive disclosure: Context injection uses index-first approach
- Lifecycle alignment: Each hook has a clear, single purpose
Further Reading
- Claude Code Hooks Reference - Official documentation
- Progressive Disclosure - Context priming philosophy
- Architecture Evolution - v3 to v4 journey
- Worker Service Design - Background processing details
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.

