Shows context/rate-limit usage in the status line and checkpoints work for the next session when a limit gets close

Wanyaldee の個人スキルパッケージ。Fable 5級のコーディング規律、開発哲学、Fable 5 プロンプティングリファレンス、メモリ規律+監査、認証情報読み取りブロックのフック、Windows→WSL/SSH設定同期フックを収録。
v1.x までは fable-coding という名前だった(旧 URL は GitHub がリダイレクトする)。v2.0.0 で改名。旧名でインストール済みの環境は、/plugin uninstall fable-coding → /plugin marketplace remove fable-coding → 下記の手順で入れ直す。
Claude Code内で:
/plugin marketplace add Wanyaldee/wanyaldee-skills
/plugin install wanyaldee-skills@wanyaldee-skills
インストール後、各スキルがタスク内容に応じて自動的に候補になる(コーディングタスク→fable-coding、設計提案→dev-philosophy、メモリ保存→memory-discipline 等)。明示的に使う場合はプロンプトでスキル名を指示する。/memory-audit はコマンドとして直接呼べる。
fable-coding — Fable 5級のコーディング規律(計画→根本原因→最小差分→検証→ADR)。v1.4.0で公式プロンプティングガイド由来の行動規則(過剰計画の抑制、ツール結果に基づく進捗報告、assessとfixの境界、最終サマリの可読性、途中終了の禁止)を統合。v1.5.0で @armadillo_ai の7則(完了の機械的定義、複数解釈の確認、ついで改善禁止、証拠付き検証報告、修正2回まで、初見セルフレビュー、確信度+3点報告)を統合。v2.4.0でディレクトリ README の規約(ファイル命名・配置・登録規則)を計画の要件として読む規則を追加(ADR 0011)。v2.5.0で Opus 5 の Claude Code ハーネスが自ら述べている規則(行動の即応性、並列検索、周囲コードへの追従、ミスの率直な承認、結果の忠実報告)を削除して重複を解消(ADR 0012)。v2.7.0でADRフォーマットの記述を writing-adrs スキルへのクロスリファレンスに置き換え(ADR 0014)。v2.8.0で Fable 5.1 ハーネスが自ら述べている規則(結論先出し、file:line 参照、最終サマリの可読性、書式より散文、自力での情報収集、停止条件、assessとfixの境界、状態変更前の証拠確認、意図表明での終了禁止)10項目を削除(ADR 0015)。description をワークフロー要約から発火条件のみに書き換え。writing-adrs (v2.7.0〜) — ADR(Architecture Decision Record)の書き方: Context・Decision・Reason・Alternatives rejected・Consequences・Verification の6節構成。核は Reason 節 —— 実際のコードを掲載した上で、値・アルゴリズム・順序などの個別選択を具体的に説明することを必須とし、「信頼性のため」等のあやふやな一般論を明示的なアンチパターンとして禁止する。AIによってコーディングの負担が軽くなった分、書いた本人がそのコードを説明できなければ責任を取れない、という考え方に基づく(ADR 0014)。prompting-fable-5 (v1.4.0〜) — Fable 5 / Mythos 5 を「使う側」のリファレンス: effort設定、公式スニペット集、サブエージェント/メモリ/send_to_userのスキャフォールディング、refusalフォールバック、旧モデルからのプロンプト移行チェックリスト。Fable 5向けのプロンプト・エージェント・スキルを書くときに発火する。v2.8.0で Fable 5.1 / Mythos 5.1 に対応(ADR 0015): 「Fable 5.1 — what changed」節を追加し、強制ツール使用の400化、履歴のappend-only必須(2026-08-31以降作成のアカウントで thinking ブロックが会話に束縛される)、進捗更新の減少と thinking.display: "updates"、エージェントループでのツール呼び出しバッチ化、散文の高密度化、チャットでの書式不足(旧モデル向けの反書式指示は削除すべき)、引用の無標化、ファイル全体書き換え、低effortでの検索抑制、xhigh/maxでの長文出力、スコープ・テストの過剰、安全分類器の誤検知3パターンを、Anthropicのスニペットを逐語で収録。フォールバック先を Opus 4.8 と Opus 5 の2つに訂正し、max effort と「effortの名前はモデル間で同じ思考量に対応しない」旨を追加。memory-discipline (v1.6.0〜) — 永続オートメモリの規範: origin(モデル+絶対日付)の記録、絶対日付の徹底、索引の同ターン更新、耐久的な事実と揮発的な状態の分離、担当者付きの未確定事項、矛盾に気づいたセッションでの即時修正。2026-07-04 の全プロジェクトメモリ監査の知見を規範化したもの。v2.8.0で Fable 5.1 ハーネスの Memory 節が述べている内容を削除し、差分だけを残す形に縮約(499語→355語、ADR 0015)。memory-audit (v1.7.0〜) — /memory-audit で全プロジェクトのメモリを一発監査: 索引整合、モデル帰属(originSessionId→セッションログ解決)、矛盾・重複・揮発性混在・形式のチェック。デフォルト報告のみ、--fix で機械的修正(索引補完・origin 補記・日付の絶対化)だけ適用。v2.8.0で description を発火条件のみに書き換え(ADR 0015)。injection-vigilance (v2.3.0〜) — プロンプトインジェクション警戒: 指示の出所はユーザー発言とシステム設定のみで、ツールで読んだものはすべてデータ。過去の Claude セッション自身の出力経由の注入(セルフインジェクション)も対象。データ内の指示めいたテキストは実行せず、位置つきで引用報告してユーザーに判断させる。ネスト claude -p での RED/GREEN テスト済み(ADR 0010)。dev-philosophy (v1.3.0〜) — 開発哲学: 自動化の境界線(ヒューマンインザループ、外部信頼サービス優先)、仕組みによるセキュリティ、技術スタック方針(Python/Rust、MariaDB、Proxmox)、MIT License 標準。システム設計・アーキテクチャ提案時に適用される。v2.8.0で description を発火条件のみに書き換え(ADR 0015)。remote-config-sync (v2.6.0〜) — Windowsマシンを正本として ~/.claude/settings.json をWSL/SSH先へ自動プッシュする SessionStart フック。リモート側のハッシュが異なる場合のみ上書きし(旧設定は .bak.<timestamp> に退避)、更新したターゲットのみ報告。同期対象は ~/.claude/remote-sync-targets.conf(リポジトリ非追跡、ユーザー個人管理)で指定。既知の制限として、このマシンを経由せずWSL/SSH側で直接セッションを開始した場合は発火しない。また前提条件として、リモート側にClaude Codeと本プラグインが事前導入されている必要がある(ADR 0013)。英語スキル(fable-coding, prompting-fable-5, injection-vigilance, memory-discipline, memory-audit, remote-config-sync, writing-adrs)には人間用の和訳 SKILL.ja.md を併設している(v1.4.0〜)。dev-philosophy は本文が日本語のため和訳を持たない。Claude が読み込むのは SKILL.md(英語版)のみ。編集は英語版に行い、和訳を追随させる。
SKILL.md は Claude Code 専用の形式(name/description の frontmatter を持ち、Claude が状況に応じて能動的に Skill ツールで呼び出す)であり、/plugin install によるパッケージ配布も Claude Code にしか通用しない。Codex CLI や Cursor など AGENTS.md 規約に従う他エージェントは、この呼び出し機構もインストール機構も持たず、リポジトリ直下の AGENTS.md を常時受動的に読み込むだけである。
そのため、汎用的にどのエージェントにも効く4スキル(fable-coding / dev-philosophy / writing-adrs / injection-vigilance)に限り、AGENTS.md 形式でも提供している。memory-discipline / memory-audit(Claude Codeのオートメモリ機能が前提)と remote-config-sync(Claude CodeのSessionStartフックが前提)はClaude Code固有の仕組みに依存するため対象外(ADR 0017)。
AGENTS.md — 上記4スキルを集約した単一ファイル。skills/<name>/AGENTS.md(fable-coding / dev-philosophy / writing-adrs / injection-vigilance の4箇所)— スキル単体の断片。1スキルだけ他プロジェクトへ持ち込みたい場合はこちらをコピーする。他エージェントでの使い方(/plugin install に相当する導入手順が無いため):
git clone https://github.com/Wanyaldee/wanyaldee-skills
してから、上記いずれかのファイルを対象プロジェクトの AGENTS.md としてコピー(または既存の AGENTS.md に追記)する。fable-coding の §8(スタック方針)や dev-philosophy の技術スタック選定はこのユーザー個人の既定値であり普遍的な標準ではないため、持ち込み先プロジェクトの実情に合わせて書き換えること。
SKILL.md が正本、AGENTS.md は追随して編集する派生物という位置づけは SKILL.ja.md と同じ(編集は SKILL.md に行い、AGENTS.md/SKILL.ja.md をそれぞれ追随させる)。
動作確認の範囲: メンテナーは普段 Claude Code のみを使用しており、Codex CLI や Cursor など他エージェントでの実地テストは行っていない。想定通りに動かない、指示の解釈が食い違う等に気づいた場合は GitHub Issue で報告してほしい。MIT License で配布している OSS なので、直してほしい内容が明確なら Issue を待つより自分で直した方が早いはず — フォークして修正・PR も歓迎する。
Claude Code の function hooks で書いた Mod。スキル本体とは独立した別プラグインとして同じマーケットプレイスに収録している(ソースは mods/context-handoff/)。
/plugin install context-handoff@wanyaldee-skills
ctx 42% (84k/200k) · 5h 63% · 7d 20% を表示。ターン終了ごと、およびレート制限の窓が1ポイント動くたびに更新。レート制限はサブスクリプション利用時のみ表示される。<repo>/.claude/handoff.md に書き出すgit add -A → chore: handoff checkpoint before token limit でコミット[続行する](y)で引き継ぎメモ付きの続きの依頼を送信、[破棄する](n)で記録を削除。答えるまで消えない。/handoff で手動の引き継ぎも可能。注意: チェックポイントは現在のブランチに、.gitignore 対象外の未追跡ファイルも含めてコミットする。判定はターン終了時のみなので、1ターンの途中で 100% に達した場合は間に合わない。詳細は ADR 0018。
プラグインを有効化すると PreToolUse フックが自動で登録され、Claude が以下のファイルを Read ツールで読むことを拒否する:
.env / .env.*(ただし .env*.example / .env*.sample / .env*.template は必要なキー名の把握のため読み取り可、v2.1.0〜)*.pem / id_rsa / id_ed25519credentials.json / .aws/ 配下加えて Bash 用フック(v2.2.0〜)が以下のコマンドを拒否する:
.env 系ファイルへの言及(テンプレートは除外)— cat .env などmkfs / dd of=/dev/*rm -rf のうち、対象を事前に提示できないもの — グロブ(build/*)、変数・コマンド置換($DIR、` pwd )、. / ..、/ と最上位パス、ホームディレクトリ自身、/etc /usr /root などシステム配下、/var /tmp /home 等の完全一致、xargs/find -exec` 経由、引用符が壊れて解析できないものmysql / mariadb / psql / sqlite3 / mongosh / redis-cli / wrangler d1 execute|migrations apply。実行するステートメントはユーザーに報告し、ユーザー自身が実行する(fable-coding セクション4の仕組み化)。v2.8.0〜、rm -rf は一律拒否ではなくなった(ADR 0016)。対象がすべて保護対象外のリテラルなパスであれば、拒否ではなく確認プロンプト(permissionDecision: "ask")を出し、削除対象のパスを示した上でユーザーが承認・却下する。完了済み worktree や node_modules の掃除がこれで通る。上の一覧にある「対象を事前に提示できないもの」は従来どおり拒否のままで、1つのコマンド行に両方が含まれる場合は拒否が優先する。あわせて、パス付きバイナリ(/bin/rm -rf /)が v2.2.0〜v2.7.0 のフックをすり抜けていた穴を修正した。
制限: 事故防止のガードであり敵対的境界ではない(詳細は ADR 0001 / 0009 / 0016)。念のため settings.json の permissions.deny に Read(**/.env) 等を併設しておくとフック無効時の保険になる。
MIT
hooks/register.tsx 173 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register, SessionContextUsage, SessionRateLimit } from 'claude-code'
3
4import type { Handoff } from '../types'
5import {
6 HANDOFF_FILE,
7 SUMMARY_PROMPT,
8 commitMessage,
9 formatStatus,
10 handoffMarkdown,
11 limitReason,
12 resumePrompt,
13 resumeQuestion,
14 storeKey,
15 wrapUpNote,
16} from './logic'
17
18const pending = atom({ plugin: 'context-handoff', key: 'pending' } as const, null)
19const hasSavedThisSession = atom({ plugin: 'context-handoff', key: 'hasSavedThisSession' } as const, false)
20
21const FALLBACK_PROMPTS = 3
22
23async function git($: EngineInterface, args: string[], cwd: string): Promise<{ isOk: boolean; out: string }> {
24 try {
25 const ran = await $.process.run(['git', ...args], { cwd })
26 return { isOk: ran.exitCode === 0, out: ran.stdout.trim() }
27 } catch (error) {
28 $.ui.log(`context-handoff: git ${args[0]} failed: ${String(error)}`)
29 return { isOk: false, out: '' }
30 }
31}
32
33async function repoRootOf($: EngineInterface): Promise<string> {
34 const cwd = await $.session.cwd()
35 const root = await git($, ['rev-parse', '--show-toplevel'], cwd)
36
37 return root.isOk && root.out !== '' ? root.out : cwd
38}
39
40/** Asks the session itself what it was doing; falls back to the last prompts typed. */
41async function summarize($: EngineInterface): Promise<string> {
42 const reply = await $.model.fork({ prompt: SUMMARY_PROMPT })
43 if (reply.isAnswered && reply.text.trim() !== '') {
44 return reply.text.trim()
45 }
46 $.ui.log(`context-handoff: summary fork gave no text (${reply.isAnswered ? 'empty' : reply.reason}), using recent prompts`)
47 const messages = await $.session.messages()
48 const prompts = messages
49 .filter(message => message.role === 'user' && message.text.trim() !== '')
50 .slice(-FALLBACK_PROMPTS)
51 .map(message => `- ${message.text.trim().slice(0, 500)}`)
52
53 return ['## 直近の依頼 (自動要約に失敗したため原文)', ...prompts].join('\n')
54}
55
56/** Stages everything .gitignore allows and commits it; answers the short sha, or '' when nothing was committed. */
57async function commitAll($: EngineInterface, repoRoot: string, reason: string): Promise<string> {
58 const isRepo = (await git($, ['rev-parse', '--is-inside-work-tree'], repoRoot)).isOk
59 if (!isRepo) {
60 return ''
61 }
62 await git($, ['add', '-A'], repoRoot)
63 const hasStaged = !(await git($, ['diff', '--cached', '--quiet'], repoRoot)).isOk
64 if (!hasStaged) {
65 return ''
66 }
67 const committed = await git($, ['commit', '-m', commitMessage(reason)], repoRoot)
68 if (!committed.isOk) {
69 $.ui.toast('context-handoff: チェックポイントのコミットに失敗しました (pre-commit hook 等)。変更はステージ済みです')
70 return ''
71 }
72
73 return (await git($, ['rev-parse', '--short', 'HEAD'], repoRoot)).out
74}
75
76async function checkpoint($: EngineInterface, reason: string): Promise<Handoff> {
77 await update($, hasSavedThisSession, () => true)
78 $.ui.toast(`${reason}。作業を記録してコミットします…`)
79
80 const repoRoot = await repoRootOf($)
81 const branch = (await git($, ['rev-parse', '--abbrev-ref', 'HEAD'], repoRoot)).out || '(なし)'
82 const createdAt = new Date(await $.clock.now()).toISOString()
83 const summary = await summarize($)
84 const draft: Handoff = { repoRoot, branch, commit: '', reason, summary, createdAt }
85
86 await $.fs.write(`${repoRoot}/${HANDOFF_FILE}`, handoffMarkdown(draft))
87 const handoff: Handoff = { ...draft, commit: await commitAll($, repoRoot, reason) }
88 await $.store.set(storeKey(repoRoot), handoff)
89
90 await $.session.append({ message: { type: 'user', content: [{ type: 'text', text: wrapUpNote(handoff) }] } })
91 $.ui.toast(`引き継ぎを保存しました (${handoff.commit || 'コミットなし'})。次回起動時に続行を確認します`)
92
93 return handoff
94}
95
96function showUsage($: EngineInterface, context: SessionContextUsage, rateLimits: readonly SessionRateLimit[]) {
97 $.ui.status(formatStatus(context, rateLimits))
98}
99
100async function settle($: EngineInterface, handoff: Handoff) {
101 await $.store.delete(storeKey(handoff.repoRoot))
102 await update($, pending, () => null)
103}
104
105export const register: Register = on => {
106 on('session.start', async ($, e, next) => {
107 await $.command.register({
108 name: 'handoff',
109 description: '今の作業を記録・コミットし、次のセッションへ引き継ぐ',
110 })
111 const usage = await $.session.usage()
112 showUsage($, usage.context, usage.rateLimits)
113
114 // A hot reload fires session.start again: never offer this session's own handoff back to it.
115 const isOwnHandoff = await read($, hasSavedThisSession)
116 if (e.isInteractive && !isOwnHandoff) {
117 const saved = (await $.store.get(storeKey(await repoRootOf($)))) as Handoff | undefined
118 if (saved !== undefined) {
119 await update($, pending, () => saved)
120 $.ui.toast(resumeQuestion(saved))
121 }
122 }
123
124 return next(e)
125 })
126
127 on('session.measure', async ($, e, next) => {
128 showUsage($, e.context, e.rateLimits)
129 const reason = limitReason(e.context, e.rateLimits)
130 if (reason !== null && !(await read($, hasSavedThisSession))) {
131 await checkpoint($, reason)
132 }
133
134 return next(e)
135 })
136
137 on('command.run', { command: 'handoff' }, async $ => {
138 const handoff = await checkpoint($, '手動で引き継ぎを要求')
139
140 return { text: `引き継ぎを ${handoff.repoRoot}/${HANDOFF_FILE} に保存しました (${handoff.commit || 'コミットなし'})。` }
141 })
142
143 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
144 const handoff = await read($, pending)
145 if (handoff === null) {
146 return next(e)
147 }
148 const { Box, Button, Text } = $.ui.resolve(e)
149
150 return (
151 <Box flexDirection="column" borderStyle="round" paddingX={1}>
152 <Text bold>{resumeQuestion(handoff)}</Text>
153 <Text dimColor>
154 {handoff.reason} · {handoff.branch} · {handoff.commit || 'コミットなし'} · {handoff.createdAt}
155 </Text>
156 <Box>
157 <Button
158 key="resume"
159 label="続行する"
160 hotkey="y"
161 variant="primary"
162 onPress={async () => {
163 await settle($, handoff)
164 await $.prompt.submit({ text: resumePrompt(handoff), asUser: true })
165 }}
166 />
167 <Button key="discard" label="破棄する" hotkey="n" onPress={() => settle($, handoff)} />
168 </Box>
169 </Box>
170 )
171 })
172}
173hooks/logic.ts 91 lines1import type { SessionContextUsage, SessionRateLimit } from 'claude-code'
2
3import type { Handoff } from '../types'
4
5export const CONTEXT_THRESHOLD = 90
6export const RATE_LIMIT_THRESHOLD = 90
7export const HANDOFF_FILE = '.claude/handoff.md'
8
9const LIMIT_LABELS: Record<string, string> = {
10 five_hour: '5h',
11 seven_day: '7d',
12 spend_limit: 'spend',
13}
14
15export const SUMMARY_PROMPT = [
16 'トークン制限が近いため、この作業を次のエージェントに引き継ぎます。',
17 '次のエージェントが会話履歴なしで作業を再開できるよう、日本語のMarkdownで以下を簡潔にまとめてください。',
18 '見出しは「## 目的」「## 完了したこと」「## 残っていること」「## 次の一手」「## 関連ファイル」の5つ。',
19 '推測は推測と明記し、まとめ以外の前置きは書かないでください。',
20].join('\n')
21
22const formatTokens = (n: number): string => (n >= 1000 ? `${Math.round(n / 1000)}k` : String(n))
23
24export const limitLabel = (kind: string): string => LIMIT_LABELS[kind] ?? kind
25
26export function formatStatus(context: SessionContextUsage, rateLimits: readonly SessionRateLimit[]): string {
27 const ctx =
28 context.percent === undefined || context.tokens === undefined
29 ? `ctx --/${formatTokens(context.window)}`
30 : `ctx ${context.percent}% (${formatTokens(context.tokens)}/${formatTokens(context.window)})`
31 const limits = rateLimits.map(limit => `${limitLabel(limit.kind)} ${limit.percentUsed}%`)
32
33 return [ctx, ...limits].join(' · ')
34}
35
36/** Why a handoff is due, or null while every figure is under its threshold. */
37export function limitReason(context: SessionContextUsage, rateLimits: readonly SessionRateLimit[]): string | null {
38 const hot = rateLimits.find(limit => limit.percentUsed >= RATE_LIMIT_THRESHOLD)
39 if (hot !== undefined) {
40 return `レート制限 ${limitLabel(hot.kind)} が ${hot.percentUsed}% に到達`
41 }
42 if (context.percent !== undefined && context.percent >= CONTEXT_THRESHOLD) {
43 return `コンテキストが ${context.percent}% に到達`
44 }
45
46 return null
47}
48
49export const storeKey = (repoRoot: string): string => `handoff:${repoRoot}`
50
51export const commitMessage = (reason: string): string =>
52 `chore: handoff checkpoint before token limit\n\n${reason}. Work in progress, see ${HANDOFF_FILE}.`
53
54export function handoffMarkdown(handoff: Handoff): string {
55 return [
56 '# 作業引き継ぎ (context-handoff)',
57 '',
58 `- リポジトリ: ${handoff.repoRoot}`,
59 `- ブランチ: ${handoff.branch}`,
60 `- 理由: ${handoff.reason}`,
61 `- 記録日時: ${handoff.createdAt}`,
62 '',
63 handoff.summary,
64 '',
65 ].join('\n')
66}
67
68export const resumeQuestion = (handoff: Handoff): string =>
69 `このリポジトリ (${handoff.repoRoot}) でこの作業をやっていましたがトークン切れで作業を一時中断しました、続行しますか`
70
71export function resumePrompt(handoff: Handoff): string {
72 const commitLine =
73 handoff.commit === '' ? 'チェックポイントのコミットはありません。' : `チェックポイントのコミット: ${handoff.commit}`
74
75 return [
76 '前のセッションがトークン制限で中断した作業の続きをお願いします。',
77 `ブランチ ${handoff.branch}。${commitLine}`,
78 `引き継ぎメモ (${HANDOFF_FILE} と同じ内容):`,
79 '',
80 handoff.summary,
81 '',
82 `まず git log と git status で状態を確認してから「次の一手」から再開してください。作業が終わったら ${HANDOFF_FILE} を削除してください。`,
83 ].join('\n')
84}
85
86export const wrapUpNote = (handoff: Handoff): string =>
87 [
88 `[context-handoff] ${handoff.reason}。作業内容を ${HANDOFF_FILE} に記録し、チェックポイントをコミットしました (${handoff.commit || 'コミットなし'})。`,
89 '新しい大きな作業は始めず、今のステップを区切りのよいところで終え、ユーザーに中断を報告してください。',
90 ].join('\n')
91types/index.d.ts 18 lines1export type Handoff = {
2 repoRoot: string
3 branch: string
4 commit: string
5 reason: string
6 summary: string
7 createdAt: string
8}
9
10declare module 'claude-code' {
11 interface PluginState {
12 'context-handoff': {
13 pending: Handoff | null
14 hasSavedThisSession: boolean
15 }
16 }
17}
18