Source confidence: medium

primer.md — The Living Handoff Document Pattern

In short: Implement the primer.md living-handoff pattern (credited to @keshavsuki, lineage from @ykdojo's HANDOFF.md) in a real project so every Claude Code session starts from an accurate state snapshot and ends by rewriting it, without the human remembering.

Key fact Detail
What it covers agents
Tools Claude, Claude Code
Posts covered 1 post (1 S-tier) · top: primer.md + memory.sh — live-updating…
Creators @keshavsuki
Sources dated Mar 2026
Prompt system Run-it-yourself prompts
Last verified 2026-10-22

Attributed to: @keshavsuki (on X/Twitter), though the broader "living handoff document" lineage predates the specific primer.md naming — Yusuke Kamiyamane (@ykdojo) popularized a similar HANDOFF.md pattern, and various SESSION.md / STATE.md / CONTEXT.md variants circulate in the awesome-claude-code ecosystem. The primer.md naming and the specific "Claude rewrites it at session end" mechanic are the contribution being catalogued here.

Research note: The specific @keshavsuki post was not directly recoverable through web search at the time of writing. The pattern described below is reconstructed from the broader family of handoff-document patterns it belongs to, cross-referenced with the Claude Code hook system that makes the "auto-rewrite on session end" mechanic possible. Treat specific code snippets below as reference implementations, not verbatim copies of keshavsuki's original.

What it is

primer.md is a sibling file to CLAUDE.md that serves a fundamentally different purpose:

The core mechanic: at the end of every Claude Code session, Claude rewrites primer.md with a snapshot of where the project currently stands and what the next session should do first. At the start of every session, Claude reads primer.md (via a CLAUDE.md pointer or via the additionalDirectories hook) and resumes exactly where the previous session left off — no "what were we doing again" dance, no re-reading five files to rebuild context.

It is a living handoff document between Claude and its future self.

Why this pattern exists

Claude Code has no memory across sessions. Every new session starts cold. The options for rebuilding context are:

  1. Re-explain to Claude manually — slow, lossy, requires you to remember.
  2. Rely on CLAUDE.md — it holds permanent rules but not current state ("we just finished phase 2, phase 3 is blocked on the Redis migration").
  3. Use /resume — works within a single conversation thread but not across branch-switches, compactions, or new terminals.
  4. Ask Claude to write a handoff document before starting fresh — the @ykdojo pattern. Works, but requires you to remember to ask.
  5. Make the handoff document self-maintaining — the primer.md pattern. The hook handles it automatically.

primer.md is option 5, and its value is that it removes the human from the loop. You never have to remember to update it.

How it works under the hood

The pattern has three parts:

1. The file itself — structured session state

# Project Primer
_Last updated: 2026-04-08 14:32 UTC (session ended)_

## Current status
Phase 3 of the trading-agent-team integration. Arena wire-up is COMPLETE
and green. Legion port is IN PROGRESS — we're 60% through migrating the
Command tab from /finance into /unicron/command.

## What changed this session
- Ported ARSENAL tab component + its three sub-widgets (bot cards, metric
  rows, status dots). Tests green.
- Discovered: the old Legion Command chart code depends on Plotly 5.x;
  /unicron is on Plotly 6.x. Needs shim or upgrade.
- Added your CSS vars to `src/styles/legion-theme.css`.

## Next up (start here)
1. Resolve the Plotly version mismatch — decide: shim or upgrade all of unicron.
   Relevant files: `src/charts/LegionChart.tsx`, `package.json` (line 42).
2. Finish migrating RANKINGS tab (parallel structure to ARSENAL — should
   be ~30 min if Plotly is resolved).
3. Run full visual-regression suite before declaring Phase 3 done.

## Blockers
- Plotly version decision (above). Needs your input if we go the
  upgrade route since it touches other tabs.

## Open questions
- Does the old `session_state` singleton in `legion_command.py` need to
  survive the port, or can we replace it with Zustand like the rest of
  unicron?

## Don't forget
- The `.env` in `/unicron` has a placeholder HL_PRIVATE_KEY — you
  need to set this before running live.
- Arena's WebSocket reconnect logic is the reference implementation for
  any new real-time wiring.

Key sections:

2. The CLAUDE.md anchor — tell Claude the file exists

Add this to your project CLAUDE.md:

## Session continuity
- At the **start** of every session: read `primer.md` first. It contains
  the current project state and next steps from the previous session.
- Follow the "Next up" list unless the user redirects.
- At the **end** of every session: rewrite `primer.md` from scratch.
  Do not append — the file is a snapshot, not a log.
- Structure: Current status / What changed / Next up / Blockers / Open
  questions / Don't forget.
- Keep it under 150 lines. If it's growing, something is wrong — the
  project has either stalled or needs a real retro.

3. The hook — automate the rewrite

Claude Code hooks (configured in .claude/settings.json) can run arbitrary commands on lifecycle events. The relevant one here is the Stop hook — fires when a session ends (user exits, or agent finishes its final turn).

{
  "hooks": {
    "Stop": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "claude -p 'Rewrite primer.md to reflect the current project state. Use the structure defined in CLAUDE.md under Session continuity. Do not append — replace the file entirely. Be specific about what the next session should do first.' --allowedTools Read,Write,Glob,Grep"
          }
        ]
      }
    ]
  }
}

