Hook Lifecycle - Claude-Mem.md15.2 KBit/ai/Hook Lifecycle - Claude-Mem.md
---
title: "Hook Lifecycle - Claude-Mem"
source_url: "https://docs.claude-mem.ai/architecture/hooks"
source_site: "docs.claude-mem.ai"
clipped_at: "2026-07-07T00:56:18.891684+00:00"
clipper: "aiwiki-url-ingest"
extractor: "readability_static"
source_strategy: "normal_web_clip"
source_strategy_label: "普通网页抓取"
---
# 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:**
```
const workerPort = process.env.CLAUDE_MEM_WORKER_PORT ?? "<worker-port>"
const workerBaseUrl = `http://127.0.0.1:${workerPort}`
// 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(`${workerBaseUrl}/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(`${workerBaseUrl}/sessions/init`, {
method: 'POST',
body: JSON.stringify({ sessionId, project, userPrompt: prompt })
})
})
// VS Code Extension - PostToolUse Hook (middleware pattern)
workspace.onDidSaveTextDocument(async (document) => {
await fetch(`${workerBaseUrl}/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
| Stage | Hook | Trigger | Purpose |
| --- | --- | --- | --- |
| **1\. SessionStart** | `context-hook.js` | User opens Claude Code | Inject prior context silently |
| **2\. UserPromptSubmit** | `new-hook.js` | User submits a prompt | Create/get session, save prompt, init worker |
| **3\. PostToolUse** | `save-hook.js` | Claude uses any tool | Queue observation for AI compression |
| **4\. Stop** | `summary-hook.js` | User stops asking questions | Generate session summary |
| **5\. SessionEnd** | `cleanup-hook.js` | Session closes | Mark session completed |
## Hook Configuration
Hooks are configured in `plugin/hooks/hooks.json`:
```
{
"hooks": {
"Setup": [{
"hooks": [{
"type": "command",
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/version-check.js",
"timeout": 60
}]
}],
"SessionStart": [{
"matcher": "startup|clear|compact",
"hooks": [{
"type": "command",
"command": "bun ${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs start",
"timeout": 60
}, {
"type": "command",
"command": "bun ${CLAUDE_PLUGIN_ROOT}/scripts/context-hook.js",
"timeout": 60
}]
}],
"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. `worker-service.cjs start` \- Starts the worker service
2. `context-hook.js` \- Fetches and silently injects prior session context
(Runtime setup is handled out\-of\-band by `npx claude-mem install` / `npx claude-mem repair`. The Setup phase runs a sub\-100ms `version-check.js` that prompts the user to repair if the `.install-version` marker is stale.)
As of Claude Code 2\.1\.0 (ultrathink update), SessionStart hooks no longer display user\-visible messages. Context is silently injected via `hookSpecificOutput.additionalContext`.
### 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:<worker-port>/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`
---
## 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 = stripMemoryTags(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:<worker-port>/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:<worker-port>/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:<worker-port>/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:<worker-port>/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:<worker-port>/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) and save-hook.js (tool_input, tool_response)
stripMemoryTags(content: 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-6',
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
### Database Schema
### Worker Service
### Hook Implementation
### SDK Integration
---
## 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
| Problem | Root Cause | Solution |
| --- | --- | --- |
| Session ID mismatch | Different `session_id` used in different hooks | Always use ID from hook input |
| Duplicate sessions | Creating new session instead of using existing | Use `INSERT OR IGNORE` with `session_id` as key |
| Blocking IDE | Waiting for full response | Use fire\-and\-forget with short timeouts |
| Memory tags in DB | Stripping tags in wrong layer | Strip at hook layer, before HTTP send |
| Worker not found | Health check too fast | Add retry loop with exponential backoff |
---