Skip to main content

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:
  1. Can’t modify Claude Code — it’s a closed-source binary
  2. Must be fast — hooks can’t slow down the main session
  3. Must be reliable — a failure must not break Claude Code
  4. Must be portable — works on any project without per-project configuration
Claude Code’s hook system provides exactly what’s needed:

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 at plugin/hooks/hooks.json. Claude Code reads this file at startup and registers the commands to run at each lifecycle event.
Key fields:

Hook chaining in SessionStart

SessionStart runs three hooks in sequence for the startup|clear|compact source:
  1. smart-install.js — checks and installs dependencies (up to 300s timeout for first-time npm install)
  2. worker-service.cjs start — ensures the Bun worker process is running (60s timeout)
  3. worker-service.cjs hook claude-code context — fetches and injects prior context (60s timeout)
All three run before Claude Code’s session becomes interactive. If any hook times out or exits with a non-blocking error code, Claude Code continues normally.

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 in hooks.json before the context hook runs. When: Claude Code starts with source startup, clear, or compact What it does:
  1. Checks whether dependencies need installation using a .install-version version marker
  2. Only runs npm install when necessary: first-time setup, or when package.json version changed
  3. Provides Windows-specific error messages for missing build tools
  4. Triggers Bun worker service startup
Version caching:
Source: 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:
  1. Extracts the project name from the current working directory
  2. Queries SQLite for recent session summaries (last 10)
  3. Queries SQLite for recent observations (configurable via CLAUDE_MEM_CONTEXT_OBSERVATIONS, default 50)
  4. Formats the results as a progressive disclosure index — titles and metadata, not full content
  5. Outputs to stdout as hookSpecificOutput.additionalContext (silently injected, not shown to user)
Output format:
Source: src/hooks/context-hook.tsplugin/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:
  1. Reads the user prompt and session ID from stdin
  2. Creates a new session record in SQLite (idempotent — uses INSERT OR IGNORE)
  3. Saves the raw user prompt for full-text search (v4.2.0+)
  4. Starts the Bun worker service if not already running
  5. Returns immediately — non-blocking
Database operations:
Output:
Source: src/hooks/new-hook.tsplugin/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:
  1. Receives tool name, input, and output from stdin
  2. Checks the skip list — discards low-value tools (TodoWrite, AskUserQuestion, ListMcpResourcesTool, SlashCommand, Skill)
  3. Strips privacy tags (<private>...</private>) from input and response
  4. POSTs the observation to the worker via HTTP (fire-and-forget, 2s timeout)
  5. Returns immediately — worker handles AI compression asynchronously
What gets sent to the worker:
Output:
Source: src/hooks/save-hook.tsplugin/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:
  1. Reads the transcript JSONL file to extract the last user message and last assistant message
  2. Sends a summarization request to the worker via HTTP (fire-and-forget)
  3. Worker queues summarization for the SDK agent asynchronously
Summary structure produced by the worker:
Source: src/hooks/summary-hook.tsplugin/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:
  1. POSTs a completion request to the worker with the session ID and reason
  2. Worker marks the session as completed in SQLite
  3. Worker broadcasts the session completion event to SSE clients (viewer UI)
Graceful vs. aggressive cleanup (v4.1.0+): Before v4.1.0, SessionEnd sent a 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:
Source: src/hooks/cleanup-hook.tsplugin/scripts/cleanup-hook.js

Exit code strategy

Claude Mem hooks use specific exit codes per Claude Code’s hook contract:
Worker and hook errors exit with code 0 by default to prevent Windows Terminal from accumulating error tab dialogs. The plugin wrapper layer handles restart logic separately.

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)

The exact fields depend on the lifecycle event. session_id and cwd are present for all events.

Stdout (hook sends back to Claude Code)

For most hooks, a simple acknowledgement:
For SessionStart context injection, a structured output:
Any output that is not valid JSON is treated as plain text and may be shown in the transcript. Stderr output is logged but does not affect context injection.

Hook timing reference


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:
The worker is a long-running Express.js server on port 37777, managed by Bun.

Design patterns

Pattern 1: Fire-and-forget hooks

Pattern 2: Queue-based decoupling

Captures and processing are independent. Worker failure doesn’t affect hooks. Retry logic lives in the worker.

Pattern 3: Graceful degradation

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 disclosure context

Context injection uses an index format, not full observation text:

Debugging hooks

Debug mode

Example output:

Testing hooks manually

Common issues

Symptoms: Hook command never runs.
  1. Open /hooks in Claude Code — is the hook registered?
  2. Check the matcher pattern (case-sensitive regex).
  3. Test the command manually: echo '{}' | node save-hook.js
  4. Verify file permissions (executable bit set?).
Symptoms: Hook execution exceeds timeout, Claude Code kills it.
  1. Check the timeout setting (default 60s, smart-install has 300s).
  2. Identify the slow operation — is it a database call or network request?
  3. Move slow operations to the worker process.
  4. Increase timeout in hooks.json if necessary.
Symptoms: SessionStart hook runs but prior context is missing.
  1. Check stdout — must be valid JSON, no extra output.
  2. Verify no stderr output is mixed into stdout (npm install logs polluted this in v4.3.0; fixed in v4.3.1).
  3. Check exit code (must be 0).
  4. Confirm hookSpecificOutput.additionalContext is present in the JSON.
Symptoms: PostToolUse hook runs but no observations appear in the viewer.
  1. Check the observation queue: sqlite3 ~/.claude-mem/claude-mem.db "SELECT * FROM observation_queue"
  2. Verify the session exists: SELECT * FROM sdk_sessions
  3. Check worker status: npm run worker:status
  4. View worker logs: npm run worker:logs

Hook performance measurements

Smart-install is slow only on first run or after a version change. The .install-version marker (v5.0.3) ensures it is skipped on all subsequent startups.

Key takeaways

  1. Hooks are interfaces — they define clean boundaries between Claude Code and the memory system.
  2. Non-blocking is critical — hooks must return fast; the worker does the heavy lifting.
  3. Graceful degradation — the memory system can fail silently without breaking Claude Code.
  4. Queue-based decoupling — capture and processing are independent pipelines.
  5. Progressive disclosure — context injection uses an index-first approach to minimize token cost.
  6. Single responsibility — each hook has one clear purpose.