Files
claude-mem/docs/public/architecture/hooks.mdx
T

617 lines
17 KiB
Plaintext

---
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": "<<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`
**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 <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**:
```json
{ "continue": true, "suppressOutput": true }
```
**Implementation**: `src/hooks/new-hook.ts`
<Note>
The same `session_id` flows through ALL hooks in a conversation. The `createSDKSession` call is idempotent - it returns the existing session for continuation prompts.
</Note>
---
## 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 <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**:
```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:<br/>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
```
<Warning>
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.
</Warning>
---
## Privacy & Tag Stripping
### Dual-Tag System
```typescript
// 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`
```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 `<private>` tags
- [Troubleshooting](/troubleshooting) - Common hook issues