- JavaScript 100%
The regime-wide push-window restriction (man–tor 08:00–17:00, fre 08:00–16:00) was lifted permanently, so SKILL.md no longer gates the push on a weekday/time window — it would otherwise park a push in the middle of working hours. Two invariants are unchanged: push is Forgejo only (never GitHub), and push stays user-triggered (the skill is disable-model-invocation: true). The pipeline script had no window logic — only its header comment was corrected. If a future repo needs a window again, reintroduce it as per-repo config, never as a hardcoded default. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BcNX2QdgmXyLd2Bt6Z25GU |
||
|---|---|---|
| .claude-plugin | ||
| docs | ||
| scripts | ||
| skills/graceful-handoff | ||
| tests | ||
| .gitignore | ||
| CHANGELOG.md | ||
| CLAUDE.md | ||
| GOVERNANCE.md | ||
| LICENSE | ||
| README.md | ||
Graceful Handoff Plugin for Claude Code
One command to end a session cleanly and hand it to the next one —
/graceful-handoffreaches a natural stopping point and overwrites the nearestSTATE.mdwith 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 for the full model and what upstream provides.
AI-generated: all code produced by Claude Code through dialog-driven development. Full disclosure →
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?
- 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
- 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.
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
Prerequisites
- 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
claude plugin marketplace add https://git.fromaitochitta.com/open/ktg-plugin-marketplace.git
Or enable directly in ~/.claude/settings.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
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.
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.
Full history in CHANGELOG.md.
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.
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 root README for related plugins.