# Graceful Handoff Plugin for Claude Code > Auto-trigger session handoff at the context threshold so long-running work survives the next session boundary. Manual `/graceful-handoff` always works as a backup. *Built for my own Claude Code workflow and shared openly for anyone who finds it useful. This is a solo project — bug reports and feature requests are welcome, but pull requests are not accepted.* *AI-generated: all code produced by Claude Code through dialog-driven development. [Full disclosure →](../../README.md#ai-generated-code-disclosure)* ![Version](https://img.shields.io/badge/version-2.1.0-blue) ![Platform](https://img.shields.io/badge/platform-Claude_Code_Plugin-purple) ![Skill](https://img.shields.io/badge/skill-1-green) ![Hooks](https://img.shields.io/badge/hooks-3-red) ![Pipeline](https://img.shields.io/badge/pipeline-deterministic-cyan) ![Tests](https://img.shields.io/badge/tests-57-success) ![License](https://img.shields.io/badge/license-MIT-lightgrey) A Claude Code plugin that solves a structural problem with long sessions: the context window fills (often within ~5 minutes of real work on Opus 4.7), and the user is forced to summarize, commit, and write a continuation prompt under time pressure — or skip steps and lose continuity. This plugin removes those steps from the user's hands using a deterministic JSON pipeline plus three hooks that detect the threshold, auto-execute the reversible work, and auto-load the artifact in the next session. --- ## Table of Contents - [What Is This?](#what-is-this) - [The Problem](#the-problem) - [Quick Start](#quick-start) - [Architecture](#architecture) - [How auto-trigger works](#how-auto-trigger-works) - [Components](#components) - [Commands & Arguments](#commands--arguments) - [Workflow Examples](#workflow-examples) - [Safety Guarantees](#safety-guarantees) - [Testing](#testing) - [Limitations & Open Assumptions](#limitations--open-assumptions) - [Version History](#version-history) - [License](#license) - [Feedback & Contributing](#feedback--contributing) --- ## What Is This? Three hooks plus one skill that handle session handoff for you: - **statusLine hint** at 60% and an urgent reminder at 70% — display only, always safe - **Stop-hook auto-execute** at estimated ≥70% — writes the artifact + creates a commit. Push remains user-triggered - **SessionStart auto-load** on `source: resume`/`compact` — handoff content is injected into the new session automatically; no `cat` needed - **Manual `/graceful-handoff`** — always works as a backup, with the same arguments The skill itself is `disable-model-invocation: true`. The model cannot autonomously invoke handoff — only the user (via the slash command) or the Stop hook (which calls the pipeline script directly, not the skill) can trigger it. This is intentional: handoff is a moment that should be deliberate. > [!TIP] > Install the plugin and forget about it. The first time the Stop hook fires, the artifact appears, a commit lands, and `git push` is yours to run when ready. --- ## The Problem Opus 4.7 fills the context window quickly. On real work — file reads, tool output, agent results — a session can hit 60–70% in five minutes. When it happens, three manual steps become rushed or skipped: 1. Summarize the state of the work (commits, local changes, what was tested) 2. Commit and push finished work (otherwise it is lost when the session ends) 3. Write a copy-paste prompt that lets the next session continue without context loss Doing these three things at 65% context, with the model already forgetting earlier turns, is exactly when mistakes happen. This plugin moves all three out of the critical path: - **Detection** — the Stop hook estimates context usage and fires at ≥70% - **Reversible execution** — the artifact is written and committed automatically - **Irreversible execution** — `git push` stays in your hands; the plugin will never push for you - **Continuation** — on the next session, the artifact is auto-loaded into context The ~10% gap between the 60% statusLine hint and the 70% Stop-hook trigger gives you a window to invoke `/graceful-handoff` manually if you want to control the slug or skip the commit. --- ## Quick Start ### Prerequisites - [Claude Code](https://docs.anthropic.com/en/docs/claude-code) v2.x+ - Node.js (any recent LTS — required for hook and pipeline scripts) - Git repository (the pipeline detects detached HEAD and missing upstream and reports gracefully — it never crashes) ### Install ```bash claude plugin marketplace add https://git.fromaitochitta.com/open/ktg-plugin-marketplace.git ``` Or enable directly in `~/.claude/settings.json`: ```json { "enabledPlugins": { "graceful-handoff@ktg-plugin-marketplace": true } } ``` The three hooks activate immediately on install. No further configuration needed. ### First handoff Manual: ``` > /graceful-handoff ``` Or just keep working — when context crosses the estimated 70% threshold, the Stop hook fires automatically: ``` ⚠️ Auto-handoff utført ved estimert 72% [kilde: direct]: artefakt /path/NEXT-SESSION-PROMPT.local.md. Push gjenstår — kjør `git push` når du er klar. ``` Start the next session with `claude --resume` and the artifact is loaded into context automatically. --- ## Architecture ```mermaid flowchart TB subgraph Detection["Detection — display + auto-trigger"] direction LR SL["statusLine
60% hint, 70% urgent"] SH["Stop hook
≥70% estimated"] end subgraph Pipeline["Deterministic pipeline (Node, no LLM)"] direction LR P["handoff-pipeline.mjs
classify → write → stage → commit"] end subgraph Resumption["Resumption — auto-load"] direction LR SS["SessionStart hook
resume / compact only"] AR["Archive after read
*.archived.local.md"] end subgraph Manual["Manual fallback"] direction LR SK["SKILL.md
disable-model-invocation: true"] end SL -.display only.-> User SH -->|spawns| P SK -->|invokes| P P -->|writes| AR SS -->|reads| AR User((user)) -->|/graceful-handoff| SK User -->|git push| Done((done)) ``` Three independent layers: **detection** (hooks watching context), **pipeline** (deterministic script that does the work), **resumption** (hook that loads the artifact in the next session). Each layer is testable in isolation. The pipeline has no LLM dependencies — `node:test` runs it against fixtures in <8 s. --- ## How auto-trigger works Claude Code does not expose real-time context-percentage to hooks (Anthropic has closed feature requests [#16988](https://github.com/anthropics/claude-code/issues/16988), [#27969](https://github.com/anthropics/claude-code/issues/27969), [#34340](https://github.com/anthropics/claude-code/issues/34340)). Instead, the Stop hook uses a **4-step resolution chain** (v2.1, in `resolveContextSource()`): | Step | Source | When used | Source label | |------|--------|-----------|--------------| | 1 | `payload.context_window.used_percentage` | If the field is present and > 0 | `direct` | | 2 | `payload.context_window.context_window_size` + transcript estimate (`chars / 3.5`) | If size > 0 but no `used_percentage` | `payload-size` | | 3 | `MODEL_WINDOWS[payload.model.id]` + transcript estimate | Opus 4.7 = 1 M, Sonnet 4.6 = 200 K, Haiku = 200 K | `model-map` | | 4 | `FALLBACK_WINDOW = 1_000_000` + transcript estimate | Last-resort default (2026-aware) | `default-1m` | When the resolved percentage is ≥ 70%, the Stop hook spawns `handoff-pipeline.mjs --auto --no-push --non-interactive` synchronously (25 s timeout, fits within the 30 s Stop-hook budget). The `additionalContext` message includes `[kilde: ]` so the source path is always visible. **Estimation drift:** Steps 2–4 use `chars / 3.5` to approximate tokens, which can drift ±10% from Claude's internal counting. The 70% threshold is conservative buffer. Step 1 (`direct`) has no drift. **Lock file:** `/.handoff-lock-` is created on first trigger to prevent repeat firing within the same session. Touch happens *before* spawning to win races on rapid Stop events. **Why 70% (not 65%)?** Earlier designs targeted 65%, but estimation drift and Stop-hook latency make 70% safer. Lower thresholds risk false positives that block normal continuation. --- ## Components ### Skill — `skills/graceful-handoff/SKILL.md` ```yaml --- name: graceful-handoff description: Produser handoff-artefakt, commit+push, og copy-paste-prompt for neste sesjon. disable-model-invocation: true model: claude-sonnet-4-6 allowed-tools: Bash(git:*) Bash(jq:*) Bash(node:*) Bash(find:*) Bash(pwd:*) Read Write Glob --- ``` Thin orchestration wrapper around the pipeline script. Pinned to Sonnet 4.6 to free Opus budget for the next session. `disable-model-invocation: true` prevents the model from calling the skill on its own — handoff is always user- or hook-triggered. > [!NOTE] > `allowed-tools` is *pre-approval*, not restriction. It removes permission prompts for the listed tools but does not block other tools from being invoked. For real sandboxing, use project-level `permissions.deny` rules. ### Pipeline — `scripts/handoff-pipeline.mjs` Deterministic Node script. Returns structured JSON. No LLM dependencies. Handles: - Classification of handoff type (`multi-sesjon` / `plugin-arbeid` / `enkelt-oppgave`) based on cwd - Writing the NEXT-SESSION artifact in the correct directory - **Explicit staging** of only the artifact (+ `REMEMBER.md` / `TODO.md` if present) — *never* `git add -A`, enforced by a regression test - Commit-message generation from `git diff --stat` (Conventional Commits) - Push (unless `--no-push`) with detached-HEAD and no-upstream detection - Idempotency check: 60 s cooldown on a clean tree with a recent artifact is a no-op ### Hooks — `hooks/scripts/` | Event | Script | What it does | |-------|--------|--------------| | `statusLine` | `statusline-monitor.mjs` | Reads `context_window.used_percentage` from payload. <60% silent, 60–69% hint, ≥70% urgent reminder. Display only — never runs git (statusLine scripts are cancellable mid-flight per official docs) | | `Stop` | `stop-context-monitor.mjs` | Resolves context via the 4-step chain. At ≥70% spawns the pipeline with `--auto --no-push --non-interactive`. Uses a lock file to prevent repeat firing | | `SessionStart` | `session-start-load-handoff.mjs` | On `source: resume` or `source: compact`, finds the most recent `NEXT-SESSION-*.local.md` (cwd + 3 levels up), injects the content via `additionalContext`, archives the file (`*.archived.local.md`) to prevent stale-load on subsequent sessions | Registered in `hooks/hooks.json`. --- ## Commands & Arguments ``` /graceful-handoff [topic-slug] [flags] ``` | Argument | Description | |----------|-------------| | `[topic-slug]` | Kebab-case slug. With slug: `NEXT-SESSION-.local.md`. Without: `NEXT-SESSION-PROMPT.local.md` | | `--no-commit` | Skip commit + push. Artifact is written; user handles git manually | | `--no-push` | Commit OK, but skip push (the Stop hook always uses this) | | `--dry-run` | No files written, no git operations; print what would happen | | `--auto` | Non-interactive, auto-Y on commit confirmation. Intended for hooks | | `--non-interactive` | Without `--auto`: error. With `--auto`: run without any prompts | The pipeline script accepts the same flags directly (`node scripts/handoff-pipeline.mjs ...`) — useful for debugging without going through the skill. --- ## Workflow Examples ### Plugin work (auto-trigger) ``` cd plugins/llm-security # ... work until ~70% context ... # Stop hook fires automatically: # → writes plugins/llm-security/NEXT-SESSION-PROMPT.local.md # → stages ONLY the artifact (not other dirty files) # → commits with auto-generated Conventional Commits message # → does NOT push git push # when you are ready ``` ### Manual trigger with slug ``` /graceful-handoff refactor-auth --no-commit # → writes plugins//NEXT-SESSION-refactor-auth.local.md # → no git operations ``` ### New session ``` claude --resume # SessionStart hook auto-injects handoff content into context # Continue working immediately # The artifact is renamed to NEXT-SESSION-*.archived.local.md # so it cannot stale-load on a third session ``` ### Dry run before committing the workflow ``` /graceful-handoff --dry-run # Pipeline prints the JSON it would produce — file paths, commit message, # next steps — without writing anything or touching git ``` --- ## Safety Guarantees These properties are enforced by tests, not by convention: - **Push is never automatic.** The auto-execute path always passes `--no-push`. Irreversible operations stay in the user's hands. (`stop-context-monitor.test.mjs`) - **Staging is explicit.** The pipeline stages *only* the handoff artifact (and `REMEMBER.md` / `TODO.md` if present). `git add -A` is never used — a regression test (`pipeline never stages unrelated dirty files`) enforces this. - **Pre-commit hooks are respected.** The pipeline never bypasses with `--no-verify`. If a pre-commit hook (gitleaks, pathguard) blocks, the handoff fails and the user fixes the underlying issue. - **Artifacts are gitignored.** All output files match `*.local.md`, which existing repos in this marketplace already gitignore via `.gitignore` patterns. - **No network calls.** No WebSearch, no Agent delegation, no MCP. The pipeline is fully local. - **Bash sub-scoped.** Skill `allowed-tools` enumerates `Bash(git:*) Bash(jq:*) Bash(node:*) Bash(find:*) Bash(pwd:*)` — pre-approval is narrow even though it is not a sandbox. - **Lock file scoped to transcript directory.** Lock path is based on `dirname(transcript_path)`, not `cwd`, so it survives `cd` mid-session. --- ## Testing ```bash node --test 'plugins/graceful-handoff/tests/**/*.test.mjs' ``` 57 tests across 6 files: | File | Coverage | |------|----------| | `tests/skill-structure.test.mjs` | SKILL.md frontmatter, model pin, allowed-tools shape, removal of legacy `commands/` | | `tests/scripts/handoff-pipeline.test.mjs` | Pipeline JSON schema, idempotency, **no-staging-regression**, detached HEAD, no-upstream, interactive y/n | | `tests/hooks/statusline-monitor.test.mjs` | Threshold transitions, null payload, malformed JSON, no side effects | | `tests/hooks/stop-context-monitor.test.mjs` | 4-step context resolution, lock file behavior, stub-pipeline isolation, env-var failure modes | | `tests/hooks/session-start-load-handoff.test.mjs` | Source filter (`resume`/`compact` only), multi-level search, archive after read | | `tests/plugin-manifest.test.mjs` | Plugin.json schema, version pin, CHANGELOG entries | Stop-hook tests use a **stub pipeline** (a fake `handoff-pipeline.mjs` written into a temp dir) so test runs do not invoke real git operations against the marketplace repo. The pipeline runs in <8 s on a 2025 Mac. The full test suite runs in ~10 s. --- ## Limitations & Open Assumptions - **Token estimation drifts ±10%** against Claude's internal counting (steps 2–4 of the resolution chain). The 70% threshold is set conservatively to absorb this. Step 1 (`direct`) has no drift but requires the payload field to be present. - **Stop-hook payload schema is undocumented.** It is not officially confirmed that Stop payloads include `used_percentage` or `model.id` (statusLine payloads do). If both are missing, the resolver falls through to `default-1m`. The `[kilde: ]` label in `additionalContext` reveals which path was actually used — first real session reveals this. - **statusLine placement in `hooks/hooks.json` is an open assumption.** Smoke-test before relying on it; the fallback is to move it to global `~/.claude/settings.json`. - **Issue [#26251](https://github.com/anthropics/claude-code/issues/26251)** — `disable-model-invocation: true` may regress and block user-invocation in some Claude Code versions. Manual smoke-test before relying on it. - **Auto-execute does not push.** Irreversible operations stay user-triggered, by design. - **Compaction events are out of scope.** PreCompact fires too late (~95%) and is not configurable. The plugin targets the 60–70% window where the user can still benefit from a clean handoff. --- ## Version History | Version | Date | Highlights | |---------|------|------------| | **2.1.0** | 2026-05-01 | **Model-aware context window detection.** Replaces 200 K fallback with 4-step resolution chain (`used_percentage` → `payload-size` → `model-map` → 1 M default). Fixes 5–7× premature firing on Opus 4.7 (1 M window). All `additionalContext` messages include `[kilde: ]` for transparency. 6 new tests (57 total). | | **2.0.0** | 2026-05-01 | **Hard cut from `commands/` to `skills/`.** New deterministic pipeline (`handoff-pipeline.mjs`), three hooks (statusLine, Stop, SessionStart), `disable-model-invocation: true`, sub-scoped `allowed-tools`, explicit staging discipline (no more `git add -A`), pinned to Sonnet 4.6 (BREAKING). | | **1.0.0** | 2026-04-19 | Initial release — single declarative `/graceful-handoff` command, 6-phase prose workflow, three handoff types, pre-commit hook respect, <60 s time budget. | Full history in [`CHANGELOG.md`](CHANGELOG.md). --- ## License MIT. See [`LICENSE`](LICENSE). --- ## Feedback & Contributing - **Bug reports + feature requests:** open an issue on [Forgejo](https://git.fromaitochitta.com/open/ktg-plugin-marketplace) - **Pull requests:** not accepted on this repo (solo project, dialog-driven development with Claude Code). Fork freely if you need to extend. - **Marketplace:** part of [ktg-plugin-marketplace](https://git.fromaitochitta.com/open/ktg-plugin-marketplace) — see the [root README](../../README.md) for related plugins.