--- title: "Hook Lifecycle" description: "Complete guide to the 5-stage memory agent lifecycle for platform implementers" --- # 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 ```mermaid flowchart TB subgraph IDE["Claude Code IDE"] SS[SessionStart] --> UPS[UserPromptSubmit] --> PTU[PostToolUse] --> ST[Stop] --> SE[SessionEnd] SS --> ctx["context"] UPS --> new["new"] PTU --> save["save"] ST --> sum["summary"] SE --> clean["cleanup"] end ctx & new & save & sum & clean --> HTTP["HTTP (fire-and-forget)"] subgraph Worker["Worker Service (PM2)"] SM[SessionMgr] ~~~ SA[SDK Agent] ~~~ DM[DatabaseMgr] SA --> SDK["Claude Agent SDK"] end HTTP --> Worker SDK --> SQLite[(SQLite DB)] SDK --> Chroma[(Chroma Vector DB)] ``` ## The 5 Lifecycle Stages | Stage | Hook | Trigger | Purpose | |-------|------|---------|---------| | **1. SessionStart** | `context-hook.js` + `user-message-hook.js` | User opens Claude Code | Inject prior context, show UI messages | | **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`: ```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 ### Context Hook (`context-hook.js`) **Purpose**: Inject context from previous sessions into Claude's initial context. **Input** (via stdin): ```json { "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): ```json { "hookSpecificOutput": { "hookEventName": "SessionStart", "additionalContext": "<>" } } ``` **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` **Input** (via stdin): ```json { "session_id": "claude-session-123", "cwd": "/path/to/project", "prompt": "User's actual prompt text" } ``` **Processing Steps**: ```typescript // 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 ... and ... // 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**: ```json { "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` **Input** (via stdin): ```json { "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**: ```typescript // 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**: ```json { "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` **Input** (via stdin): ```json { "session_id": "claude-session-123", "cwd": "/path/to/project", "transcript_path": "/path/to/transcript.jsonl" } ``` **Processing Steps**: ```typescript // 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 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**: ```json { "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` **Input** (via stdin): ```json { "session_id": "claude-session-123", "cwd": "/path/to/project", "transcript_path": "/path/to/transcript.jsonl", "reason": "exit" } ``` **Processing Steps**: ```typescript // 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**: ```json { "continue": true, "suppressOutput": true } ``` **Implementation**: `src/hooks/cleanup-hook.ts` --- ## Data Flow Diagram ```mermaid flowchart TD subgraph step1["1. USER SUBMITS PROMPT"] CC1[Claude Code] --> SS1[SessionStart hook] SS1 --> CH[context-hook.js] SS1 --> UMH[user-message-hook.js] CH --> |"GET /api/context/inject"| CTX[returns context markdown] UMH --> DISP[displays context info to user] CC1 --> UPS1[UserPromptSubmit hook] UPS1 --> NH[new-hook.js] NH --> |1| CREATE["db.createSDKSession()"] NH --> |2| INC["db.incrementPromptCounter()"] NH --> |3| STRIP["stripMemoryTagsFromPrompt()"] NH --> |4| SAVE["db.saveUserPrompt()"] NH --> |5| INIT["POST /sessions/{id}/init"] INIT --> W1[Worker] W1 --> SM1[SessionManager] SM1 --> SA1[SDK Agent] end subgraph step2["2. CLAUDE USES A TOOL"] CC2[Claude Code] --> PTU1[PostToolUse hook] PTU1 --> SH[save-hook.js] SH --> |"Skip if in SKIP_TOOLS"| CHECK{Check tool} CHECK --> |allowed| OBS["POST /api/sessions/observations"] OBS --> W2[Worker] W2 --> SA2["SDK Agent → Claude compresses"] SA2 --> STORE1["Store in SQLite + Chroma"] STORE1 --> SSE[Broadcast to SSE clients] end subgraph step3["3. USER STOPS ASKING QUESTIONS"] CC3[Claude Code] --> STOP1[Stop hook] STOP1 --> SUMH[summary-hook.js] SUMH --> EXT[Extract last messages from transcript] EXT --> SUM["POST /api/sessions/summarize"] SUM --> W3[Worker] W3 --> SA3["SDK Agent → Claude generates summary"] SA3 --> STORE2["Store in SQLite + Chroma"] end subgraph step4["4. SESSION CLOSES"] CC4[Claude Code] --> SE1[SessionEnd hook] SE1 --> CLN[cleanup-hook.js] CLN --> COMP["POST /api/sessions/complete"] COMP --> W4[Worker] W4 --> MARK["Mark session as 'completed'"] end step1 --> step2 step2 --> step3 step3 --> step4 ``` --- ## Session ID Threading The same `session_id` flows through ALL hooks in a conversation: ```mermaid flowchart TD SID["session_id (from Claude Code)"] subgraph SS["SessionStart"] SS_ID["session_id"] end subgraph UPS["UserPromptSubmit"] UPS_ID["session_id (same)"] UPS_CREATE["new-hook creates:
sdk_sessions.claude_session_id = session_id"] UPS_RET["returns: sessionDbId (primary key)"] UPS_ALL["All subsequent operations use sessionDbId"] UPS_ID --> UPS_CREATE --> UPS_RET --> UPS_ALL end subgraph PTU["PostToolUse"] PTU_ID["session_id (same)"] PTU_GET["createSDKSession() returns sessionDbId"] PTU_OBS["All observations tagged with sessionDbId"] PTU_ID --> PTU_GET --> PTU_OBS end subgraph STOP["Stop"] STOP_ID["session_id (same)"] STOP_SUM["Summary tagged with sessionDbId"] STOP_ID --> STOP_SUM end subgraph SEND["SessionEnd"] SEND_ID["session_id (same)"] SEND_MARK["Mark sessionDbId as completed"] SEND_ID --> SEND_MARK end SID --> SS --> UPS --> PTU --> STOP --> SEND ``` 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 ```typescript // User-Level Privacy Control (manual) sensitive data // System-Level Recursion Prevention (auto-injected) ... ``` ### Processing Pipeline **Location**: `src/utils/tag-stripping.ts` ```typescript // 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` ```typescript 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-haiku-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) - [ ] PM2 or equivalent 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 | 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 | --- ## Related Documentation - [Worker Service](/architecture/worker-service) - HTTP API and async processing - [Database Schema](/architecture/database) - SQLite tables and FTS5 search - [Privacy Tags](/usage/private-tags) - Using `` tags - [Troubleshooting](/troubleshooting) - Common hook issues