Claude Code sessions run out of context. This tool writes a short, human-readable handoff file at the end of a session and loads it automatically when the next session starts — so you pick up where you left off instead of starting from a blank slate or an auto-compacted digest you never read.
- Snapshots where you left off — git state plus a written summary of decisions, open questions, and next steps.
- Auto-loads that snapshot into your next session in the same repo — no copy-paste, no kickoff prompt.
- Keeps a per-turn backup, so a crashed or killed session can be reconstructed afterwards.
- Saves a mechanical git-state snapshot on every clean exit, even if you forget to hand off.
- Nudges you when context is filling (~40% used), while Claude is still sharp enough to write a good summary — so you hand off confidently instead of riding a bloated session, which also means less context re-processing and fewer wasted tokens.
- Signs each handoff with a per-machine key, so a cloned repo can't inject fake standing rules.
- Keeps a history of past handoffs (last 5 by default) that you can pull back into context on demand.
- Works in git and non-git projects, in both the Claude Code CLI and the desktop app.
Two supported install modes, both fully maintained: install as a
plugin (recommended — hooks and skills come with it automatically,
no manual patching) or install the bare scripts (legacy, still
fully supported) into ~/.claude/. Pick one — see
Dual-mode warning below before running both.
Inside Claude Code:
/plugin marketplace add Sting25/claude-code-handoff
/plugin install claude-code-handoff@claude-code-handoff
This adds the claude-code-handoff marketplace — self-hosted in this
repo at .claude-plugin/marketplace.json
— and installs the claude-code-handoff plugin from it. Hooks
(hooks/hooks.json) and skills (skills/) come
with the plugin and are picked up automatically — no
~/.claude/settings.json patching, no symlinking. Scripts run from
the plugin's own bin/, wherever Claude Code checks the plugin out
(cache path pattern
${CLAUDE_CONFIG_DIR:-~/.claude}/plugins/cache/<marketplace>/claude-code-handoff/<version>/bin/).
The one thing a plugin install can't wire up for you is the optional status line — details below, under How it works → Status line.
Still fully supported — dual-mode is a deliberate design choice, not a deprecated path.
git clone https://github.com/Sting25/claude-code-handoff.git ~/code/claude-code-handoff
cd ~/code/claude-code-handoff
./install.shThe installer symlinks the scripts and skills into ~/.claude/ and
patches ~/.claude/settings.json (backed up first; idempotent; your
own settings are left untouched).
install.sh is a generated artifact (concatenated from install.d/
by tools/build-install.sh; CI rebuilds and diffs it on every change)
and ships with a checksum. Inside a full clone, git already guarantees
integrity — but if you ever fetch install.sh on its own instead of
cloning, fetch install.sh.sha256 from the same tag and verify before
running:
shasum -a 256 -c install.sh.sha256A mismatch means a corrupted or tampered download — don't run it. (Both files travel the same channel, so this catches corruption and casual tampering, not a fully compromised host.)
Once installed (either mode), in any project:
/handoff— run at the end of a working session; writes the snapshot and fills in the "Notes from this session" prose block. Invoke at 30-50% context remaining, not at 5%./handoff-recover— run when a new session shows anACTION: RUN /handoff-recoverbanner; reconstructs the notes a crashed or un-handed-off session never wrote./handoff-more— run in a fresh session to pull older handoffs into context, beyond the most recent one that auto-loads.
Everything else — loading, per-turn backups, the context nudge — runs through hooks without you thinking about it.
Claude Code does not dedupe hooks across install modes. If you install
the plugin and leave a bare-scripts install wired into
~/.claude/settings.json, both sets of hooks fire on every event —
every SessionStart/Stop/SessionEnd/etc. runs twice, doubling
writes and raw-dump appends. Pick one mode per machine. The doctor
(./install.sh --doctor) warns when it detects both installed at
once — run it if you're unsure which mode you're in.
bashandjq. Ifjqis not on PATH, the installer refuses to install (exit 1) because the Stop hook, context nudge, and/handoff-recoverall depend on it at runtime. (--uninstalland--doctorstill work withoutjq.)git— optional since 0.8.4. Outside a git worktree the snapshot simply omits the git sections and the project directory becomes the root; the installer runs no git at all.perl— optional. The Stop hook and/handoff-recoveruse it to strip transcript noise and fall back tocatwhen it is absent, so the dumps are just noisier without it.openssl— optional. Without it, handoffs aren't HMAC-signed and the rules layer loads as reference data instead of binding; nothing errors.- Tested on Linux — that CI job blocks. The macOS CI job (bash 3.2 /
BSD userland) is advisory, non-blocking; the scripts handle BSD
differences like
flockanddate. Windows (Git Bash / WSL) is untested in CI.
Six hooks. A bare-scripts install writes these into
~/.claude/settings.json via ./install.sh; a plugin install ships
the same six in hooks/hooks.json and Claude Code
loads them automatically — no settings.json patching either way:
| Hook | Job |
|---|---|
SessionStart |
Loads the latest handoff into the new session; prints an ACTION: RUN /handoff-recover banner if the last session ended without curated notes. |
SessionEnd |
Safety-net git-state snapshot on clean exit. No-op if you already ran /handoff; skipped on /resume session-switches. |
Stop |
After each assistant turn: appends the turn to a raw-dump backup and records context measurements. |
UserPromptSubmit |
Injects an advisory /handoff nudge once usage crosses ~40% of the window; periodically re-injects verified rules so they don't decay. |
PreCompact |
Same safety-net snapshot before compaction destroys the conversation. |
PostCompact |
Resets context measurements so the freed window is treated as fresh. |
Files. The latest snapshot lives at
<repo>/.claude/handoff_current.md; each new write rotates the old one
into .claude/handoff_history/ (last 5 kept; HANDOFF_HISTORY_KEEP=N
to change, 0 disables pruning entirely). Per-turn raw dumps and
context measurements live under .claude/handoff_backups/. Both dirs
are gitignored on first write — handoffs are per-developer, not
checked-in artifacts. Retention only ever deletes files this tool
generated; anything you drop into those directories yourself is left
alone.
The handoff doc. Above the fold: mechanical git state (HEAD,
branch, recent commits, working tree, in-flight docs). Below: the
"Notes from this session" block — decisions, open questions, next
steps — which only /handoff fills in (the automatic safety net
leaves it as a placeholder). An HMAC trailer proves the doc was
written locally. Full example in
docs/reference.md.
Trusted rules. A handoff can carry standing rules (a pinned file at
.claude/handoff_pinned.md plus a ## Rules section) meant to bind the
next session. They load as binding only when provenance is proven: the
file is untracked in git AND carries a valid HMAC-SHA256 signature made
with a per-machine secret (~/.claude/handoff_secret, never in any
repo) — so a cloned repo can't inject fake rules. Anything less (no
openssl, tracked file, bad signature, HANDOFF_TRUST_DISABLE=1)
loads the whole file as defanged reference data. Model-authored notes
never bind. Verified rules are re-injected as the transcript grows so
they don't decay. Full design:
docs/reference.md.
Why the cryptography (in one paragraph). Prompt injection can't be
fully prevented — a session that reads a poisoned README or web page can
be manipulated. What the signing prevents is that compromise persisting:
the handoff would be the natural place for a manipulated session to plant
standing orders for every future session, so nothing gets promoted to
binding without a seal only this machine's writer can produce. Since
v0.13.0 that includes a structural fingerprint recorded at publish time —
--restamp (re-signing after the model curates its notes) refuses to
bless a document whose structure changed outside the two zones the model
is allowed to edit. The failure mode is always a downgrade: tampered or
unverifiable rules still load, but as visibly untrusted reference notes.
One boundary is deliberate: rules the model writes in its own sanctioned
Rules section (that's the carry-your-fences-forward feature) do bind —
review them when they change.
Status line (optional). handoff_statusline.sh renders
Fable | ctx 34% (340k/1000k) | handoff: curated and caches Claude
Code's own context numbers (.ctx_sl_<session_id>) so the nudge uses
real usage instead of model-id guessing. statusLine is a single slot
in ~/.claude/settings.json — only one command can occupy it — so how
it gets wired differs by install mode:
-
Bare-scripts install —
./install.shwires it for you, but only if you don't already have a statusLine (an existing one is never overwritten; the installer prints the manual step instead). -
Plugin install — plugins cannot set
statusLineat all (Claude Code has no per-plugin mechanism for it); wiring it is a manual, optional paste into~/.claude/settings.json:"statusLine": { "type": "command", "command": "bash \"$(ls -td \"${CLAUDE_CONFIG_DIR:-$HOME/.claude}\"/plugins/cache/*/claude-code-handoff/*/bin/handoff_statusline.sh 2>/dev/null | head -1)\" 2>/dev/null" }
ls -tdpicks the newest cached plugin version by directory mtime, not by version number, so it can pick the wrong script if an older version's cache directory was touched more recently than the current one.
Details: docs/reference.md.
Common env vars (set in ~/.bashrc / ~/.zshrc):
HANDOFF_HISTORY_KEEP— snapshots kept in history (default 5;0disables pruning entirely).HANDOFF_CTX_WINDOW_TOKENS— pin the context-window size the nudge assumes; overrides all auto-detection.HANDOFF_PINNED_FILE— alternate path for the pinned-context file.HANDOFF_TRUST_DISABLE=1— disable binding rules; everything loads as reference data.HANDOFF_FENCES_REINJECT_KB— transcript growth between rule re-injections (default ~200KB;0disables).HANDOFF_SESSIONEND_SKIP_REASONS— SessionEnd reasons treated as a pause, not an ending (default:resume).
Full list in skills/handoff/README.md. Test/debug overrides:
# Override where handoff_recover_tail.sh (and ONLY that script) looks for the
# per-session cursor file used by /handoff-recover. Default:
# <repo>/.claude/handoff_backups. This does NOT relocate the backup directory
# project-wide: the Stop hook, ctx-check, compact-reset, and statusline hooks
# each resolve <repo>/.claude/handoff_backups independently and ignore this
# var, so setting it to anything but the real write location desyncs
# recover_tail from its own cursor file (it warns on stderr if it detects
# this). Meant for tests that stage a throwaway cursor file, not for
# relocating handoffs.
export HANDOFF_BACKUP_DIR=/custom/path
# Override the projects root searched by /handoff-recover to find the
# current session's transcript. Default: $HOME/.claude/projects.
export HANDOFF_PROJECTS_DIR=/custom/projects/path
# Use this exact JSONL path as the transcript, skipping the projects-dir
# search. Useful for testing or when the transcript lives outside the
# normal Claude Code structure.
export HANDOFF_RECOVER_TRANSCRIPT=/path/to/transcript.jsonl
# How old a lock must be before it is presumed ORPHANED and forcibly
# reclaimed. Not a timeout: nothing waits this long, and lowering it does
# not make anything give up faster — it makes locks get STOLEN from
# holders that are slow but alive, which interleaves dump content and
# clobbers the cursor. Read by the Stop hook's per-turn dump lock AND by
# write_handoff.sh's whole-run and .gitignore locks. Default: 300, chosen
# to sit comfortably above Claude Code's 60s hook timeout. Raise it if you
# see dumps interleaving on a very slow machine; there is no reason to
# lower it. Must be a plain integer — anything else falls back to 300.
export HANDOFF_LOCK_STALE_SECS=600This section covers the bare-scripts install. Symlink install
means updating is just git pull in the clone — live next session, no
re-install (re-run ./install.sh only when
CHANGELOG.md says a hook changed). Run
./install.sh --doctor anytime to confirm every installed hook still
resolves (and to check for a plugin install running alongside it — see
Dual-mode warning). ./install.sh --uninstall
removes the symlinks, strips only the entries it can prove are its own
from settings.json (backup first), and deletes the per-machine HMAC
secret — details in
docs/reference.md.
Plugin installs update and uninstall through Claude Code's own plugin
management (/plugin in-session) instead — there's no git pull or
install.sh step for that mode.
Installing from a volatile path (/tmp, CI scratch) auto-switches to
copy mode; --copy / --link force it, and --model 'opus[1m]' pins
the model on a new machine. See
docs/reference.md.
- docs/reference.md — full reference: the four paths in detail, context tracking, status line internals, retention edge cases, pinned context, trusted-rules design, install internals.
- docs/handoff-pattern.md — design philosophy: why state lives on the filesystem and the handoff carries only what it can't; the WRITE/READ discipline.
- skills/handoff/README.md — skill spec, env vars, and limitations worth knowing — notably that Claude Code can't force a session restart at a context threshold, so the human keystroke is still required.
Edits land live (symlink install). Run the test suite with
./tests/run.sh — dependency-free bash + git (tests needing
jq/perl self-skip when absent). Each suite file is a standalone
tests/test_*.sh. New changes ship with a test. If you change a hook
command string or add a new hook/permission, update CHANGELOG.md so
users know to re-run ./install.sh after pulling.
MIT.