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