---
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": "<<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

```
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
```

<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
