Skip to main content

Hook Lifecycle

Claude-Mem implements a 5-stage hook system that captures development work across Claude Code sessions. This document provides a complete technical reference for developers implementing this pattern on other platforms.

Architecture Overview

System Architecture

This two-process architecture works in both Claude Code and VS Code: Key Principles:
  • Extension process never blocks (fire-and-forget HTTP)
  • Worker processes observations asynchronously
  • Session state persists across IDE restarts

VS Code Extension API Integration Points

For developers porting to VS Code, here’s where to hook into the VS Code Extension API: Implementation Examples:
// VS Code Extension - SessionStart Hook
export async function activate(context: vscode.ExtensionContext) {
  const sessionId = generateSessionId()
  const project = workspace.name || 'default'

  // Fetch context from worker
  const response = await fetch(`http://localhost:37777/api/context/inject?project=${project}`)
  const context = await response.text()

  // Inject into chat or UI panel
  injectContextToChat(context)
}

// VS Code Extension - UserPromptSubmit Hook
const command = vscode.commands.registerCommand('extension.command', async (prompt) => {
  await fetch('http://localhost:37777/sessions/init', {
    method: 'POST',
    body: JSON.stringify({ sessionId, project, userPrompt: prompt })
  })
})

// VS Code Extension - PostToolUse Hook (middleware pattern)
workspace.onDidSaveTextDocument(async (document) => {
  await fetch('http://localhost:37777/api/sessions/observations', {
    method: 'POST',
    body: JSON.stringify({
      claudeSessionId: sessionId,
      tool_name: 'FileSave',
      tool_input: { path: document.uri.path },
      tool_response: 'File saved successfully'
    })
  })
})

Async Processing Pipeline

How observations flow from extension to database without blocking the IDE: Critical Pattern: The extension’s HTTP call has a 2-second timeout and doesn’t wait for AI processing. The worker handles compression asynchronously using an event-driven queue.

The 5 Lifecycle Stages

StageHookTriggerPurpose
1. SessionStartcontext-hook.js + user-message-hook.jsUser opens Claude CodeInject prior context, show UI messages
2. UserPromptSubmitnew-hook.jsUser submits a promptCreate/get session, save prompt, init worker
3. PostToolUsesave-hook.jsClaude uses any toolQueue observation for AI compression
4. Stopsummary-hook.jsUser stops asking questionsGenerate session summary
5. SessionEndcleanup-hook.jsSession closesMark session completed

Hook Configuration

Hooks are configured in plugin/hooks/hooks.json:
{
  "hooks": {
    "SessionStart": [{
      "matcher": "startup|clear|compact",
      "hooks": [{
        "type": "command",
        "command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/context-hook.js",
        "timeout": 300
      }, {
        "type": "command",
        "command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/user-message-hook.js",
        "timeout": 10
      }]
    }],
    "UserPromptSubmit": [{
      "hooks": [{
        "type": "command",
        "command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/new-hook.js",
        "timeout": 120
      }]
    }],
    "PostToolUse": [{
      "matcher": "*",
      "hooks": [{
        "type": "command",
        "command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/save-hook.js",
        "timeout": 120
      }]
    }],
    "Stop": [{
      "hooks": [{
        "type": "command",
        "command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/summary-hook.js",
        "timeout": 120
      }]
    }],
    "SessionEnd": [{
      "hooks": [{
        "type": "command",
        "command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/cleanup-hook.js",
        "timeout": 120
      }]
    }]
  }
}

Stage 1: SessionStart

Timing: When user opens Claude Code or resumes session Hooks Triggered (in order):
  1. context-hook.js - Fetches and injects prior session context
  2. user-message-hook.js - Displays context info to user via stderr

Sequence Diagram

Context Hook (context-hook.js)

