Skip to content

Repository files navigation

claude-code-handoff

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.

What it does

  • 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.

Installation

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.

Plugin install (recommended)

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.

Bare-scripts install (legacy / alternative)

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.sh

The 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.sha256

A 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.)

Either way

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 an ACTION: RUN /handoff-recover banner; 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.

Dual-mode warning

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.

Requirements

  • bash and jq. If jq is not on PATH, the installer refuses to install (exit 1) because the Stop hook, context nudge, and /handoff-recover all depend on it at runtime. (--uninstall and --doctor still work without jq.)
  • 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-recover use it to strip transcript noise and fall back to cat when 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 flock and date. Windows (Git Bash / WSL) is untested in CI.

How it works

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.sh wires 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 statusLine at 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 -td picks 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.

Configuration

Common env vars (set in ~/.bashrc / ~/.zshrc):

  • HANDOFF_HISTORY_KEEP — snapshots kept in history (default 5; 0 disables 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; 0 disables).
  • 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=600

Updating, doctor, uninstall

This 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.

Going deeper

  • 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.

Develop

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.

License

MIT.

About

Session continuity for Claude Code — auto-snapshots git state and model decisions at context boundaries so the next session starts informed, not blank.

Topics

Resources

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages