ktg-plugin-marketplace/plugins/graceful-handoff/README.md
Kjell Tore Guttormsen 490d4eddc6 docs: introduce GOVERNANCE.md and unify fork-and-own blurb
Establish a single governance document at marketplace root and copy
it into each of the 9 plugins so every plugin folder remains 100%
self-contained. Replace the inconsistent provocative blurb across
all READMEs with a uniform fork-and-own paragraph that links to
the local GOVERNANCE.md.

[skip-docs]

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-03 14:57:00 +02:00

18 KiB
Raw Permalink Blame History

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.

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 →

Version Platform Skill Hooks Pipeline Tests License

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?

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 6070% 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 executiongit 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 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

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
  }
}

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

flowchart TB
    subgraph Detection["Detection — display + auto-trigger"]
        direction LR
        SL["statusLine<br/>60% hint, 70% urgent"]
        SH["Stop hook<br/>≥70% estimated"]
    end

    subgraph Pipeline["Deterministic pipeline (Node, no LLM)"]
        direction LR
        P["handoff-pipeline.mjs<br/>classify → write → stage → commit"]
    end

    subgraph Resumption["Resumption — auto-load"]
        direction LR
        SS["SessionStart hook<br/>resume / compact only"]
        AR["Archive after read<br/>*.archived.local.md"]
    end

    subgraph Manual["Manual fallback"]
        direction LR
        SK["SKILL.md<br/>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, #27969, #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: <source>] so the source path is always visible.

Estimation drift: Steps 24 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: <transcript_dir>/.handoff-lock-<session_id> 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

---
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, 6069% 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-<slug>.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/<root>/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

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 24 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: <source>] 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 #26251disable-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 6070% 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_percentagepayload-sizemodel-map → 1 M default). Fixes 57× premature firing on Opus 4.7 (1 M window). All additionalContext messages include [kilde: <source>] 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.


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.