- JavaScript 100%
Ran the repo-standard gate (plugin class): 5 ERROR + 2 WARN + 2 SKIP. Now 0 ERROR / 0 WARN / 0 SKIP, 13 checks passing. Docs only — no plugin code, version or behaviour changed. - H1 is the repo name; opening line is verbatim the forge description, so description == catalog == README is machine-checkable. - `## Install` promoted to a top-level heading on the first screen and given the CLI command `claude plugin install graceful-handoff@ktg-plugin-marketplace` next to `marketplace add` (the enabledPlugins JSON stays as a second form). Prerequisites split out as `## Requirements`; Quick Start keeps the usage. - Added `## Non-goals` (what it deliberately does not do) and `## Changelog`. - Dropped the static tests-30 badge: no CI runner exists on this forge, so it asserted a run nothing verifies. The Testing section now states the command and that nothing runs it automatically. - Fixed two dead `../../README.md` references inherited from the monorepo — both point outside a standalone repo and 404. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019x1C8tjmB2vUCM9jE668ZF |
||
|---|---|---|
| .claude-plugin | ||
| docs | ||
| scripts | ||
| skills/graceful-handoff | ||
| tests | ||
| .gitignore | ||
| CHANGELOG.md | ||
| CLAUDE.md | ||
| GOVERNANCE.md | ||
| LICENSE | ||
| README.md | ||
graceful-handoff
One-command session handoff into the STATE.md continuity system. Overwrites the nearest STATE.md with a complete state-of-play and commits per remote policy.
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 for the full model and what upstream provides.
AI-generated: all code produced by Claude Code through dialog-driven development. Every change is human-directed, reviewed, and validated before commit.
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.
Install
claude plugin marketplace add https://git.fromaitochitta.com/open/ktg-plugin-marketplace.git
claude plugin install graceful-handoff@ktg-plugin-marketplace
Or enable directly in ~/.claude/settings.json:
{
"enabledPlugins": {
"graceful-handoff@ktg-plugin-marketplace": true
}
}
Requirements
- 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)
Table of Contents
- What Is This?
- Non-goals
- The Problem
- The STATE.md Model
- Quick Start
- Architecture
- The Handoff Ritual
- Components
- Remote Policy — tracked vs local-only
- Commands & Arguments
- Safety Guarantees
- Testing
- What Changed in v3.0 (BREAKING)
- Limitations & Open Assumptions
- Changelog
- License
- 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:
- Reaches the first natural stopping point — finishes the current logical unit, leaves the code working.
- Overwrites the nearest
STATE.mdwith a complete state-of-play in a fixed format, headed by a mandatory👉 NESTE — START HERblock (where we are + the next concrete step + what to read). - Commits per remote policy — staging only
STATE.md(when tracked), nevergit add -A. - 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.
Non-goals
What this plugin deliberately does not do — read this before adopting it:
- It never triggers itself. No hooks, no Stop auto-trigger, no context-percentage threshold. If you want a handoff written automatically when a session fills up, this is the wrong plugin — v2 did that and v3 removed it on purpose.
- It does not read
STATE.mdback. Injecting the nearestSTATE.mdat session start is the job of a globalsession-start.shhook you own. This plugin only writes. - It does not push. The helper has no push path at all; pushing stays a separate, user-triggered step.
- It is not a general-purpose commit tool.
--commitstagesSTATE.mdplus explicitly named--alsopaths, and nothing else. Unrelated dirty files stay dirty — by design. - It does not edit
MEMORY.mdorCLAUDE.md. It warns whenMEMORY.mdgrows past 200 lines; the edit is yours. - It does not publish anything.
STATE.mdis internal state-of-play, never a public artifact — on a public remote it stays gitignored and uncommitted.
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-SESSIONfile is exactly the kind of local handover invention a single-source continuity system forbids.STATE.mdalready 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 logsnapshot 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
# STATE — <name>
_Current state-of-play. Overwritten (not appended) at session end. History → git._
## 👉 NESTE — START HER
**<one line: where we are + the next concrete action>**
1. **(you/me)** <concrete step>
2. **(you/me)** <concrete step>
<pointers: which files / plans / memory MUST be read to stand on its own>
## 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
Installed? Then the whole workflow is one command:
> /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
flowchart TB
User((user)) -->|/graceful-handoff| SK
subgraph Skill["SKILL.md — model-driven ritual (full session context)"]
SK["1. reach natural stopping point<br/>3. WRITE STATE.md (👉 NESTE block)<br/>7. fixed closing line"]
end
subgraph Helper["handoff-pipeline.mjs — deterministic, no LLM"]
PLAN["--plan<br/>resolve nearest STATE.md<br/>classify remote · git facts"]
COMMIT["--commit<br/>stage ONLY STATE.md (+ --also)<br/>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:
- 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.
--plan— fetch deterministic facts (resolvedSTATE.mdpath, remote class, git status, recent commits).- Write / overwrite
STATE.mdin the mandatory format (title →👉 NESTEblock → fixed sections → short history). Max ~60 lines. Overwrite, never append. --commit— stage onlySTATE.md(when tracked) plus any explicit--alsopaths the model judges belong to the unit. Nevergit add -A.- Push — Forgejo only, never GitHub. Push is unconditional — no window gate.
- MEMORY.md check — if it exceeds 200 lines, warn (don't auto-edit).
- Fixed closing line — STATE.md status (path) · next session's first action (= the
👉 NESTEblock) · commit/push status.
Components
Skill — skills/graceful-handoff/SKILL.md
---
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-toolsis pre-approval, not a sandbox. It removes permission prompts for the listed tools but does not block others. For real sandboxing, use project-levelpermissions.denyrules.
Helper — scripts/handoff-pipeline.mjs
Deterministic Node script, structured JSON output, no LLM dependencies:
--plan(default, read-only): resolve the nearestSTATE.md(cwd → repo root), classify theoriginremote, gather git facts, and report whetherSTATE.mdis gitignored (the authoritative commit signal) plus aleak_warningon misconfiguration.--commit: stageSTATE.mdonly when it is tracked, plus explicit--also <path>paths; commit with a generated or supplied message. Nevergit 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/<repo> (public Forgejo mirror) |
public | local-only (gitignored), never committed |
…/ktg/<repo> (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.
--commitstages onlySTATE.md(plus explicit--alsopaths).git add -Ais never used — a regression test (never stages unrelated dirty files) enforces this. - STATE.md never leaks to a public mirror. On a public/
open/remoteSTATE.mdis 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-toolsenumeratesBash(git:*) Bash(node:*) Bash(date:*).
Testing
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. Nothing runs it automatically: this forge has no CI runner, so the command above from a clean clone is the only verification. That is also why this README carries no test badge — a static one would assert a run nobody performed.
What Changed in v3.0 (BREAKING)
- Writes
STATE.md, not aNEXT-SESSIONartifact. 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 goodSTATE.md). - Inverted architecture. The model writes
STATE.md; the script is a slim deterministic helper. - Remote-aware tracked/local-only policy so
STATE.mdnever leaks to a public mirror. - Removed the Sonnet pin — handoff synthesis inherits the session model.
Limitations & Open Assumptions
disable-model-invocation: truehas open issue #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 isgit check-ignore STATE.md, so misclassification cannot cause a leak — only an advisoryleak_warning. - STATE.md read-side depends on the global session-start hook. This plugin only writes; injecting the nearest
STATE.mdat session start is the user's global~/.claude/hooks/session-start.sh.
Changelog
See CHANGELOG.md.
License
MIT. See LICENSE.
Feedback & Contributing
- Bug reports + feature requests: open an issue on Forgejo
- 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 — see the catalog for related plugins.