Skip to main content

Hook scripts reference

Claude Mem registers hooks via plugin/hooks/hooks.json. At runtime, Claude Code fires lifecycle events and Claude Mem responds by running the appropriate script. This page documents every script, what it does, and the data contracts it expects.

Hooks.json — the complete configuration

The actual file shipped with the plugin:
${CLAUDE_PLUGIN_ROOT} is set by Claude Code to the installed plugin’s root directory. The fallback $HOME/.claude/plugins/marketplaces/thedotmack/plugin handles environments where the variable is not set.

The 5 lifecycle stages


Script 1: smart-install.js

File: scripts/smart-install.js (pre-hook, not a lifecycle hook) Lifecycle event: Runs via command chaining before context injection in SessionStart Matcher: startup|clear|compact Timeout: 300 seconds

What it does

Checks whether Node.js dependencies are up to date and installs them only when necessary. Without this, every session startup would run npm install (2–5 seconds). Decision logic:
  1. Does node_modules exist?
  2. Does .install-version match the current package.json version?
  3. Is better-sqlite3 present? (Legacy check — bun:sqlite requires no installation)
If all checks pass: skip install (~10ms). Otherwise: run npm install and write the new version to .install-version.
What it reads: package.json, .install-version marker file What it writes: .install-version marker file Exit codes:
  • 0 — Check complete (installed or skipped)
  • 1 — Installation error (shown to user on stderr)
Added in: v5.0.3

Script 2: worker-service.cjs start

File: plugin/scripts/worker-service.cjs Command argument: start Lifecycle event: SessionStart (second hook in sequence) Matcher: startup|clear|compact Timeout: 60 seconds

What it does

Ensures the Bun-managed worker process is running. Uses a three-layer health check:
  1. Does ~/.claude-mem/.worker.pid exist?
  2. Is the process with that PID alive? (kill(pid, 0))
  3. Does GET http://127.0.0.1:37777/health return OK?
If all three pass, the worker is already running and the hook returns immediately. Otherwise, it spawns a new Bun process. What it reads: ~/.claude-mem/.worker.pid, ~/.claude-mem/.worker.port What it writes: ~/.claude-mem/.worker.pid, ~/.claude-mem/.worker.port Exit codes:
  • 0 — Worker confirmed running
  • 0 — Worker started successfully (or startup errors are swallowed to prevent session breakage)

Script 3: context-hook.js (worker-service hook … context)

File: plugin/scripts/context-hook.js / worker-service.cjs hook claude-code context Lifecycle event: SessionStart (third hook in sequence) Source: src/hooks/context-hook.ts Matcher: startup|clear|compact Timeout: 60 seconds

What it does

Fetches context from previous sessions and injects it silently into Claude’s initial system prompt. Processing steps:
  1. Wait for worker to be healthy (health check with retry, max 10s)
  2. Extract project name from cwd: path.basename(cwd)
  3. Call GET http://127.0.0.1:37777/api/context/inject?project={project}
  4. Return the formatted markdown as hookSpecificOutput.additionalContext

Stdin

Stdout

Context format (progressive disclosure index):
What it reads: Worker HTTP API (/api/context/inject) → SQLite session_summaries and observations tables What it writes: Nothing (read-only) Configuration: CLAUDE_MEM_CONTEXT_OBSERVATIONS env var controls the number of observations shown (default: 50) Exit codes:
  • 0 — Context injected (or gracefully empty if worker unavailable)
As of Claude Code 2.1.0, context is injected silently via additionalContext. It is not shown to the user as a message.

Script 4: new-hook.js (worker-service hook … session-init)

File: plugin/scripts/new-hook.js / worker-service.cjs hook claude-code session-init Lifecycle event: UserPromptSubmit Source: src/hooks/new-hook.ts Matcher: None (runs for every prompt) Timeout: 60 seconds

What it does

Creates or retrieves the session record for this conversation and saves the user’s prompt for full-text search. Processing steps:
  1. Extract project name from cwd
  2. Strip privacy tags (<private>...</private>, <claude-mem-context>...</claude-mem-context>) from the prompt
  3. If prompt is entirely private after stripping, skip saving
  4. INSERT OR IGNORE INTO sdk_sessions — idempotent; returns existing row if session already exists (continuation prompts)
  5. Increment prompt_counter on the session row
  6. INSERT INTO user_prompts with the cleaned prompt text and prompt number
  7. POST http://127.0.0.1:37777/sessions/{sessionDbId}/init (fire-and-forget, 2s timeout)
Idempotency: Using the same session_id always maps to the same database row. This handles continuation prompts correctly.

Stdin

Stdout