How this works:

Alternative: PreCompact hook. If your sessions are long and you rely on compaction rather than explicit exits, use the PreCompact hook instead. Same logic, fires before Claude Code compresses context rather than when the session terminates.

Alternative: manual command. If you don't want automation, add a slash command:

# .claude/commands/prime.md
Rewrite primer.md with the current project state per the structure in
CLAUDE.md. Then summarize what you wrote in 2 sentences.

Now /prime at the end of any session does the same thing without a hook.

Key mechanics

What makes primer.md different from HANDOFF.md / SESSION.md / a changelog:

  1. Snapshot, not log. It is rewritten, not appended to. This is critical — a log grows unboundedly and loses the "what matters now" signal. A snapshot stays focused.
  2. Self-maintaining via hook. The human never types it. This is what keshavsuki's contribution adds over the manual handoff pattern.
  3. Action-oriented "Next up." The first line of the first next-up item should be immediately executable. No "continue working on the thing" — specific file, specific change.
  4. Paired with CLAUDE.md, not replacing it. CLAUDE.md is permanent rules; primer.md is state. Don't conflate them.

Quick start

# 1. Create the file with a starter template
cd my-project
cat > primer.md <<'EOF'
# Project Primer
_Last updated: [session init]_

## Current status
Project just bootstrapped. No work yet.

## Next up
1. Define what we're actually building.
EOF

# 2. Add the CLAUDE.md anchor
# (paste the "Session continuity" block above into CLAUDE.md)

# 3. Configure the Stop hook
# Edit .claude/settings.json and paste the hook JSON above

# 4. Start a session, do some work, exit
claude
> (work happens)
> exit

# 5. Verify primer.md was rewritten
cat primer.md

If the hook didn't fire, check claude --debug output and .claude/settings.json syntax — hooks silently skip on JSON errors.

Cross-reference: how this complements your MEMORY.md system

you already run a related but distinct pattern at ~/.claude/projects/<project>/memory/MEMORY.md — an index of per-topic memory files covering feedback, projects, and references. It is auto-updated across conversations and provides a global knowledge substrate.

The overlap:

The differences:

Dimension your MEMORY.md primer.md
Scope Global across all projects Single project
Shape Index pointing to many sub-files Single self-contained snapshot
Lifetime Permanent — entries accumulate over months Ephemeral — rewritten every session
Granularity Learnings, project summaries, reference docs Active state + next steps
Update trigger forge / retro / manual capture Stop hook at session end
What it answers "What do I know about you and his projects?" "Where did we stop and what's next?"

They are complementary, not redundant. MEMORY.md says "project_unicron.md: Arena fully wired end-to-end, Legion port pending" — the high-altitude state. A primer.md inside ~/projects/unicron/ would add "Legion port is 60% done, blocked on Plotly version mismatch, next file to touch is src/charts/LegionChart.tsx line 42."

