Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/coleam00/claude-memory-compiler/llms.txt

Use this file to discover all available pages before exploring further.

The hook system is the automatic capture layer of the Claude Memory Compiler. Three hooks fire at key moments in the Claude Code session lifecycle: SessionStart injects accumulated knowledge into every new conversation, SessionEnd extracts the transcript when a session closes, and PreCompact acts as a safety net before Claude Code discards context during auto-compaction. None of the hooks make API calls directly — they are designed to complete in under ten seconds and hand off heavy work to the background flush.py process.

Configuration

Hooks are registered in .claude/settings.json at the project root. Claude Code reads this file automatically when you open the project.
{
  "hooks": {
    "SessionStart": [{ "matcher": "", "hooks": [{ "type": "command", "command": "uv run python hooks/session-start.py", "timeout": 15 }] }],
    "PreCompact": [{ "matcher": "", "hooks": [{ "type": "command", "command": "uv run python hooks/pre-compact.py", "timeout": 10 }] }],
    "SessionEnd": [{ "matcher": "", "hooks": [{ "type": "command", "command": "uv run python hooks/session-end.py", "timeout": 10 }] }]
  }
}
An empty matcher string causes each hook to fire on every matching event, regardless of the prompt or tool involved. Commands use relative paths from the project root.

SessionStart — hooks/session-start.py

SessionStart fires when Claude Code opens a new session. It is the only hook that produces output Claude reads directly; the other two hooks exist purely to capture data.

Trigger

Claude Code emits a SessionStart event at the beginning of every new conversation in the project.

I/O contract

Stdin: none — session-start.py does not read from stdin. Stdout: a JSON object in the exact format Claude Code expects for context injection:
{
  "hookSpecificOutput": {
    "hookEventName": "SessionStart",
    "additionalContext": "..."
  }
}
The additionalContext string is prepended to Claude’s context window, so Claude “remembers” the knowledge base index and recent activity before the user types anything.

What it reads

The hook assembles its context from two sources:
  1. knowledge/index.md — the master catalog of every compiled article. This is the primary retrieval mechanism: Claude reads it to decide which articles are relevant to the current task.
  2. The most recent daily log — today’s daily/YYYY-MM-DD.md if it exists, otherwise yesterday’s. Only the last MAX_LOG_LINES (30) lines are included to keep the payload small.
The assembled context also includes the current date.

Limits and truncation

MAX_CONTEXT_CHARS = 20_000  # hooks/session-start.py
MAX_LOG_LINES = 30           # hooks/session-start.py
If the assembled context string exceeds 20,000 characters, it is hard-truncated at that boundary with a ...(truncated) suffix appended.

Performance

No API calls are made. The hook is pure local file I/O and completes well under one second. The 15-second timeout in settings.json is a generous safety margin.

SessionEnd — hooks/session-end.py

SessionEnd fires when the user closes or exits a Claude Code session. It extracts the conversation transcript and hands it to flush.py for background processing.

Trigger

Claude Code emits a SessionEnd event when a session terminates normally.

Recursion guard

At the top of the script, before any other logic:
if os.environ.get("CLAUDE_INVOKED_BY"):
    sys.exit(0)
When flush.py calls the Claude Agent SDK, the SDK may spin up its own Claude Code subprocess. That subprocess would fire session-end.py again, creating an infinite loop. The guard breaks it: flush.py sets CLAUDE_INVOKED_BY=memory_flush in its environment before it starts, so any hook fired by its subprocess exits immediately.

I/O contract

Stdin: a JSON object from Claude Code:
{
  "session_id": "abc123",
  "transcript_path": "/path/to/conversation.jsonl",
  "cwd": "/path/to/project"
}
Stdout: nothing — the hook produces no output. All logging goes to scripts/flush.log.

What it does

1

Parse stdin

Reads the JSON payload from stdin. Handles Windows path escaping quirks (unescaped backslashes) with a regex fix before parsing.
2

Extract context

Reads the JSONL transcript file, extracts up to 30 message turns, and serializes them as a plain-markdown string (at most 15,000 characters, trimmed to a clean turn boundary).
3

Write temp file

Writes the extracted context to a temp file in scripts/. session-end.py names it session-flush-<session_id>-<timestamp>.md; pre-compact.py names it flush-context-<session_id>-<timestamp>.md. The background process reads this file; writing it in the hook avoids passing large data through process arguments.
4

Spawn flush.py

Launches flush.py as a background subprocess with stdout=DEVNULL and stderr=DEVNULL. On Windows, CREATE_NO_WINDOW is set to prevent a console flash. On Mac/Linux, no creation flag is set — flush.py handles its own process group detachment when it later spawns compile.py.

Skip conditions

The hook exits without spawning flush.py when:
  • transcript_path is absent or empty in the stdin payload
  • The transcript file does not exist on disk
  • The extracted context is empty after parsing
  • Fewer than 1 turn is present in the transcript

PreCompact — hooks/pre-compact.py

PreCompact fires immediately before Claude Code auto-compacts the context window. Its architecture is identical to SessionEnd — the same recursion guard, the same JSONL parsing, the same temp-file-then-spawn pattern.

Trigger

Claude Code emits a PreCompact event when the context window approaches its limit and Claude Code is about to summarize and discard accumulated context.

Why PreCompact exists alongside SessionEnd

Long sessions may trigger multiple auto-compactions before the user ever closes the session. Each compaction discards the raw message history for the compacted portion. By the time SessionEnd fires at the end of a long session, the transcript may contain only the final summary rather than the full detail. PreCompact captures the raw context before each compaction, ensuring nothing is lost. The 60-second deduplication window in flush.py prevents multiple flushes from the same burst.

Bug guard — empty transcript path

# transcript_path can be empty (known Claude Code bug #13668)
if not transcript_path_str or not isinstance(transcript_path_str, str):
    logging.info("SKIP: no transcript path")
    return
Claude Code has a known bug (issue #13668) where transcript_path is sometimes an empty string in the PreCompact payload. The hook checks for this and exits cleanly rather than crashing.

Difference from SessionEnd

The only behavioral difference is the minimum-turn threshold:
HookMIN_TURNS_TO_FLUSH
session-end.py1
pre-compact.py5
PreCompact requires at least 5 turns before flushing. Compactions can fire early in a session with very little content; the higher threshold avoids flushing nearly-empty transcripts.

JSONL transcript format

Claude Code stores conversations as newline-delimited JSON (.jsonl) files. Each line is one event. Messages are nested under a message key:
entry = json.loads(line)
msg = entry.get("message", {})
role = msg.get("role", "")      # "user" or "assistant"
content = msg.get("content", "") # string or list of content blocks
Content may be a plain string or a list of typed blocks. The hooks handle both:
if isinstance(content, list):
    text_parts = []
    for block in content:
        if isinstance(block, dict) and block.get("type") == "text":
            text_parts.append(block.get("text", ""))
        elif isinstance(block, str):
            text_parts.append(block)
    content = "\n".join(text_parts)
Only user and assistant roles are extracted. Tool calls, system messages, and other event types are silently ignored.

Hook summary

HookFileTimeoutMakes API callsProduces output
SessionStarthooks/session-start.py15 sNoYes (JSON to stdout)
SessionEndhooks/session-end.py10 sNoNo
PreCompacthooks/pre-compact.py10 sNoNo

Build docs developers (and LLMs) love