# Graceful Handoff Plugin for Claude Code > One command to end a session cleanly and hand it to the next one — `/graceful-handoff` reaches a natural stopping point and overwrites the nearest `STATE.md` with a complete, self-standing state-of-play. > **Solo-maintained, fork-and-own.** This plugin is a starting point, not a vendor product. Issues are welcome as signals; pull requests are not accepted. See [GOVERNANCE.md](GOVERNANCE.md) for the full model and what upstream provides. *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-3.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-0-lightgrey) ![Pipeline](https://img.shields.io/badge/STATE--helper-deterministic-cyan) ![Tests](https://img.shields.io/badge/tests-30-success) ![License](https://img.shields.io/badge/license-MIT-lightgrey) A Claude Code plugin that makes session handoff a single deliberate command, fully integrated with a `STATE.md`-based continuity system. When a session is filling up — or you simply want to stop — `/graceful-handoff` reaches a natural stopping point, **overwrites the nearest `STATE.md`** with where you are, the next concrete step, and what to read, then commits per your remote policy. The next session reads that `STATE.md` and stands on its own. --- ## Table of Contents - [What Is This?](#what-is-this) - [The Problem](#the-problem) - [The STATE.md Model](#the-statemd-model) - [Quick Start](#quick-start) - [Architecture](#architecture) - [The Handoff Ritual](#the-handoff-ritual) - [Components](#components) - [Remote Policy — tracked vs local-only](#remote-policy--tracked-vs-local-only) - [Commands & Arguments](#commands--arguments) - [Safety Guarantees](#safety-guarantees) - [Testing](#testing) - [What Changed in v3.0 (BREAKING)](#what-changed-in-v30-breaking) - [Limitations & Open Assumptions](#limitations--open-assumptions) - [License](#license) - [Feedback & Contributing](#feedback--contributing) --- ## What Is This? One skill — `/graceful-handoff` — plus a slim deterministic helper script. No hooks. Invoke it whenever you want to hand off. The session: 1. **Reaches the first natural stopping point** — finishes the current logical unit, leaves the code working. 2. **Overwrites the nearest `STATE.md`** with a complete state-of-play in a fixed format, headed by a mandatory `👉 NESTE — START HER` block (where we are + the next concrete step + what to read). 3. **Commits per remote policy** — staging *only* `STATE.md` (when tracked), never `git add -A`. 4. **Leaves push to you** — push to Forgejo, you-triggered (no window gate). The skill is `disable-model-invocation: true`: the model cannot trigger a handoff on its own. Handoff is a deliberate, user-initiated moment. --- ## The Problem Long sessions fill the context window. The classic failure mode is summarizing, committing, and writing a continuation prompt under time pressure — or skipping steps and losing continuity. Earlier versions of this plugin attacked that with auto-trigger hooks that wrote a separate `NEXT-SESSION-PROMPT` artifact. That turned out to be the wrong shape for a continuity system built on `STATE.md`: - A separate `NEXT-SESSION` file **is exactly the kind of local handover invention** a single-source continuity system forbids. `STATE.md` already *is* the handoff. - The valuable part of a handoff — *where are we, what's the next step, what should I read* — **requires the session's understanding**. A deterministic `git log` snapshot can't write it; only the model can. - An auto-trigger hook has no session context, so it could only ever produce a poor snapshot — and would **overwrite a good, model-written `STATE.md`**. v3.0 fixes the shape: the model writes `STATE.md`; the script does the deterministic mechanics around it. --- ## The STATE.md Model `STATE.md` is one layer of a three-layer continuity system: | Layer | Job | |-------|-----| | **`STATE.md`** | Current state-of-play *where you work*. Overwritten (not appended) at session end. The nearest one (cwd → repo root) is auto-injected at session start by a global hook. | | auto-memory **`MEMORY.md`** | Durable facts about the user and preferences. | | **`CLAUDE.md`** | Invariants — rules, commands. | Git history is the long-term log. This plugin owns the **write** side of `STATE.md` at session end. The **read** side (injecting the nearest `STATE.md` into a new session) is handled by the user's global `session-start.sh` — so the plugin ships no SessionStart loader of its own. ### Mandatory STATE.md format ```markdown # STATE — _Current state-of-play. Overwritten (not appended) at session end. History → git._ ## 👉 NESTE — START HER **** 1. **(you/me)** 2. **(you/me)** ## Mission & run-mode ## Gotchas / inviolable rules ## Push status ## Repo & env ## Session sequence (short — most of it lives in git) ``` The `👉 NESTE` block is always first, right after the title — it carries the actionable part so the operator never has to hunt for it. --- ## Quick Start ### Prerequisites - [Claude Code](https://docs.anthropic.com/en/docs/claude-code) v2.x+ - Node.js (any recent LTS — for the helper script) - A git repository (the helper detects detached HEAD / missing upstream and reports gracefully) ### 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 } } ``` ### Hand off ``` > /graceful-handoff ``` The session finishes the current unit, writes `STATE.md`, commits (if your remote policy allows), and prints a fixed closing line: **STATE.md status · next session's first action · commit/push status.** Start the next session and the nearest `STATE.md` is loaded into context automatically by your global session-start hook. --- ## Architecture ```mermaid flowchart TB User((user)) -->|/graceful-handoff| SK subgraph Skill["SKILL.md — model-driven ritual (full session context)"] SK["1. reach natural stopping point
3. WRITE STATE.md (👉 NESTE block)
7. fixed closing line"] end subgraph Helper["handoff-pipeline.mjs — deterministic, no LLM"] PLAN["--plan
resolve nearest STATE.md
classify remote · git facts"] COMMIT["--commit
stage ONLY STATE.md (+ --also)
never git add -A"] end SK -->|2. fetch facts| PLAN PLAN -->|JSON| SK SK -->|4. safe commit| COMMIT User -->|5. git push (Forgejo)| Done((done)) ``` Two pieces, one responsibility each: the **skill** synthesizes `STATE.md` (only it has the context); the **helper** does the deterministic mechanics (path resolution, remote classification, safe staging). The helper has no LLM dependencies and runs under `node:test` in seconds. --- ## The Handoff Ritual `SKILL.md` instructs the session to, in order: 1. **Reach the first natural stopping point.** Finish the current logical unit; leave the code working. Don't cut mid-change; don't start new work. 2. **`--plan`** — fetch deterministic facts (resolved `STATE.md` path, remote class, git status, recent commits). 3. **Write / overwrite `STATE.md`** in the mandatory format (title → `👉 NESTE` block → fixed sections → short history). Max ~60 lines. Overwrite, never append. 4. **`--commit`** — stage only `STATE.md` (when tracked) plus any explicit `--also` paths the model judges belong to the unit. Never `git add -A`. 5. **Push** — Forgejo only, never GitHub. Push is unconditional — no window gate. 6. **MEMORY.md check** — if it exceeds 200 lines, warn (don't auto-edit). 7. **Fixed closing line** — STATE.md status (path) · next session's first action (= the `👉 NESTE` block) · commit/push status. --- ## Components ### Skill — `skills/graceful-handoff/SKILL.md` ```yaml --- name: graceful-handoff description: Reach a natural stopping point, overwrite the nearest STATE.md (👉 NESTE block), commit per remote policy, write the fixed closing line. argument-hint: "[--no-commit] [--dry-run]" disable-model-invocation: true allowed-tools: Bash(git:*) Bash(node:*) Bash(date:*) Read Write Edit Glob --- ``` No `model:` pin — the skill inherits the session model, because writing a self-standing `STATE.md` is human-facing synthesis that warrants top-tier quality. > [!NOTE] > `allowed-tools` is *pre-approval*, not a sandbox. It removes permission prompts for the listed tools but does not block others. For real sandboxing, use project-level `permissions.deny` rules. ### Helper — `scripts/handoff-pipeline.mjs` Deterministic Node script, structured JSON output, no LLM dependencies: - **`--plan`** (default, read-only): resolve the nearest `STATE.md` (cwd → repo root), classify the `origin` remote, gather git facts, and report whether `STATE.md` is gitignored (the authoritative commit signal) plus a `leak_warning` on misconfiguration. - **`--commit`**: stage `STATE.md` *only* when it is tracked, plus explicit `--also ` paths; commit with a generated or supplied message. **Never `git add -A`. Never pushes.** Detached-HEAD guard. - **`--dry-run`**: emit the plan JSON, write nothing, touch no git. --- ## Remote Policy — tracked vs local-only `STATE.md` must never reach a public mirror. The helper classifies `origin`: | Remote | Class | STATE.md | |--------|-------|----------| | `github.com` / `github.io` | public | local-only (gitignored), never committed | | `…/open/` (public Forgejo mirror) | public | local-only (gitignored), never committed | | `…/ktg/` (private Forgejo) | private | tracked + committed | | no remote | none | local-only | The authoritative commit decision is `git check-ignore STATE.md` — so a misclassified remote can never cause a leak; it only produces a `leak_warning` when the remote looks public but `STATE.md` is *not* gitignored. **This plugin's own repo has an `open/` remote, so its `STATE.md` is local-only.** --- ## Commands & Arguments ``` /graceful-handoff [flags] ``` | Argument | Description | |----------|-------------| | `--no-commit` | Write `STATE.md`, skip commit/push (you handle git manually) | | `--dry-run` | Write nothing, no git — show the `--plan` JSON for inspection | The helper accepts the same modes directly (`node scripts/handoff-pipeline.mjs --plan` / `--commit` / `--dry-run`) — useful for debugging without going through the skill. --- ## Safety Guarantees Enforced by tests, not convention: - **Staging is explicit.** `--commit` stages *only* `STATE.md` (plus explicit `--also` paths). `git add -A` is never used — a regression test (`never stages unrelated dirty files`) enforces this. - **STATE.md never leaks to a public mirror.** On a public/`open/` remote `STATE.md` is gitignored and the helper refuses to commit it (`local-only-skipped`), verified by test. - **Push is never automatic.** The helper never pushes; push stays user-triggered (no window gate); never automatic. - **Pre-commit hooks are respected.** The helper never uses `--no-verify`. - **No network calls.** No WebSearch, no Agent delegation, no MCP — fully local. - **Bash sub-scoped.** Skill `allowed-tools` enumerates `Bash(git:*) Bash(node:*) Bash(date:*)`. --- ## Testing ```bash node --test 'tests/**/*.test.mjs' ``` 30 tests across 3 files: | File | Coverage | |------|----------| | `tests/skill-structure.test.mjs` | SKILL.md frontmatter (no `model:` pin, `Write` in allowed-tools, `disable-model-invocation`), `👉 NESTE` mandate, removal of `commands/` and `hooks/` | | `tests/scripts/handoff-pipeline.test.mjs` | `--plan`/`--commit`/`--dry-run`, nearest-STATE resolution, remote classification, **no-`git add -A` regression**, gitignored-STATE skip, detached HEAD | | `tests/plugin-manifest.test.mjs` | plugin.json version + description, CHANGELOG `[3.0.0]` BREAKING + preserved history | The suite runs in a few seconds — no LLM involvement. --- ## What Changed in v3.0 (BREAKING) - **Writes `STATE.md`, not a `NEXT-SESSION` artifact.** Full integration with the three-layer continuity system; the separate handover file is gone. - **All three hooks removed.** Stop auto-trigger, statusLine hint, and SessionStart loader deleted — redundant (global hook already injects `STATE.md`), dead (user statusLine wins), or harmful (would overwrite a good `STATE.md`). - **Inverted architecture.** The model writes `STATE.md`; the script is a slim deterministic helper. - **Remote-aware tracked/local-only policy** so `STATE.md` never leaks to a public mirror. - **Removed the Sonnet pin** — handoff synthesis inherits the session model. Full history in [`CHANGELOG.md`](CHANGELOG.md). --- ## Limitations & Open Assumptions - **`disable-model-invocation: true`** has open issue [#26251](https://github.com/anthropics/claude-code/issues/26251) — it may regress and block user-invocation in some Claude Code versions. Smoke-test after install. - **Remote classification is heuristic** (`open/` / `github` → public). The authoritative commit decision is `git check-ignore STATE.md`, so misclassification cannot cause a leak — only an advisory `leak_warning`. - **STATE.md read-side depends on the global session-start hook.** This plugin only writes; injecting the nearest `STATE.md` at session start is the user's global `~/.claude/hooks/session-start.sh`. --- ## 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.