Recommendation for you: Adopt primer.md per-project for active builds (unicron, trading-agent-team, ai-catalogs). Keep MEMORY.md as the global flywheel it already is. The primer.md hook can even update MEMORY.md at project milestones — fold "session ended" into "if this was a significant milestone, also update the relevant project_*.md in MEMORY.md."

Gotchas

  1. Rewrite, don't append. The moment you start appending, primer.md becomes a changelog and loses its "next session starts here" focus. Enforce this in the CLAUDE.md anchor.
  2. The Stop hook runs a second Claude call. That's extra tokens and a few seconds of latency on exit. Worth it for important projects; overkill for throwaway scripts.
  3. Hook failures are silent by default. Test the hook with claude --debug the first time you set it up. JSON syntax errors in .claude/settings.json kill hooks without warning.
  4. Don't put secrets in primer.md. It's meant to be committable. If you need to note "set HL_PRIVATE_KEY before running," reference the variable name, never the value.
  5. Git commit it. primer.md should be in version control so team members (or your future self on a different machine) can pick up state too.
  6. Size discipline. Target ~100 lines. If it balloons past 200, the project has either stalled (too many open questions accumulating) or needs a real retrospective — not a longer primer.
  7. Don't read primer.md in CLAUDE.md twice. If you load primer.md via additionalDirectories AND reference it in CLAUDE.md, Claude sees it twice. Pick one mechanism.

What we could build with it

Sources


Prompt system — run this yourself

Everything below is pasteable into Claude Code, Claude, or any agent. Use the one-shot when you want the whole result in one run; use the step prompts when you want to steer. Every step carries its own audit, and the gap audit at the end makes the agent find and fill what it missed.