Purpose: Inject context from previous sessions into Claude’s initial context. Input (via stdin):
{
  "session_id": "claude-session-123",
  "cwd": "/path/to/project",
  "source": "startup"
}
Processing:
  1. Wait for worker to be available (health check, max 10 seconds)
  2. Call: GET http://127.0.0.1:37777/api/context/inject?project={project}
  3. Return formatted context as additionalContext in hookSpecificOutput
Output (via stdout):
{
  "hookSpecificOutput": {
    "hookEventName": "SessionStart",
    "additionalContext": "<<formatted context markdown>>"
  }
}
Implementation: src/hooks/context-hook.ts

User Message Hook (user-message-hook.js)

Purpose: Display helpful user messages during first-time setup or when viewing context. Behavior:
  • Shows first-time setup message when node_modules is missing
  • Displays formatted context information with colors
  • Provides tips for using claude-mem effectively
  • Shows link to viewer UI (http://localhost:37777)
  • Uses stderr as communication channel (only output available in Claude Code UI)
Implementation: src/hooks/user-message-hook.ts

Stage 2: UserPromptSubmit

Timing: When user submits any prompt in a session Hook: new-hook.js

Sequence Diagram

Key Pattern: The INSERT OR IGNORE ensures the same session_id always maps to the same sessionDbId, enabling conversation continuations. Input (via stdin):
{
  "session_id": "claude-session-123",
  "cwd": "/path/to/project",
  "prompt": "User's actual prompt text"
}
Processing Steps:
// 1. Extract project name from working directory
project = path.basename(cwd)

// 2. Create or get database session (IDEMPOTENT)
sessionDbId = db.createSDKSession(session_id, project, prompt)
// INSERT OR IGNORE: Creates new row if first prompt, returns existing if continuation

// 3. Increment prompt counter
promptNumber = db.incrementPromptCounter(sessionDbId)
// Returns 1 for first prompt, 2 for continuation, etc.

// 4. Strip privacy tags
cleanedPrompt = stripMemoryTagsFromPrompt(prompt)
// Removes <private>...</private> and <claude-mem-context>...</claude-mem-context>

// 5. Skip if fully private
if (!cleanedPrompt || cleanedPrompt.trim() === '') {
  return  // Don't save, don't call worker
}

// 6. Save user prompt to database
db.saveUserPrompt(session_id, promptNumber, cleanedPrompt)

// 7. Initialize session via worker HTTP
POST http://127.0.0.1:37777/sessions/{sessionDbId}/init
Body: { project, userPrompt, promptNumber }
Output:
{ "continue": true, "suppressOutput": true }
Implementation: src/hooks/new-hook.ts
The same session_id flows through ALL hooks in a conversation. The createSDKSession call is idempotent - it returns the existing session for continuation prompts.

Stage 3: PostToolUse

Timing: After Claude uses any tool (Read, Bash, Grep, Write, etc.) Hook: save-hook.js

Sequence Diagram

Key Pattern: The hook returns immediately after HTTP POST. AI compression happens asynchronously in the worker without blocking Claude’s tool execution. Input (via stdin):
{
  "session_id": "claude-session-123",
  "cwd": "/path/to/project",
  "tool_name": "Read",
  "tool_input": { "file_path": "/src/index.ts" },
  "tool_response": "file contents..."
}
Processing Steps:
// 1. Check blocklist - skip low-value tools
const SKIP_TOOLS = {
  'ListMcpResourcesTool',  // MCP infrastructure noise
  'SlashCommand',          // Command invocation
  'Skill',                 // Skill invocation
  'TodoWrite',             // Task management meta-tool
  'AskUserQuestion'        // User interaction
}

if (SKIP_TOOLS[tool_name]) return

// 2. Ensure worker is running
await ensureWorkerRunning()

// 3. Send to worker (fire-and-forget HTTP)
POST http://127.0.0.1:37777/api/sessions/observations
Body: {
  claudeSessionId: session_id,
  tool_name,
  tool_input,
  tool_response,
  cwd
}
Timeout: 2000ms
Worker Processing:
  1. Looks up or creates session: createSDKSession(claudeSessionId, '', '')
  2. Gets prompt counter
  3. Checks privacy (skips if user prompt was entirely private)
  4. Strips memory tags from tool_input and tool_response
  5. Queues observation for SDK agent processing
  6. SDK agent calls Claude to compress into structured observation
  7. Stores observation in database and syncs to Chroma
Output:
{ "continue": true, "suppressOutput": true }
Implementation: src/hooks/save-hook.ts

Stage 4: Stop

Timing: When user stops or pauses asking questions Hook: summary-hook.js

Sequence Diagram

Key Pattern: The summary is generated asynchronously and doesn’t block the user from resuming work or closing the session. Input (via stdin):
{
  "session_id": "claude-session-123",
  "cwd": "/path/to/project",
  "transcript_path": "/path/to/transcript.jsonl"
}
Processing Steps:
// 1. Extract last messages from transcript JSONL
const lines = fs.readFileSync(transcript_path, 'utf-8').split('\n')
// Find last user message (type: "user")
// Find last assistant message (type: "assistant", filter <system-reminder> tags)

// 2. Ensure worker is running
await ensureWorkerRunning()

// 3. Send summarization request (fire-and-forget HTTP)
POST http://127.0.0.1:37777/api/sessions/summarize
Body: {
  claudeSessionId: session_id,
  last_user_message: string,
  last_assistant_message: string
}
Timeout: 2000ms

// 4. Stop processing spinner
POST http://127.0.0.1:37777/api/processing
Body: { isProcessing: false }
Worker Processing:
  1. Queues summarization for SDK agent
  2. Agent calls Claude to generate structured summary
  3. Summary stored in database with fields: request, investigated, learned, completed, next_steps
Output:
{ "continue": true, "suppressOutput": true }
Implementation: src/hooks/summary-hook.ts

Stage 5: SessionEnd

Timing: When Claude Code session closes (exit, clear, logout, etc.) Hook: cleanup-hook.js

Sequence Diagram

Key Pattern: Session completion is tracked for analytics and UI updates, but doesn’t prevent the user from closing the IDE. Input (via stdin):
{
  "session_id": "claude-session-123",
  "cwd": "/path/to/project",
  "transcript_path": "/path/to/transcript.jsonl",
  "reason": "exit"
}
Processing Steps:
// Send session complete (fire-and-forget HTTP)
POST http://127.0.0.1:37777/api/sessions/complete
Body: {
  claudeSessionId: session_id,
  reason: string  // 'exit' | 'clear' | 'logout' | 'prompt_input_exit' | 'other'
}
Timeout: 2000ms
Worker Processing:
  1. Finds session by claudeSessionId
  2. Marks session as ‘completed’ in database
  3. Broadcasts session completion event to SSE clients
Output:
{ "continue": true, "suppressOutput": true }
Implementation: src/hooks/cleanup-hook.ts

Session State Machine

Understanding session lifecycle and state transitions: Key Insights:
  • session_id never changes during a conversation
  • sessionDbId is the database primary key for the session
  • promptNumber increments with each user prompt
  • State transitions are non-blocking (fire-and-forget pattern)

Database Schema

The session-centric data model that enables cross-session memory: Idempotency Pattern:
-- This ensures same session_id always maps to same sessionDbId
INSERT OR IGNORE INTO sdk_sessions (claude_session_id, project, first_user_prompt)
VALUES (?, ?, ?)
RETURNING id;

-- If already exists, returns existing row
-- If new, creates and returns new row
Foreign Key Cascade: All child tables (user_prompts, observations, session_summaries) use session_id foreign key referencing SDK_SESSIONS.id. This ensures:
  • All data for a session is queryable by sessionDbId
  • Session deletions cascade to child tables
  • Efficient joins for context injection
Never generate your own session IDs. Always use the session_id provided by the IDE - this is the source of truth for linking all data together.

Privacy & Tag Stripping

Dual-Tag System

// User-Level Privacy Control (manual)
<private>sensitive data</private>

// System-Level Recursion Prevention (auto-injected)
<claude-mem-context>...</claude-mem-context>

Processing Pipeline

Location: src/utils/tag-stripping.ts
// Called by: new-hook.js (user prompts)
stripMemoryTagsFromPrompt(prompt: string): string

// Called by: save-hook.js (tool_input, tool_response)
stripMemoryTagsFromJson(jsonString: string): string
Execution Order (Edge Processing):
  1. new-hook.js strips tags from user prompt before saving
  2. save-hook.js strips tags from tool data before sending to worker
  3. Worker strips tags again (defense in depth) before storing

SDK Agent Processing

Query Loop (Event-Driven)

Location: src/services/worker/SDKAgent.ts
async startSession(session: ActiveSession, worker?: any) {
  // 1. Create event-driven message generator
  const messageGenerator = this.createMessageGenerator(session)

  // 2. Run Agent SDK query loop
  const queryResult = query({
    prompt: messageGenerator,
    options: {
      model: 'claude-sonnet-4-5',
      disallowedTools: ['Bash', 'Read', 'Write', ...],  // Observer-only
      abortController: session.abortController
    }
  })

  // 3. Process responses
  for await (const message of queryResult) {
    if (message.type === 'assistant') {
      await this.processSDKResponse(session, text, worker)
    }
  }
}

Message Types

The message generator yields three types of prompts:
  1. Initial Prompt (prompt #1): Full instructions for starting observation
  2. Continuation Prompt (prompt #2+): Context-only for continuing work
  3. Observation Prompts: Tool use data to compress into observations
  4. Summary Prompts: Session data to summarize

Implementation Checklist

For developers implementing this pattern on other platforms:

Hook Registration

  • Define hook entry points in platform config
  • 5 hook types: SessionStart (2 hooks), UserPromptSubmit, PostToolUse, Stop, SessionEnd
  • Pass session_id, cwd, and context-specific data

Database Schema

  • SQLite with WAL mode
  • 4 main tables: sdk_sessions, user_prompts, observations, session_summaries
  • Indices for common queries

Worker Service

  • HTTP server on configurable port (default 37777)
  • Bun runtime for process management
  • 3 core services: SessionManager, SDKAgent, DatabaseManager

Hook Implementation

  • context-hook: GET /api/context/inject (with health check)
  • new-hook: createSDKSession, saveUserPrompt, POST /sessions/{id}/init
  • save-hook: Skip low-value tools, POST /api/sessions/observations
  • summary-hook: Parse transcript, POST /api/sessions/summarize
  • cleanup-hook: POST /api/sessions/complete

Privacy & Tags

  • Implement stripMemoryTagsFromPrompt() and stripMemoryTagsFromJson()
  • Process tags at hook layer (edge processing)
  • Max tag count = 100 (ReDoS protection)

SDK Integration

  • Call Claude Agent SDK to process observations/summaries
  • Parse XML responses for structured data
  • Store to database + sync to vector DB

Key Design Principles

  1. Session ID is Source of Truth: Never generate your own session IDs
  2. Idempotent Database Operations: Use INSERT OR IGNORE for session creation
  3. Edge Processing for Privacy: Strip tags at hook layer before data reaches worker
  4. Fire-and-Forget for Non-Blocking: HTTP timeouts prevent IDE blocking
  5. Event-Driven, Not Polling: Zero-latency queue notification to SDK agent
  6. Everything Saves Always: No “orphaned” sessions

Common Pitfalls

ProblemRoot CauseSolution
Session ID mismatchDifferent session_id used in different hooksAlways use ID from hook input
Duplicate sessionsCreating new session instead of using existingUse INSERT OR IGNORE with session_id as key
Blocking IDEWaiting for full responseUse fire-and-forget with short timeouts
Memory tags in DBStripping tags in wrong layerStrip at hook layer, before HTTP send
Worker not foundHealth check too fastAdd retry loop with exponential backoff