What it reads: SQLite sdk_sessions table What it writes: SQLite sdk_sessions, user_prompts tables; HTTP POST to worker Exit codes:
  • 0 — Session initialized (or skipped for private prompts)
Never generate your own session IDs. Always use the session_id provided by Claude Code via stdin — this is the source of truth for linking all observations and summaries together.

Script 5: save-hook.js (worker-service hook … observation)

File: plugin/scripts/save-hook.js / worker-service.cjs hook claude-code observation Lifecycle event: PostToolUse Source: src/hooks/save-hook.ts Matcher: * (all tools) Timeout: 120 seconds

What it does

Captures tool execution data and sends it to the worker for asynchronous AI compression. The hook itself returns in ~8ms; the AI compression runs in the background over 1–30 seconds. Processing steps:
  1. Check the skip list — discard low-value tools immediately:
  2. Ensure worker is running (health check)
  3. Strip privacy tags from tool_input and tool_response
  4. HTTP POST to worker (fire-and-forget, 2s timeout):

Stdin

Stdout

What it reads: Stdin from Claude Code What it writes: HTTP POST to worker (worker writes to SQLite and Chroma) Async worker processing:
  1. createSDKSession(claudeSessionId, '', '') → returns sessionDbId
  2. Checks privacy (skips observation if user prompt was entirely private)
  3. Strips memory tags from tool_input and tool_response (defense-in-depth)
  4. Queues observation for SDK agent
  5. SDK agent calls Claude to compress into structured XML
  6. Stores compressed observation in database, syncs to Chroma vector DB
Exit codes:
  • 0 — Observation queued (or skipped for low-value/private tools)

Script 6: summary-hook.js (worker-service hook … summarize)

File: plugin/scripts/summary-hook.js / worker-service.cjs hook claude-code summarize Lifecycle event: Stop Source: src/hooks/summary-hook.ts Matcher: None (runs on every stop) Timeout: 120 seconds

What it does

Triggers asynchronous session summary generation when the user stops asking questions. The hook returns immediately; the actual summarization runs in the worker. Processing steps:
  1. Read the transcript JSONL file to extract:
    • Last user message (type: "user")
    • Last assistant message (type: "assistant", filtering out <system-reminder> tags)
  2. Ensure worker is running
  3. POST to worker (fire-and-forget, 2s timeout):
  4. Signal the worker to stop the processing spinner:

Stdin

Stdout

What it reads: Transcript JSONL file from disk What it writes: HTTP POST to worker (worker writes to SQLite session_summaries table) Async worker processing:
  1. Looks up sessionDbId from claudeSessionId
  2. Queues summarization for SDK agent
  3. SDK agent calls Claude with structured prompt
  4. Parses XML response with fields: request, investigated, learned, completed, next_steps
  5. Stores in session_summaries table, syncs to Chroma
Exit codes:
  • 0 — Summary queued

Script 7: cleanup-hook.js (inline SessionEnd)

File: Inline node -e "..." in hooks.json Lifecycle event: SessionEnd Source: src/hooks/cleanup-hook.ts Matcher: None (runs on all session ends) Timeout: 5 seconds

What it does

Marks the session as completed so the worker can update its state and broadcast to SSE clients (viewer UI). The hook is intentionally minimal — it is inlined directly in hooks.json to avoid a process startup penalty for a short-lived operation. Processing steps:
  1. Parse sessionId from stdin JSON
  2. POST to worker (fire-and-forget, 3s timeout):
  3. Exit 0 regardless of outcome (HTTP errors are swallowed)

Stdin

Stdout

None (HTTP fire-and-forget). What it reads: Stdin from Claude Code What it writes: HTTP POST to worker (worker updates SQLite, broadcasts SSE event) Async worker processing:
  1. Looks up sessionDbId
  2. UPDATE sdk_sessions SET status = 'completed', completed_at = NOW()
  3. Broadcasts session completion event to SSE clients (updates viewer UI)
Exit codes:
  • 0 — Always (failure to mark complete is non-fatal)

Session state machine

All seven scripts operate on the same session state:
Key invariants:
  • session_id never changes within a conversation
  • sessionDbId is the integer primary key — all operations use it
  • promptNumber increments with each UserPromptSubmit
  • State transitions are non-blocking — hooks fire and return

Database schema

The session-centric schema that all hook scripts write to:

Privacy and tag stripping

Both new-hook.js and save-hook.js strip special tags before data is saved:
Stripping happens at the hook layer (edge processing) before data is sent to the worker. The worker strips tags again as defense-in-depth. Functions: stripMemoryTagsFromPrompt() and stripMemoryTagsFromJson() — source: src/utils/tag-stripping.ts.

Common pitfalls