Carry the thread across sessions.
handoff saves the directive of a wrap-up session, then surfaces it at the start of the next one. It replaces the manual "paste a prompt into the new window" workflow with an auto-save-with-confirm loop and a surface-and-confirm resume.
claude plugin install handoff@apurvbazari-plugins
That's it — nothing to configure. The SessionStart hook ships inside the plugin and activates automatically.
Stop copy-pasting a prompt into the next window
Long Claude Code sessions often end with you typing a paragraph into the next window to continue. That paragraph is reproducible — it's just "the directive of where we left off". handoff captures it at end-of-session and surfaces it at the start of the next.
Saying a wrap-up phrase like "save handoff", "pick this up later", or "I'll come back to this" auto-invokes /handoff:save. It drafts a directive — what the next session should pick up, pointers worth reading first, constraints, and open questions — and confirms via an AskUserQuestion before writing to .claude/handoff/active.md. The directive is intent only; current commits, branch, and working-tree state are derivable at resume time, so they are deliberately not embedded.
The continuity loop
Save at the end of one session → the SessionStart hook surfaces it at the start of the next → you pick one of four ways to resume. Intent crosses the session boundary; nothing else has to.
The save → resume pipeline
One save writes a single-slot directive. At the next session start, the bundled hook reads it, computes progress signals, and routes Claude to the pickup flow. Click a stage for detail.
On every session start the hook reads .claude/handoff/active.md (exiting silently if absent), parses its frontmatter, and computes three progress signals: age in days, commits past saved-at-sha, and branch change. Two short-circuits run first — a snooze check (suppress re-surface for deferral-snooze-hours, default 24h, after a "Save for later") and a stale backstop (auto-archive anything older than stale-day-threshold, default 90 days). Otherwise it emits additionalContext and Claude invokes /handoff:pickup.
The four-option pickup
/handoff:pickup re-reads the file from disk (the surfaced content is treated as a stale snapshot), shows the directive plus progress, runs a cwd guard, then asks via AskUserQuestion. You stay in control — handoff never auto-executes a directive.
Act on the directive in this session, then archive to archive/consumed-<ts>.md.
Open the directive in $EDITOR, revise, then re-surface for another confirm.
Archive to archive/discarded-<ts>.md without acting.
Leave it in place; snooze for 24h so the next start doesn't re-surface immediately.
Before the four options, a cwd guard compares pwd against the saved saved-from-cwd. If you cd'd into a different project mid-session, the surfaced handoff was captured elsewhere — pickup asks you to reconfirm before resuming here.
Trusted routing, untrusted directive
A saved handoff is an installable, possibly-shared file — so handoff treats the directive as untrusted data. The SessionStart hook splits its output: the routing instruction and metadata are trusted; the directive content is wrapped in <untrusted-source> framing.
- Routing instruction — plain text telling Claude to invoke
/handoff:pickup. Not inside the untrusted wrapper. - Metadata —
saved-at,saved-at-sha, branch, cwd, progress tags, snooze status.
- Directive content — wrapped in
<untrusted-source>, presented as data describing user intent, not instructions to act on. - Even on Execute, the directive is guidance for Claude to apply judgement to — never commands to mechanically run.
The split matters: an attacker who lands a malicious active.md can only influence directive content, not the routing instruction. The four-option AskUserQuestion ensures you confirm any action. If you don't recognize a saved handoff in your repo, pick Discard and investigate — though with .claude/handoff/ in .gitignore (the first save offers this), that's unlikely. Defense-in-depth for a publicly-installable plugin.
Skills & the hook
Four skills, all /handoff:<name>. save and pickup auto-invoke on intent; check auto-invokes but is read-only; discard is destructive and user-invoked only.
| Skill | Invocation | What it does |
|---|---|---|
| /handoff:save | auto | Auto-invokes on a wrap-up phrase; drafts a directive and confirms via AskUserQuestion before writing active.md. The slash form is the no-confirm fallback. |
| /handoff:pickup | auto | Auto-invokes after the hook surfaces a handoff. Re-reads from disk, runs the cwd guard, presents the four-option flow (Execute / Edit / Discard / Save for later). |
| /handoff:check | auto · read-only | Reports whether a handoff exists, its age, saved-at SHA/branch, how far the repo has moved past it, and snooze status. A sanity check for "why isn't this surfacing?". |
| /handoff:discard | user-only | Archive the active handoff without acting — the same effect as picking Discard in pickup, but invocable from anywhere. disable-model-invocation. |
| Hook | Event | Role |
|---|---|---|
| session-start.sh | SessionStart | Auto-registered (no setup). Reads active.md, runs snooze + stale checks, computes git progress, and emits additionalContext routing Claude to pickup. Exits silent on the common no-handoff path. |
One active slot, an archive, and a few knobs
Everything lives under .claude/handoff/ (the v0.2.0 folder layout): a single active handoff plus a timestamped archive. Settings are optional — defaults apply if absent.
One per repo (single-slot). Frontmatter holds saved-at, saved-at-sha, saved-at-branch, saved-from-cwd.
The directive that was acted on, kept (renamed, not deleted) so it stays recoverable.
Archived without acting — from the pickup flow or /handoff:discard.
Moved here once older than stale-day-threshold (default 90 days).
Override thresholds, snooze window, gitignore behavior, archive retention, and trigger phrases. Edit directly.
| Key | Default | Effect |
|---|---|---|
| stale-commit-threshold | 3 | Commits past saved-at-sha before the handoff is tagged "progress made" (a surface tag, not a behavior change). |
| stale-day-threshold | 90 | Days past saved-at before the hook silently auto-archives to expired-<ts>.md. |
| deferral-snooze-hours | 24 | Hours to suppress re-surface after "Save for later". |
| gitignore-prompt | ask | ask or never — whether the first save offers to add .claude/handoff/ to .gitignore. |
| archive-retention | 10 | Cap on archive file count (shared across consumed / discarded / expired). Special: 0, unlimited, -1. |
| trigger-phrases | (built-in list) | Additions/overrides for the save NL trigger whitelist. |
Upgrading from 0.1.x? v0.2.0 moves every artifact into .claude/handoff/. There is no automatic detection of 0.1.x files — the README ships a one-liner to migrate the flat .claude/handoff.md layout and update the .gitignore pattern from .claude/handoff*.md to .claude/handoff/.