Claude Code Hooks Design¶
Status¶
This document defines the Claude Code hook integration. The current implementation supports ten hooks: the original six (SessionStart, UserPromptSubmit, UserPromptExpansion, PreCompact, Stop, SessionEnd) plus four added in the second release (PreToolUse, PostToolUse, SubagentStart, SubagentStop). It also provides persistent segmented session memory, complete transcript text capture, context injection, and settings installation.
Structured semantic summarization, configurable budgets, stronger secret classification, cross-process MCP memory sharing, PostToolUseFailure capture, and a subagent parent/child segment split remain future work.
The design deliberately keeps Claude-specific protocol types at the integration boundary. Membrick's public ingestion and query APIs remain SourceDocument, MemoryService, MemoryIndexSnapshot, and SearchResult.
The entire adapter is compiled only with the non-default claude-code Cargo feature. Core library and Python builds do not include Claude protocol or CLI surfaces unless they explicitly enable that feature.
Goals¶
The hook integration should let Membrick act as a persistent memory layer for Claude Code:
- retrieve relevant project and session memory before Claude starts work
- preserve useful state before context compaction or session termination
- route retrieval to a small number of relevant memory segments
- reduce repeated repository exploration and repeated user explanations
- bound hook latency and the number of tokens injected into Claude's context
The integration stores every parsed user and assistant transcript observation in source order as session memory, including tool calls, tool results, empty messages, and image blocks. Text blocks and serialized structured blocks are kept in full. Credentials are redacted before persistence.
Each capture hook stores only the transcript written since the previous capture of the same session. The byte position of the last capture is kept per session under .membrick/claude-state/capture-cursors/, and it advances only after the capture is stored, so a failed capture is retried by the next hook. A trailing line that is still being written is left for the next capture.
Each parsed transcript observation in that slice is stored as its own stable SourceDocument with its speaker, order, provider message ID when available, timestamp when available, and complete text or serialized blocks. The slice's text is also stored once as a turn document of the hook's type (outcome, checkpoint, or session-summary) with project semantic scope, so its claims are compared with other sessions. Transcript discovery uses the hook path or project .membrick/settings.toml; it does not read environment variables.
One session-context document per session holds a short header and links the observations of the latest capture. Every capture is a single batch sent to POST /provider-memory/mcp/documents/upsert. The resident owner journals the batch and replies once it is durable; the background publisher makes it searchable, usually within the refresh interval. The server does not rebuild the session from the store, so a capture never waits for a full index refresh. If the transcript becomes shorter than the stored position, the capture starts from the beginning of the file and upserts the observations again. Observations from the old transcript that no longer exist remain as history. Session documents participate in normal hook retrieval. Hooks inject only query-matched excerpts, and the shared policy excludes conflicted results. Explicit search remains available for inspecting conflicts and full source documents.
Ownership Boundary¶
Claude Code owns hook names, hook payload JSON, matcher behavior, and hook response JSON. The Claude adapter translates those values into existing Membrick operations.
Claude hook JSON
|
v
Claude protocol and adapter
|
+---- retrieval ----> MemoryService::query(...)
| |
| v
| Vec<SearchResult>
| |
| v
| Claude additionalContext
|
+---- capture ------> ClaudeCodeDocument
|
v
SourceDocument
|
v
MemoryService::upsert(...)
The Claude integration does not construct MemoryIndex or SegmentedMemoryIndex directly. MemoryService owns the published MemoryIndexSnapshot, which can be either a single index or a segmented index.
Current Directory Structure¶
mod.rscontains MCP serving and configuration installation.document.rsdefinesClaudeCodeDocumentand converts it toSourceDocument.hooks/protocol.rscontains only Claude hook input and output schemas.hooks/mod.rsdispatches hooks, queriesMemoryService, extracts full transcript content, and captures durable memory.
MCP and hook behavior can be split into smaller files later when those modules grow independently. That refactor does not change the API boundary described here.
ClaudeCodeDocument¶
Only hook events selected for durable capture become a ClaudeCodeDocument. Retrieval-only hooks use their Claude protocol payload directly.
pub struct ClaudeCodeDocument {
pub event_id: String,
pub session_id: String,
pub document_type: ClaudeCodeDocumentType,
pub content: String,
pub cwd: PathBuf,
pub timestamp: Option<String>,
pub command_name: Option<String>,
pub command_args: Option<String>,
pub affected_paths: Vec<String>,
pub branch: Option<String>,
pub revision: Option<String>,
}
impl ClaudeCodeDocument {
pub fn into_source_document(self) -> anyhow::Result<SourceDocument>;
}
The object is Claude-specific, but its output is the existing generic ingestion type. A future Codex integration can define CodexDocument with its own mapping to SourceDocument.
SourceDocument Mapping¶
ClaudeCodeDocument::into_source_document() uses the following mapping:
SourceDocument field | Claude mapping |
|---|---|
doc_id | deterministic event or checkpoint ID |
source | claude-code://<project>/<session>/<type> |
content | filtered checkpoint, outcome, or session summary |
concept | document type, such as checkpoint or outcome |
group_id | stable project and session segment ID |
headings | structured capture sections, when present |
links | affected repository paths |
timestamp | event timestamp |
author_agent | claude-code |
filters | project, session, type, and command metadata |
The initial segment ID is:
project-id must be stable for the repository and must not expose the complete absolute path. Session segmentation gives the router a natural local-memory boundary. Profiles from every session remain available to the router, which allows relevant older sessions to be selected.
MemoryService Configuration¶
Claude interaction memory uses a dedicated persistent store:
The adapter opens it with MemoryService::at_path() and segmented layout:
let options = PipelineOptions {
memory_index_layout: MemoryIndexLayout::Segmented {
query_top_n: 3,
routing_strategy: SegmentRoutingStrategy::LocalDistinctiveness,
},
..PipelineOptions::default()
};
let store = MemoryService::at_path(memory_path, options)?;
Capture uses the current mutable API:
let source_document = claude_document.into_source_document()?;
store.upsert(source_document);
store.refresh()?;
Retrieval uses the current query API:
The same store can be inspected without reading internal persistence files:
membrick inspect-index <project>/.membrick/memory
membrick inspect-index <project>/.membrick/memory --view source-documents
membrick inspect-index <project>/.membrick/memory --view records
membrick inspect-index <project>/.membrick/memory --view segments
With segmented layout, MemoryService::search_with_filters() routes over the configured top-N segments. Its global index scores documents from the selected segments and is also the persistence surface. The current router returns no results when it finds no segment with useful signal; automatic all-segment fallback is not part of the current MemoryService API.
The Claude memory store is separate from the existing workspace MCP index in the first release. Command hooks are short-lived processes, while the MCP server is long-lived; sharing one mutable on-disk store across those processes would require explicit locking and snapshot reload semantics that MemoryService does not currently provide.
Supported Hooks¶
SessionStart¶
Purpose: restore useful memory when a session starts or resumes.
Operation:
- Read
session_id,cwd, and session source from the Claude payload. - Resolve the project memory store.
- Build a retrieval query for recent decisions, unresolved work, and failures.
- Call
MemoryService::search_with_filters(). - Format results within the session-start context budget.
- Return the text as Claude
additionalContext.
This hook does not create a SourceDocument.
UserPromptSubmit¶
Purpose: retrieve memory relevant to a normal user prompt before Claude handles it.
Operation:
- Preserve the submitted prompt unchanged.
- Use the prompt as the base retrieval query.
- Apply Membrick's existing query analysis and expansion through the normal
MemoryIndexquery path. - Route through the segmented
MemoryServicesnapshot. - Drop low-confidence, duplicate, or oversized results.
- Return bounded
additionalContextalongside the original prompt.
The hook does not rewrite the user's prompt. Prompt capture is disabled in the initial release.
UserPromptExpansion¶
Purpose: retrieve command-specific memory when a user-entered slash command, skill, or MCP prompt expands before reaching Claude.
Claude provides expansion_type, command_name, command_args, command_source, and the original prompt. The retrieval query combines the command name, arguments, and original prompt:
command_name is a strong routing signal and should be included in captured memory content or concepts so the current segment profiles can index it. It is also retained in filters for provenance. The current segmented query API does not apply filters during routing, so filter-based preference is a possible follow-up rather than an initial dependency. The expanded skill body must not be stored or repeatedly indexed because common skill instructions would dominate segment profiles.
This hook returns additionalContext and does not create a SourceDocument in the initial release.
PreCompact¶
Purpose: preserve resumable state before Claude compacts its context.
Operation:
- Read the complete transcript identified by the hook.
- Preserve each parsed message's speaker and full text in source order.
- Redact credentials before persistence.
- Create a deterministic
ClaudeCodeDocumentof typeCheckpoint. - Convert it to
SourceDocumentand upsert it throughMemoryService.
Repeated compaction for the same session uses deterministic checkpoint IDs or content hashes so retries do not create duplicate memory.
Stop¶
Purpose: capture the durable outcome after Claude finishes a response.
The adapter stores all parsed transcript text observations, including earlier decisions, corrections, tool text, and results, in source order. The complete snapshot becomes a ClaudeCodeDocument of type Outcome and is upserted into the current session segment. The stable per-session document identity replaces the prior snapshot on each capture, avoiding a duplicate copy for every turn.
The implementation must respect Claude's stop-hook recursion indicator and must not cause another stop cycle.
SessionEnd¶
Purpose: record final resumable session state and termination metadata.
The adapter creates a compact SessionSummary document containing the latest known outcome, unresolved work, and affected paths. It does not inject context. Session end does not make the segment immutable; resumed sessions may update it.
Retrieval Provenance¶
Each injected memory item includes its source, document type, capture timestamp, memory status, and relation evidence. Hooks do not run git or read the repository's .git directory, so captures carry no branch or revision metadata. Staleness comes from the capture timestamp and Membrick's semantic status (current, superseded, or conflicted), not from repository ancestry.
Retrieval emits at most one document per Claude session. When a session has multiple durable captures, the adapter prefers session-summary, then outcome, then checkpoint; the newest record breaks ties within a type. Session ranking still comes from MemoryService::search_with_filters.
The adapter does not inject complete stored records. It scores content lines using the query and the index's matched terms, keeps up to three matching lines, and limits each excerpt to 800 UTF-8 bytes. Records with no matching line use a small bounded leading excerpt.
SubagentStart¶
Purpose: retrieve memory relevant to a delegated subagent's task before it begins.
Operation:
- Use the subagent's prompt as the retrieval query.
- Route through the segmented store using the parent session's context.
- Return bounded
additionalContext.
The subagent runs in the same session segment as its parent. A parent/child segment split is deferred pending evidence that it improves retrieval precision.
This hook does not create a SourceDocument.
SubagentStop¶
Purpose: capture the durable outcome after a subagent finishes.
The adapter captures the subagent's completed work as an Outcome document in the current session segment. This follows the same pattern as the Stop hook. The subagent does not check the stop_hook_active recursion guard because SubagentStop is only delivered to the parent hook process, not re-entered.
PostToolUse¶
Purpose: retrieve memory relevant to a tool result, so Claude has past context about this tool or related paths before acting on the output.
Operation:
- Build a retrieval query from
tool_name,tool_input,tool_response,turn_id,agent_id, andagent_typefields. - Truncate large
tool_inputandtool_responsevalues toMAX_EXCERPT_BYTESbefore forming the query string. - Route through the segmented store.
- Return bounded
additionalContext.
This hook performs retrieval only. Capture on tool events is deferred because raw tool output is high-volume and requires stronger filtering before committing to the memory store.
PreToolUse¶
Purpose: retrieve memory relevant to a tool call before Claude executes it.
The adapter builds a query from the available tool name, input, turn, agent, and response metadata and returns bounded additionalContext. Unlike PostToolUse, this hook does not require a minimum query-term threshold because the pre-tool event is the earliest opportunity to provide context for the planned action.
This hook performs retrieval only. It does not approve, deny, or modify the tool call; tool policy remains Claude Code's responsibility.
Hooks Deferred From The First Release¶
PostToolUseFailure: useful after failure deduplication is defined. The failure case is currently not distinguishable without additional filtering.SubagentStart/SubagentStopparent/child segment split: subagents currently write into the parent session segment. A dedicated child segment and cross-segment retrieval policy is deferred pending benchmark evidence.PermissionRequest: a policy hook rather than a memory hook.- notification and configuration hooks: do not add durable memory value yet.
Hook Response¶
Retrieval hooks return structured Claude hook output:
{
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": "Relevant Membrick memory..."
}
}
UserPromptExpansion uses the same shape with its own hook event name. Capture hooks normally emit no context after a successful write.
The hook executable writes protocol JSON only to stdout. Diagnostics and errors go to stderr so they cannot corrupt Claude's hook response.
Context Selection¶
Retrieved memories are formatted under a configured byte or token budget. The formatter should:
- include source, timestamp, and affected paths when available
- preserve the highest-scoring distinct memories
- avoid repeating the same content from checkpoints and outcomes
- exclude results below a relevance threshold
- return no context when routing has no useful signal
Initial budgets and thresholds must be benchmarked rather than treated as API constants.
Failure Behavior¶
Retrieval hooks fail open by default. If the store is unavailable or a query fails, Claude receives no added context and continues processing the prompt.
Capture failures are logged to stderr and return success unless strict capture mode is explicitly enabled. A memory-layer failure must not prevent a user from ending or compacting a Claude session.
Security And Retention¶
Before creating SourceDocument, capture redacts credentials, tokens, and private keys. It retains the transcript's tool input, output, and image blocks so session claims can be checked against the recorded evidence.
Retention and deletion operate through stable doc_id values and MemoryService::remove(). A later retention policy can remove old session outcomes without changing the Claude protocol adapter.
Installation Shape¶
The installer will continue to preserve existing user configuration. It will:
- Install or update the
membrickMCP entry. - Merge the selected hook entries into Claude settings.
- Avoid replacing unrelated hooks or settings.
- Produce the same configuration on repeated installation.
Installed hook command shape:
membrick hook claude session-start /path/to/project
membrick hook claude user-prompt-submit /path/to/project
membrick hook claude user-prompt-expansion /path/to/project
membrick hook claude pre-tool-use /path/to/project
membrick hook claude post-tool-use /path/to/project
membrick hook claude pre-compact /path/to/project
membrick hook claude stop /path/to/project
membrick hook claude session-end /path/to/project
membrick hook claude subagent-start /path/to/project
membrick hook claude subagent-stop /path/to/project
Each command reads one Claude hook payload from stdin and writes one hook response to stdout.
Remaining Work¶
- Add benchmark coverage for retrieval quality, hook latency, and injected token usage, including the three new hooks added in this release.
- Replace deterministic transcript extraction with optional structured summarization after its quality and cost are measured.
- Add configurable context budgets, routing thresholds, and retention.
- Define cross-process locking and snapshot reload semantics before sharing the hook memory store with the long-running MCP server.
- Evaluate
PostToolUseFailurecapture after failure deduplication is defined. - Evaluate a parent/child segment split for subagents using benchmark evidence.
Acceptance Criteria¶
- existing MCP
searchandinfobehavior remains compatible - the default non-Claude
MemoryServicebehavior remains a single index - Claude memory documents are assigned stable session
group_idvalues - hook retrieval uses segmented
MemoryService::search_with_filters() - capture survives process restart
- repeated hook delivery does not duplicate memory
- irrelevant retrieval injects no context
- hook failures do not interrupt normal Claude use by default
- installation preserves unrelated Claude configuration
- tests cover every supported hook payload and response shape
PostToolUseretrieval uses tool name and truncated input/response as queryPreToolUseretrieval uses the planned tool call as querySubagentStartretrieval uses the subagent promptSubagentStopcapture is idempotent and stores anoutcomedocument type