Mission. Implement the primer.md living-handoff pattern (credited to @keshavsuki, lineage from @ykdojo's HANDOFF.md) in a real project so every Claude Code session starts from an accurate state snapshot and ends by rewriting it, without the human remembering. Running this produces a verified hook setup, a primer linter, a SessionStart injection, and a resume trial against a no-primer control. Done means hooks fire on the right events without recursion, primer.md stays under 150 lines with all six sections, and fresh sessions reach the first correct action faster than the control.

1 · Deeper, not bigger

Before running anything, make the task deeper rather than larger. Ask: what would a top professional do here that I am not asking for; what is the rawest material; which methods and tools; what, other than the model or me, proves it right. Then ask for exactly that.

Rewritten prompt — ask for exactly that:

Set up the primer.md handoff pattern in my project, but verify it against the current Claude Code hook docs first instead of copying the guide.

1. Read the hooks reference for my installed version. Confirm when Stop, SessionEnd, PreCompact, and SessionStart fire, what JSON arrives on stdin (transcript_path, session_id, cwd), and how SessionStart can inject additionalContext. Tell me whether the guide's Stop hook running claude -p would fire every turn and whether the child process would trigger hooks recursively, and whether a CLAUDE_TRANSCRIPT_PATH env var exists.
2. Write a primer template with Current status, What changed, Next up, Blockers, Open questions, Don't forget, and a linter: all six headings, under 150 lines, Next up item 1 names a file or command, no secret-shaped strings.
3. Implement rewrite hooks on SessionEnd and PreCompact with a recursion guard, a timeout, background execution, the rewriter limited to Read, Write, Glob, Grep, and the transcript path taken from stdin.
4. Add a SessionStart hook that injects primer.md so reading it does not depend on CLAUDE.md compliance.
5. Commit primer.md after each rewrite.
6. Run a resume trial: 5 fresh sessions with primer versus 5 without, timing to the first correct action.

Return: the doc findings table, the config, the linter, debug logs proving hook counts, and trial results.

2 · One-shot prompt

ROLE: You are a Claude Code harness engineer. You verify hook behavior against the installed version's documentation and debug logs, not against blog posts. You are implementing the primer.md living-handoff pattern for Victor, who already keeps primer.md files by hand per his global CLAUDE.md (six sections, under 150 lines, rewritten as a snapshot at session end).

CONTEXT:
- Pattern: primer.md is live project state, sibling to CLAUDE.md (permanent rules). Rewritten from scratch at session end, read at session start. Sections: Current status, What changed, Next up, Blockers, Open questions, Don't forget.
- Guide config to test, not copy: a Stop hook running claude -p 'Rewrite primer.md ...' --allowedTools Read,Write,Glob,Grep; the claim that the subprocess reads context via a CLAUDE_TRANSCRIPT_PATH env var; PreCompact as an alternative; a /prime slash command at .claude/commands/prime.md as the manual fallback; the claim that hooks silently skip on JSON errors.
- Known risks to check: Stop fires at the end of every main-agent response, not only at session end; a child claude -p inherits project hooks and can trigger its own Stop hook; hook input arrives as JSON on stdin.

HARD CONSTRAINTS:
- Work in a scratch test project first; touch Victor's real projects only after the trial passes, and only the settings file he names.
- The rewriter may only use Read, Write, Glob, Grep. It must never write secrets into primer.md; the linter enforces a secret regex (private keys, sk-ant, HL_PRIVATE_KEY=, 0x followed by 64 hex chars).
- A recursion guard is mandatory: the hook exits 0 immediately when PRIMER_REWRITE=1 is set, and sets it for the child.
- Hooks must time out (60-120 s) and must not block session exit for the user.
- primer.md is a snapshot: rewritten whole, under 150 lines.

DELIVERABLES, in order:
1. hook_facts.md: for the installed version (claude --version), when each relevant event fires, the stdin JSON fields, exit-code behavior, SessionStart additionalContext, and verdicts on each guide claim, with doc URLs.
2. templates/primer.md and lint_primer.py (six headings, line cap, Next up item 1 contains a path or command, secret regex, Last updated stamp).
3. hooks/primer_rewrite.sh and the settings.json block wiring it to SessionEnd and PreCompact with the guard, timeout, background run, and transcript_path from stdin.
4. hooks/primer_inject.sh on SessionStart emitting primer.md as additional context.
5. Auto-commit of primer.md after a successful lint, with a message naming the session id.
6. trial.md: 5 sessions with primer versus 5 without on the same project state; minutes and clarifying questions until the first correct action; scored by a reviewer who did not write the primer.

GAP ANALYSIS (built in): keep gaps.md. Record every behavior you could not observe directly (SessionEnd on terminal kill, PreCompact on auto-compaction) and fill by forcing the event (/compact, exit, kill -TERM) with --debug on. State the rest with the next action.

SELF-QUESTIONS before finishing:
- Does my rewrite fire once per session end and once per compaction, never per turn?
- Does the child claude -p trigger any hook, and does the guard stop it?
- Can the rewriter actually read the transcript at transcript_path, and is it large enough to need a summary step?
- What happens when the session is killed rather than exited?
- Does SessionStart injection duplicate the CLAUDE.md read instruction or replace it cleanly?
- Is Next up item 1 executable by a fresh session with zero other context?
- Did any rewrite drop a still-open blocker from the previous primer?
- What does each rewrite cost in tokens and time?

STOP CONDITIONS: stop if the rewrite fires per turn or recurses (fix before any trial); stop before editing any of Victor's real project settings; stop if the linter finds a secret in any rewrite.

RETURN FORMAT: markdown report: Hook facts table, Config (full JSON), Scripts (paths), Debug evidence (hook fire counts), Linter results across 10 rewrites, Trial table, Gaps ranked, file paths.

3 · Step-by-step prompts

Step 1 — Ground truth: hook semantics for this version

Goal: Replace the guide's hook assumptions with documented and observed behavior.

Run claude --version and record it. Read the Claude Code hooks reference and hooks guide on docs.claude.com. For each event (SessionStart, Stop, SubagentStop, PreCompact, SessionEnd) record: when it fires, matcher support, stdin JSON fields (look for session_id, transcript_path, cwd, hook_event_name, reason, trigger), exit-code meanings, timeout default, and output options (additionalContext for SessionStart).

Then observe: in a scratch project, register a logging hook for all five events that appends the stdin JSON plus env | grep -i claude output to hooks.log in the scratch project. Run a session with three turns, run /compact, then exit. Count events.

Write hook_facts.md with a verdict per guide claim: Stop fires at session end (true or false, observed count), CLAUDE_TRANSCRIPT_PATH env var exists, hooks silently skip on invalid JSON, a child claude -p inherits project hooks. Cite doc URLs and log lines.

Expect: hook_facts.md with an event table and five claim verdicts; hooks.log showing event counts for a 3-turn session plus compaction plus exit.

Audit before moving on: Event counts in hooks.log must match your table; if Stop appears 3 times for 3 turns, the guide's config is per-turn and must not be used. Any verdict without a doc URL or log line is unverified. Delete the logging hooks after the step.

Step 2 — Template and linter

Goal: Define what a valid primer is and enforce it mechanically.

Write templates/primer.md with the six sections in order: Current status (one paragraph), What changed (bullets), Next up (numbered, item 1 immediately executable with a file path or command), Blockers, Open questions, Don't forget; plus a Last updated line with UTC timestamp and session id.

Write lint_primer.py taking a path, exiting non-zero with messages on: missing or misordered headings; over 150 lines; Current status over 120 words; Next up item 1 lacking a path-like token or backticked command; secret-shaped strings (BEGIN PRIVATE KEY, sk-ant-, HL_PRIVATE_KEY=, 0x plus 64 hex, AKIA plus 16 uppercase alphanumerics); a Last updated stamp older than the file's mtime by more than a day.

Write 8 fixture primers (2 valid, 6 each breaking one rule) and a test runner proving each fixture produces the expected result. Run the linter against Victor's existing primer.md in this repo, read-only, and report its findings without editing it.

Expect: templates/primer.md, lint_primer.py, fixtures/ with 8 files, test output showing 8/8 expected results, a findings list for the existing primer.md.

Audit before moving on: All 8 fixtures must behave as expected; a valid fixture failing means the rules are too strict. Confirm the secret regex does not print the matched value, only the line number.

Step 3 — Rewrite hook with guard

Goal: Rewrite primer.md exactly once per session end and per compaction, safely.

Write hooks/primer_rewrite.sh: read stdin JSON with jq; exit 0 immediately if PRIMER_REWRITE=1; extract transcript_path, session_id, cwd, hook_event_name; copy the current primer.md to primer.prev.md; launch in the background with nohup and timeout 120: PRIMER_REWRITE=1 claude -p with a prompt that reads primer.prev.md, the transcript at transcript_path (tail the last 400 lines if larger than 2 MB), and git log --oneline -20 output saved to a temp file, then rewrites primer.md whole per the template, keeping unresolved blockers from the previous version, with --allowedTools Read,Write,Glob,Grep. After the child exits, run lint_primer.py; on failure restore primer.prev.md and write the lint errors to primer.lint.log.

Wire it in .claude/settings.json of the scratch project for SessionEnd and PreCompact only. Validate the JSON with jq before saving. Run three sessions with --debug, including one with /compact and one killed with kill -TERM.

Expect: hooks/primer_rewrite.sh, settings.json block, three debug logs, primer.md versions after each event, lint results.

Audit before moving on: Count rewrites per debug log: exactly one per exit and one per compaction, zero per turn, zero from the child. If the killed session produced no rewrite, record it as a gap and document the manual /prime command at .claude/commands/prime.md as the fallback. Fail if a rewrite dropped an unresolved blocker; tighten the prompt.

Step 4 — SessionStart injection and commit

Goal: Make the next session see the primer without relying on instruction-following.

Write hooks/primer_inject.sh for SessionStart: if primer.md exists in cwd, output the documented JSON with additionalContext containing the file content prefixed by a one-line instruction to start from Next up item 1 unless the user redirects; cap at 150 lines; exit 0 silently if absent. Register it in the scratch settings.json.

Add to primer_rewrite.sh: after a passing lint, git add primer.md and commit with message primer: session <session_id> <event>, only if the working tree has no other staged changes (never sweep other files into the commit). Start a fresh session and ask: what is the first thing to do? Record the answer and whether it matches Next up item 1.

Decide whether the CLAUDE.md Session continuity block stays (as a fallback) or is shortened, and justify the choice in one paragraph.

Expect: hooks/primer_inject.sh, updated settings.json, a commit history with primer-only commits, a transcript excerpt where the new session names Next up item 1.

Audit before moving on: Run git show --stat on each primer commit: any file other than primer.md is a fail; fix the staging logic. If the new session does not reference the primer content, check the hook output format against hook_facts.md.

Step 5 — Resume trial against a control

Goal: Measure whether primer.md actually shortens cold starts.

Prepare a project state with real mid-task work: a half-finished feature, one blocker, one gotcha outside the code (an env var that must be set). Snapshot the repo with git stash or a branch so every trial starts identical.

Run 5 fresh sessions with the primer hooks enabled and 5 with primer.md removed and hooks disabled (control). In each, the first user message is only: continue. Record: minutes until the first correct action (an edit or command that advances Next up item 1), number of clarifying questions, files read before acting, whether the gotcha was respected, and tokens used. A reviewer who did not write the primer scores correct action from transcripts, blind to arm where possible.

Write trial.md with a per-session table and medians per arm.

Expect: trial.md with 10 rows and arm medians; transcripts saved; reviewer name or id noted.

Audit before moving on: Confirm both arms started from the identical commit. If the primer arm is not faster on median time and questions, the step does not fail silently: record it and inspect whether Next up item 1 was actually executable. Note that 5 per arm is a small sample and report ranges, not just medians.

Step 6 — Validate against oracles, gap report

Goal: Prove the setup works and list what remains.

Run the oracles and record pass or fail with evidence: (1) debug logs show one rewrite per exit and per compaction, zero per turn, zero recursive; (2) lint_primer.py passes on 10 consecutive rewrites; (3) primer git history matches code history (each What changed list is consistent with that session's commits); (4) the resume trial shows the primer arm faster on median with fewer clarifying questions, scored by a reviewer who did not write it; (5) no secret pattern in any primer version (run the linter over git log -p primer.md).

Write gap_report.md: every guide claim (Stop hook at session end, CLAUDE_TRANSCRIPT_PATH, additionalDirectories reading, hooks skipping on JSON errors, /prime fallback) and every deliverable, marked verified / unverified / false with evidence, ranked by impact. Fix the top three. Then write rollout.md: the exact settings block and files to copy into a real project, for Victor to approve.

Expect: oracles.md with five rows, gap_report.md ranked, rollout.md ready for approval.

Audit before moving on: Any oracle without a log or file reference is unverified. Do not apply rollout.md to any real project in this step; it waits for Victor.

4 · Gap analysis and audit

Run this after the build. It makes the agent enumerate its own claims, mark what is verified, rank the gaps and fix the top ones.

You are reviewing a primer.md hook implementation you did not build. Read hook_facts.md, lint_primer.py, the hook scripts, settings.json, debug logs, primer git history, trial.md, oracles.md, and gap_report.md.

1. Enumerate every claim: event semantics, rewrite counts, recursion guard, transcript access, injection working, lint pass rate, trial results, guide-claim verdicts. Mark each verified, unverified, or false. Re-run one session with --debug and count hook firings yourself.
2. List missing pieces: killed-session behavior, very large transcripts, concurrent sessions in the same repo racing to rewrite, primer commits sweeping other files, blockers silently dropped across rewrites, token cost per rewrite.
3. Rank by impact: anything that loses state or leaks secrets first, then wrong-event firing, then trial weakness.
4. Fix the top three now and show evidence.
5. Report what you could not fix and why, with the exact next action.
Return a claim table (claim, status, evidence, fix) and the ranked gap list.

Questions the agent must ask itself before it says done:

5 · What proves it right

6 · Failure modes

7 · Agent direction (unattended runs)

Do everything in a scratch project first; Victor's real projects get only an approved rollout.md. Reach first for the hooks docs and claude --debug; observed fire counts beat any blog post. Cache hook_facts.md and refer to it rather than re-reading docs. A sub-agent can write the linter and fixtures in parallel with hook research. Never git add -A; stage primer.md by path only. Never print secrets; the linter reports line numbers, not matches. Stop and ask Victor before editing any real project's settings.json or CLAUDE.md, and if hooks fire per turn or recurse after your fix. Write outputs to docs/primer_pattern/: hook_facts.md, lint_primer.py, fixtures/, hooks/, trial.md, oracles.md, gap_report.md, rollout.md. Return the oracle table, the gap list, and the rollout path.

Prompt system updated 2026-10-22 · all prompt systems · JSON