Coming back to a project after a rest, be it the next morning or after a long-awaited vacation, normally leaves me a little fuzzy on the particulars. Re-reading the previous sessions is tiresome to the point of reaching back to my suitcases, and a generated recap misses too many details and much of the context, mostly found wanting.
handoff is the skill I built to close that gap. It writes a state snapshot detailing reasons for chosen options, which parts are left uncertain, and what the next steps are. I no longer need to re-work and re-derive past decisions from a multiverse of threads.
- Captures state, decisions, proposals, rejections, open questions, unverified claims, and recurring corrections, beyond just the conversation.
- Separates what was decided from what was only proposed, and from what was considered and dropped.
- Ends every snapshot with exactly one executable next action.
In this post
- 01What a recap leaves out
- 02A handoff is an interface
- 03Inside the folder
- 04How it connects to the rest of the ecosystem
- 05Worked example
- 06Design decisions
- 07Best practices
- 08Limits
- 09Next
What a recap leaves out
A narrative summary reads well as the heroic chronicles of sessions past, yet without the track record of ground gains, it fails to give traction for the next foothold.
To combat that, every snapshot handoff writes uses labelled lines instead of paragraphs, one fact per line, fields separated the same way every time, noun phrases instead of full sentences. A sentence appears only where a decision turned on the user's exact wording, and there it's quoted directly, word for word.
| Typical recap | Structured handoff |
|---|---|
| Narrative summary | Resume-ready state record |
| Blends facts and assumptions | Labels evidence and uncertainty |
| Lists activity | Separates decisions from what was only proposed |
| Ends with vague next steps | Names one exact next action |
| Requires re-reading context | Enables targeted resumption |
flowchart TD
A[Work session] --> B[Capture decisions and evidence]
B --> C[Record current state and open questions]
C --> D[Write explicit next actions]
D --> E[Future session resumes from the handoff]
What's not obvious: every session ends at step E whether the work is finished or not. Skip straight from B to D and a handoff turns into a to-do list with no reasoning behind it.
A handoff is an interface
I think of it as a stable API, or a well-kept experiment log. Both stand in for a contract, listing what is known, what changed, what has to happen next, and what shouldn't be assumed.
That framing also rules out a plain prose recap, which is optimised for a single read by someone who was already in the room, whereas a handoff doesn't rely on existing context. In the same sense that an API that fails to document changes in its guarantees is useless no matter how well it works internally, so is a handoff that drops the changes from the version before it.
A snapshot that does this job opens with a one-line objective, then covers eight labelled sections, each holding a specific kind of fact so it never gets confused with another:
- STATE: the paths, installs, and running processes in play, each path carrying a computed line and byte count against the real file, never estimated.
- DECIDED: what the user explicitly committed to. No reasoning attached, by design.
- PROPOSED: recommendations on the table, still awaiting acceptance.
- REJECTED: options considered and dropped, with why.
- OPEN: questions ranked by what they block; when they came up doesn't factor in.
- UNVERIFIED: claims carried forward that were never independently rechecked.
- PATTERN: corrections that recurred more than once this session.
- FIRST: one line, the exact next action, executable without a clarifying question.
Where PATTERN comes from
The protocol reads the project's critique log, when one exists, for any defect appearing in two or more runs. The path is fixed at ${CLAUDE_PROJECT_DIR}/.claude/critique-log/*.jsonl. Where a retrospective journal exists, it's read too, for the same slug, as a second recurrence source, since it captures friction a critique log never records. The protocol doesn't fix a path for the journal, since that pairing is optional and project-specific, and no fixed location is guaranteed for it. Absent both sources, PATTERN draws on the session alone.
graph TD
CL["critique log<br/>.claude/critique-log/*.jsonl"] -->|"defect in 2+ runs"| PT["handoff's PATTERN section"]
RJ["retrospective journal<br/>path not fixed by the protocol"] -->|"friction, same slug"| PT
PT --> SS["This session's snapshot"]
What's not obvious: a defect only earns a PATTERN line once it's shown up twice. One occurrence is just a mistake; two, whether both sit in the review log or one sits there and one in the retro journal, is worth writing down.
DECIDED strips out reasoning by design. Separating the choice from the justification is what stops a well-argued suggestion from being misread as a commitment later.
State deserves a closer look, since it's the field most likely to have gone stale by the time anyone reads it. handoff records a line count and byte count alongside each path it names, so a resuming reader can tell in one glance whether the file has changed underneath the snapshot before acting on anything it says about that file.
Open questions get ranked by what they block, and when they came up doesn't factor in. A question raised in the first five minutes that now blocks nothing belongs below one raised in the last five minutes that blocks everything the next session needs to do.
The line doing the most work here separates DECIDED from PROPOSED. Recording that someone chose an option when they merely reacted well to it means the next session acts on a decision that was never actually made. The safer failure mode is under-claiming. Anything with a genuinely unclear origin gets logged as a proposal, marked origin-uncertain, and flagged as such. Rounding it up to a decision because that reads more confidently is the exact mistake this field exists to prevent.
Here's what actually happens, in order, each time a session invokes it:
flowchart TD
A[Session reaches a stopping point] --> B[Check for a prior handoff to supersede]
B --> C[Read the critique log and retrospective journal for recurring defects]
C --> D[Assemble the eight fields, decisions kept separate from proposals]
D --> E[Apply the hard cap on each field]
E --> F[Write the snapshot]
F --> G[Trigger a review pass against the snapshot itself]
G --> H[Snapshot ready for the next session]
What's not obvious: step G isn't conditional on whether the session looks unfinished. The review runs every time, checking the snapshot itself, separate from the work it describes. It asks whether the handoff is honest about what happened. Whether the code or the writing behind it is any good is a different question entirely.
Inside the folder
handoff/
├── SKILL.md
├── README.md
├── example-handoff.md
├── CLAUDE_md-snippet.md
└── tools/
├── session_watch.py
├── measure_savings.py
├── test_session_watch.py
└── test_measure_savings.py
Each file has one job:
SKILL.md: the protocol itself, always loaded. The eight-field schema, the DECIDED-versus-PROPOSED rule, where PATTERN draws from, and the hand-off to a review skill on close.README.md: the rationale behind the schema, plus both tools' command-line usage.example-handoff.md: a sanitised worked example, checked against the same linter as any real snapshot.CLAUDE_md-snippet.md: the paste-in text for~/.claude/CLAUDE.md, the hook registration JSON for macOS/Linux and Windows, the tuning variables, and a verification command.tools/session_watch.py: theUserPromptSubmithook. It counts turns deterministically and emits the count as plain text, since JSON output on this event trips a known Claude Code bug on a session's first prompt. Every emitted line is phrased as a fact, never an instruction, and it fails open on any error, a malformed input, a missing transcript, an unwritable state directory, since a hook that raises could block prompt submission.tools/measure_savings.py: three checks against a real snapshot.--lintcatches structural violations, a missingFIRSTline, an exceeded cap, a missing header.--state-checkrecomputes STATE's claimed line and byte counts against the files actually on disk, instead of trusting what was written.--compareturns the schema's own "30 to 40 percent fewer tokens" claim into a measured figure against a paired prose document.tools/test_session_watch.pyandtools/test_measure_savings.py: the test suites behind both tools, 32 and 23 functions, 55 in total, all currently passing.
The snapshot is written to <notes-root>/handoffs/, the folder the project's own CLAUDE.md names, and the next session reads it from there on turn one; .claude/handoff/ is not a destination on any surface. The format stays dense enough that pasting it by hand still works where no folder is shared, and the saved copy is what supersedes cites later.
How it connects to the rest of the ecosystem
The hook, the CLAUDE.md text, and the protocol's own PATTERN field are three separate pieces built to click together.
flowchart TD
S["session_watch.py"] -->|"turn count, as a fact"| C["CLAUDE.md session-length rule"]
C -->|"suggests running it"| H["/handoff"]
H -->|"reads"| L[(critique log + retro journal)]
L -->|"defect in 2+ runs"| P[PATTERN field]
H -->|"archivist lens, on close"| R[critique]
What's not obvious: the hook and the CLAUDE.md text are two deliberately separate pieces. Detection belongs in code, since the assistant has no turn counter of its own and its sense of elapsed conversation is exactly what degrades as a session grows. CLAUDE.md only says what to do once the code's signal arrives, and that signal is worded as a stated fact, which keeps it from tripping the prompt-injection defenses an instruction-shaped hook message would trip.
session_watch.py fires at most twice, a soft nudge around turn 25 and a direct one at turn 35, bracketing the schema's own session-length: long threshold of roughly 30 turns. That first nudge is the cue to write the snapshot partway through a long session, before the whole thing has to be reconstructed from memory at the end, since recall is worst exactly where this protocol gets invoked.
On close, handoff hands off to a review skill, critique in this project, with its purpose set to the next session's objective, requesting its archivist lens explicitly instead of leaving lens selection to defaults. critique's docs bundle, which carries the archivist lens and is covered in the first post of this series, is built for exactly this kind of check, content across files that's supposed to agree, and doesn't always.
Worked example
Here's an example prompt I actually use:
create a comprehensive /handoff for this session, fact check everything in every stage, print only the final version
A typical end-of-session recap might read: "We discussed the blog series structure and made good progress. The five-post plan seems solid, though we may want to revisit post ordering later. Next step is to keep writing."
A structured handoff for the same session looks closer to this:
| Field | Content |
|---|---|
| Decided | Adopt the six-post blueprint fully (supersedes the earlier "five posts, one per skill" plan) |
| Proposed | Apply the same comment-review pass and the chosen HTML template to the other two existing posts, once ready |
| Rejected | Reusing an old filename for the new post (already belonged to an older, abandoned draft) |
| Open | The remaining three posts in the series aren't started against the blueprint yet (blocks: series completion) |
| First | Ask whether to run the other two posts through the same review pass next, or move on to drafting the next post in the series |
The second version tells a resuming reader exactly what to trust, what's still unresolved, and what to do first. The first version tells a story that has to be re-interrogated before anyone can act on it. Notice, too, that the structured version is shorter. That's a side effect of the format. Once activity gets separated from decisions and decisions get separated from open questions, most of what a recap spends words narrating simply isn't needed anymore. How much shorter varies case to case. The format's own claim about typical savings is stated as an estimate, with a comparison tool built specifically to turn it into a measured percentage against a real prose equivalent, instead of leaving it as an assertion.
That table is a simplified illustration. The real schema every snapshot actually follows is stricter, and every section carries a hard cap:
HANDOFF v3.3 | <date> | supersedes: <prior handoff id or none> | session-length: <short|long>
OBJ: <next objective, one line>
STATE <=8 lines, paths first, then installs, then running processes
DECIDED <=10 lines, user's explicit choices only, no reasoning
PROPOSED <=6 lines, assistant recommendations not yet accepted
REJECTED <=5 lines
OPEN <=5 lines, ranked by what blocks OBJ
UNVERIFIED <=5 lines
PATTERN <=3 lines, corrections that recurred this session
FIRST 1 line
The caps are load-bearing. Remove one and the schema stops doing its job. The linter (tools/measure_savings.py --lint) flags a cap violation as an error, along with a missing or duplicated FIRST line, a missing header, and, on a long session, a missing UNVERIFIED line about context degradation.
supersedes names the prior handoff and voids it. Snapshots replace each other by name instead of accumulating, so there is exactly one live snapshot per thread of work at any time, and nobody has to read a chain of them to find current state.
Design decisions
Facts and hypotheses stay intentionally separated. I've watched a strong hunch drift into the decisions section just because it was stated with confidence, and the handoff that came out of it looked more settled than the session actually was.
An incomplete handoff says what's missing. If a claim was never checked, or a section had to be cut short, the snapshot says so plainly. A tidy document that implies more coverage than it actually has creates false continuity, and false continuity is worse than an acknowledged gap, since it fails silently on the next session's first move instead of on the handoff's own page.
A handoff always triggers a review of itself. Writing the snapshot isn't the end of the protocol; the last step is running a review against the snapshot with its purpose set to the next session's objective, every time, regardless of whether the author judges the work unresolved. An archivist-style check catches whether a stranger could actually reconstruct the state from the document alone. A handoff nobody checked is just a more organised way to be confidently wrong, and the review step exists specifically to catch that before it costs the next session anything.
None of this is asked to be taken on faith. STATE's line and byte counts are computed against the real files, never estimated, and a state-check pass can flag drift between what a snapshot claims and what's actually on disk. The format's own efficiency pitch gets the same treatment, stated as an estimate in the spec itself, with a comparison tool that turns "shorter, probably" into a measured figure against a paired document. The section caps, the single-line rule for FIRST, and the long-session UNVERIFIED requirement are enforced by a linter, backed by 55 tests across both tools, instead of being left to good faith.
Continuity means preserving state, decisions, and open questions in a checkable format. A good story about what happened doesn't count.
Best practices
- Lint your own example instead of trusting it. The example that ships with this skill failed its own linter the first time anyone actually ran it, an invalid
session-lengthvalue and aFIRSTline that ran three lines against a one-line cap. - Give a hook the same scrutiny as the protocol it serves. In this project, the wrapper code around a skill has caused more bugs than the skill itself. That's why
session_watch.py's tests spend more effort on what happens when things go wrong than on the happy path. - Run the verification command after installing a silent hook. A hook that fails open looks the same whether it's working or broken, until the moment it's supposed to fire. A different skill's Stop hook broke silently for weeks in this project, and only a stale date on a downstream file gave it away.
- More voices won't improve the handoff. Multi-persona authoring was tried and dropped. A handoff transfers state instead of judging it, so extra voices multiplied tokens without adding anything orthogonal. The win came from the schema itself.
Limits
- A handoff doesn't replace source control, project management, documentation, or a direct conversation with a teammate. It's a bridge across working sessions, built for one narrow job, getting the next session acting correctly on the first move.
- It depends entirely on what went into it. A wrong line in the decisions section won't be caught by the format itself, only by the review pass or a careful reader's own restatement before acting.
- A good format doesn't substitute for judgement on the reading end. The next session restates STATE and OBJ before acting and asks about anything it can't ground, which is what catches a snapshot that reads coherently but transferred nothing.
The schema makes a snapshot checkable. It doesn't make what's in it true, and it doesn't make careful reading automatic.
Next
A handoff preserves the local state of a session. A retrospective asks what repeated patterns in that state are trying to teach us.