Claude Code sessions hit a context limit. When they do, the next session starts blind — you re-explain the project, the in-flight work, the decisions you made twenty minutes ago, all from memory. This skill makes the next session not blind.
Three paths, doing different jobs:
/handoff(preferred, you invoke it at session end): writes the snapshot AND asks the assistant to append a "Notes from this session" prose block — decisions, open questions, "next session should start with X." The prose is the part git can't see. Use this at clean boundaries (commit lands, track wraps) or when your context meter is getting tight. Rule of thumb: invoke at 30-50% remaining, not at 5% — quality degrades well before the meter runs out, and you want the reflection to happen while the model is still sharp./handoff-more(you invoke it in a fresh session): pulls older handoffs into the new session's context, beyond the single most-recent one that auto-loads. Use it when the loaded handoff is thin, when you reference work from a session further back than yesterday, or to give a sibling re-entering the repo continuity deeper than the last session alone./handoff-recover(auto-triggered by the SessionStart hook): composes a retroactive curated handoff when the previous session ended without/handoff— crashed, killed, or just never invoked. The SessionStart hook detects the placeholder Notes block and prints anACTION: RUN /handoff-recoverbanner; the skill reads the previous session's raw per-turn dump under.claude/handoff_backups/, the prior curated handoff under.claude/handoff_history/, and (if present) the host-wide session registry, then reconstructs what the lost session would have written and persists it back intohandoff_current.mdso the recovery survives into future history.SessionEndhook (automatic, safety net): on clean session exit, fires the same snapshot script — but no model is in the loop, so the "Notes from this session" block stays empty. You get git state (HEAD, branch, recent commits, working tree, in-flight docs) and nothing else. It's there so an unplanned exit isn't a total loss, not as a substitute for/handoff. The hook passes--if-curated, so if you already ran/handoffthis session (it replaced the placeholder block with curated Notes), the safety-net write is a no-op — your curated content stays put rather than being rotated into history.
The next session in the same repo auto-loads the latest snapshot via
the SessionStart hook. No /compact to remember, no kickoff prompt
to write, no copy-paste. The new session starts with a fresh context
window — the loaded handoff itself consumes a few KB (more if the
Notes from this session prose is long), which is negligible against
a 200k or 1M window.
A third hook (Stop) does two jobs each assistant turn:
- Appends the turn to a raw-dump backup under
.claude/handoff_backups/— the fallback for cases where the process is killed beforeSessionEndcan fire (SIGKILL, terminal closed). - Records context measurements into
.claude/handoff_backups/: the real token count from the latest assistant turn'susage(same number/contextshows) into.ctx_tokens_<session_id>, and the transcript JSONL byte size into.ctx_<session_id>as a fallback. A fourth hook (UserPromptSubmit) reads those on the next prompt and, if usage has crossed ~50% of the configured context window (auto-detected as 1,000,000 tokens if~/.claude.jsonshows a[1m]model active for this project, else 200,000), injects a<system-reminder>telling the assistant to flag this passively as a natural/handoffmoment. That's how the assistant knows to mention it without you having to glance at the meter.
Only the latest snapshot is named handoff_current.md. Each new
write rotates the previous one into <repo>/.claude/handoff_history/,
filename stamped with the snapshot's own timestamp. The last 5 are
kept (override via HANDOFF_HISTORY_KEEP=N, or 0 to disable
retention); older entries are pruned. So the on-disk layout looks
like:
<repo>/.claude/
├── handoff_current.md # the latest snapshot (always)
├── handoff_pinned.md # optional: carried forward verbatim (see below)
└── handoff_history/ # rotated older snapshots
├── handoff_2026-05-13_174853.md # yesterday's
├── handoff_2026-05-12_194751.md # two sessions ago
├── handoff_2026-05-11_103022.md
├── handoff_2026-05-10_215800.md
└── handoff_2026-05-09_142105.md
Two consumers read this directory:
- The
SessionStarthook auto-includes the most recent history entry ifhandoff_current.mdwas an auto-write (no curated Notes from/handoff) — so an unplanned exit doesn't strand the next session with only mechanical git state. When current already has curated Notes, the hook just notes that history exists. /handoff-morereads up to N retained snapshots into context on demand, so the assistant can see further back than just yesterday.
The retention dir is bootstrapped into the repo's .gitignore on
first write — handoffs are intentionally per-developer, not
checked-in artifacts.
Some context outlives a single session but isn't a permanent rule —
load-bearing facts the next session needs, and guardrails ("don't drop
X", "Y connects via Z, not a password"). Re-typing those into the Notes
block every session is lossy. Drop them in <repo>/.claude/handoff_pinned.md
and they're injected verbatim at the top of every handoff. The script
only reads that file — never rotates or regenerates it — so it persists
untouched until you edit it. Three layers, by lifetime:
AGENTS.md— permanent governance rules.handoff_pinned.md— durable-but-temporary context + guardrails that expire when the underlying state resolves (a migration finishes, an incident closes). The pin is where you record those.- Notes block — this-session intent only.
The pin is gitignored on first write (same per-developer posture as the
handoff). Override its path with HANDOFF_PINNED_FILE. Absent file →
no pinned section; repos that don't use it are unaffected.
If your repo keeps a SYSTEM_LOG.md (an append-only record of
shape-changing work — security posture, scaffold/topology, migrations),
the handoff flags a ⚠️ section when this session's commits look
system-level (by changed-path or commit-subject heuristic) but none of
them touched the log. It's a reminder to record the work before context
is lost, not a gate. It fires only at handoff time over the
previous-handoff→HEAD commit range, so routine sessions stay silent.
Override the watched file with HANDOFF_SYSTEMLOG_FILE; tune the
heuristics inline in write_handoff.sh if they over-fire for your repo.
Absent file → no nudge.
After /handoff (or any session exit) you get
<repo>/.claude/handoff_current.md:
# myproject — session handoff (auto-generated)
**Generated:** 2026-05-12 14:59 UTC
---
## Repo: myproject
**HEAD:** `abc1234` — wire up the new endpoint
**Branch:** `feature/new-endpoint` (feature/new-endpoint...origin/feature/new-endpoint [ahead 2])
### Recent commits
```
abc1234 wire up the new endpoint
def5678 add request validator
9012abc move shared types out of the handler
```
### Working tree
```
M src/handler.ts
?? docs/design-new-endpoint.md
```
## In-flight (untracked or modified .md under `docs/`)
- `docs/design-new-endpoint.md`
## Verify state matches reality
```bash
git -C /path/to/myproject status && git -C /path/to/myproject log --oneline -5
```
---
## Notes from this session
Decided to ship the single-tenant version first; multi-tenant deferred
to a follow-up. Open question: whether to validate the upload size on
the client or rely on the server limit. The design doc at
`docs/design-new-endpoint.md` is the source of truth; next session
should start by reading it.The auto-snapshot above the --- is git state — cheap, mechanical,
always correct. The "Notes from this session" block is the part git
can't see: decisions, in-flight tracks, open questions. The
SessionEnd hook leaves that block as a placeholder. Running
/handoff is what fills it in, so for any session that involved real
discussion, /handoff is the preferred path; SessionEnd is the
safety net for unplanned exits.
git clone https://github.com/Sting25/claude-code-handoff.git ~/code/claude-code-handoff
cd ~/code/claude-code-handoff
./install.shThat:
- Symlinks the bin scripts and skills into
~/.claude/. - Patches
~/.claude/settings.jsonto add four hooks (SessionStart,SessionEnd,Stop,UserPromptSubmit) and four permission entries.
Settings.json is backed up before any change and the patch is idempotent — existing hooks and permissions are detected by marker substring and skipped on re-runs. Unrelated entries in your settings.json (other hooks, theme, etc.) are left untouched.
Requires jq for the settings.json patch. If you don't have it, the
installer prints the JSON snippet for you to paste manually.
By default the bin scripts are symlinked, so a git pull in the
clone is live in the next session with no re-install. But if you install
from a volatile checkout — a /tmp worktree, a git archive
extract, a CI scratch dir — those symlinks dangle the moment the source
is cleaned up, and the hooks then silently no-op. So when the installer
detects a volatile repo_root (under /tmp, /var/tmp, /dev/shm,
$TMPDIR, or an mktemp-style tmp.XXXX path) it switches to copy
mode automatically and says so. A normal persistent clone always
symlinks.
./install.sh --copy # force copy mode (snapshot; survives source deletion)
./install.sh --link # force symlinks even from a volatile path
./install.sh --doctor # report any dangling/missing installed hooks (exit ≠0 if broken)HANDOFF_FORCE_SYMLINK=1 is the env-var escape hatch for the volatile
auto-copy. If a previous install left dangling links (e.g. you installed
from /tmp), re-run ./install.sh from a persistent clone — or
--copy — to repair it; --doctor tells you whether you need to. As a
second line of defense, every SessionStart self-checks its own hook
links and prints a visible warning if any dangle.
Runs on Linux, macOS, and Windows (Git Bash / WSL). Needs bash, git,
and jq; the Stop hook also uses perl to strip transcript noise. The
hook scripts are kept portable across GNU and BSD/macOS userlands (e.g.
flock/tac/mapfile/date differences are handled), and the test
suite exercises the BSD code paths under tool shims.
Because the install is symlink-based, updating the scripts is just:
cd ~/code/claude-code-handoff && git pullNo re-install needed. Edits in the repo are live in the next Claude
Code session. The exception is if a future release changes the hook
command string or adds a new hook — that's called out in
CHANGELOG.md, and re-running ./install.sh after
git pull re-patches your settings.json.
Run ./install.sh --doctor anytime to confirm every installed hook
still resolves (handy after moving or re-cloning the repo).
./install.sh --uninstallRemoves the symlinks and strips the patched hooks + permissions from settings.json (backup first). The repo itself is untouched.
.
├── bin/
│ ├── write_handoff.sh # snapshot script (used by skill + SessionEnd hook); also rotates history
│ ├── handoff_session_start.sh # SessionStart hook: cats current + previous-as-fallback + history pointer
│ ├── handoff_turn_append.sh # Stop hook: per-turn dump + records transcript size
│ └── handoff_ctx_check.sh # UserPromptSubmit hook: flags /handoff past threshold
├── skills/
│ ├── handoff/
│ │ ├── SKILL.md # /handoff slash command spec
│ │ └── README.md # full docs: env vars, customization, limitations
│ ├── handoff-more/
│ │ └── SKILL.md # /handoff-more slash command: load older handoffs into context
│ └── handoff-recover/
│ └── SKILL.md # /handoff-recover slash command: retroactively compose Notes when the previous session crashed
├── docs/
│ └── handoff-pattern.md # design philosophy: the WRITE/READ discipline behind the tool
├── tests/ # dependency-free bash test suite (./tests/run.sh)
├── install.sh # symlink + settings.json patcher
├── CHANGELOG.md
├── LICENSE # MIT
└── README.md # this file
For the skill spec, env vars (substrate pattern, in-flight directories,
gitignore bootstrap), and the limitations worth knowing about —
notably that Claude Code can't actually force a session restart at a
context threshold, so the human keystroke is still required — see
skills/handoff/README.md.
For the design philosophy behind the pattern — why state lives on the
filesystem and the handoff carries only what it can't, and the WRITE/READ
discipline that keeps it small — see
docs/handoff-pattern.md.
Edits land live (symlink install). Commit, push, pull on other
machines. If you change any of the hook command strings or add a new
hook / permission, update CHANGELOG.md so users know to re-run
./install.sh after pulling.
Run the test suite with ./tests/run.sh — dependency-free bash + git
(tests needing jq/perl self-skip when those are absent). Each suite
file is a standalone tests/test_*.sh. New changes ship with a test.
MIT.