--- 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 ``` ┌─────────────────────────────────────────────────────────────────────┐ │ Claude Code IDE │ ├─────────────────────────────────────────────────────────────────────┤ │ SessionStart → UserPromptSubmit → PostToolUse → Stop → SessionEnd │ │ ↓ ↓ ↓ ↓ ↓ │ │ [context] [new] [save] [summary] [cleanup] │ │ ↓ ↓ ↓ ↓ ↓ │ │ └──────────────┴────────┬────────┴──────────┴─────────┘ │ │ ↓ │ │ HTTP (fire-and-forget) │ │ ↓ │ ├─────────────────────────────────────────────────────────────────────┤ │ Worker Service (PM2) │ │ ┌─────────────┐ ┌──────────────┐ ┌────────────────┐ │ │ │ SessionMgr │ │ SDK Agent │ │ DatabaseMgr │ │ │ └─────────────┘ └──────────────┘ └────────────────┘ │ │ ↓ │ │ Claude Agent SDK │ │ ↓ │ │ ┌────────────────┴────────────────┐ │ │ ↓ ↓ │ │ SQLite DB 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 ``` 1. USER SUBMITS PROMPT ↓ Claude Code → SessionStart hook ├─ context-hook.js │ └─ GET /api/context/inject → returns context markdown └─ user-message-hook.js └─ displays context info to user ↓ Claude Code → UserPromptSubmit hook ├─ new-hook.js │ ├─ db.createSDKSession(session_id, project, prompt) │ ├─ db.incrementPromptCounter(sessionDbId) │ ├─ stripMemoryTagsFromPrompt(prompt) │ ├─ db.saveUserPrompt(session_id, promptNumber, cleaned) │ └─ POST /sessions/{sessionDbId}/init → Worker │ ↓ │ Worker → SessionManager → SDK Agent │ ↓ │ Claude SDK processes init prompt │ 2. CLAUDE USES A TOOL ↓ Claude Code → PostToolUse hook ├─ save-hook.js │ ├─ Skip if tool in SKIP_TOOLS │ └─ POST /api/sessions/observations → Worker │ ↓ │ Worker → SDK Agent → Claude compresses observation │ ↓ │ Store in SQLite + Sync to Chroma │ ↓ │ Broadcast to SSE clients (viewer UI) │ 3. USER STOPS ASKING QUESTIONS ↓ Claude Code → Stop hook ├─ summary-hook.js │ ├─ Extract last messages from transcript │ └─ POST /api/sessions/summarize → Worker │ ↓ │ Worker → SDK Agent → Claude generates summary │ ↓ │ Store in SQLite + Sync to Chroma │ 4. SESSION CLOSES ↓ Claude Code → SessionEnd hook └─ cleanup-hook.js └─ POST /api/sessions/complete → Worker ↓ Mark session as 'completed' in database ``` --- ## Session ID Threading The same `session_id` flows through ALL hooks in a conversation: ``` SessionStart: session_id (from Claude Code) ↓ UserPromptSubmit: session_id (same) ├─ new-hook creates: sdk_sessions.claude_session_id = session_id ├─ returns: sessionDbId (primary key) └─ All subsequent operations use sessionDbId PostToolUse: session_id (same) ├─ createSDKSession(session_id, '', '') returns sessionDbId └─ All observations tagged with this sessionDbId Stop: session_id (same) └─ Summary tagged with same sessionDbId SessionEnd: session_id (same) └─ Mark sessionDbId as completed ``` 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