ponytailエージェントに「書かない」を教える ruleset の設計を読む
エージェントにコードを書かせていると、頼んだ以上のものが返ってくることがあります。日付の入力欄が欲しいと言えば flatpickr を入れ、ラッパーコンポーネントを書き、スタイルシートを足し、タイムゾーンの扱いについて相談を始める。動くものは出てきますが、読む量と保守する量が増えます。
ponytail は、この振る舞いを抑えるための ruleset です。Dietrich Gebert 氏が MIT ライセンスで公開しており、2026 年 6 月 12 日の公開から 3 か月で star 132,417、fork 7,085 に達しています (数値は 2026 年 9 月 9 日時点)。Claude Code のプラグインとしてのバージョンは 4.9.0 です。GitHub のリポジトリ説明は「あなたの AI エージェントを、部屋でいちばん怠惰なシニア開発者のように考えさせる。最良のコードは書かなかったコードだ」です。
本記事は README の要約ではなく、公開されている実装を読んで設計を追います。筆者は ponytail を install していないので、使ってみた感想は書きません。代わりに、判断の順序をどう定義しているか、その定義をどの経路でエージェントへ届けているか、効果をどう測ったかを順に読みます。引用は commit 356918e 時点のものです。
第 1 章: ponytail の正体コードではなく判断の順序を配る
ponytail はライブラリではありません。エージェントが生成したコードに後から手を入れるものでもありません。install すると起きるのは、セッションが始まるたびに同じ規範テキストがエージェントの context へ注ぎ込まれることです。振る舞いを変えるのはそのテキストです。Claude Code のプラグインとして動く hook がするのは、テキストの注入、モードの記録、そして初回に statusline の設定を促すことで、生成されたコードそのものには触りません。
README が冒頭に置く例が、変わり方をそのまま示しています。日付入力を頼まれたエージェントが flatpickr を入れてラッパーを書き始めるのに対し、ponytail 有効時の答えはこうなります。
<!-- ponytail: browser has one -->
<input type="date">
ブラウザに最初から入っている要素を使う、という判断です。この 1 行が示しているのは実装技法ではなく、何を作るかを決める前に「作らずに済むか」を問う順序そのものです。ponytail はこの順序を 7 段のラダーとして明文化していて、次章で扱います。
install と強度の切り替え
Claude Code への導入はプロンプト 2 つです。
/plugin marketplace add DietrichGebert/ponytail
/plugin install ponytail@ponytail
README には「install を通すには 2 つのプロンプトを別々に送る必要がある」という注意書きがあります。プラグインは Node.js のライフサイクル hook を動かすので node が PATH 上にある必要があり、無い場合は skill としては機能するものの常時有効化の部分が黙って止まる、とも書かれています。
強度は lite、full、ultra の 3 段階で、/ponytail lite|full|ultra|off で切り替えます (off は無効化)。既定は full です。同じ「API レスポンスにキャッシュを足して」という依頼に対して、SKILL.md は段階ごとの応答例をこう置いています。
| 段階 | 応答の例 |
|---|---|
lite | 「キャッシュを追加しました。参考までに、自分でキャッシュクラスを持ちたくなければ functools.lru_cache で 1 行です」 |
full | 「fetch 関数に @lru_cache(maxsize=1000)。独自キャッシュクラスは省略、lru_cache が実測で足りなくなったら追加を」 |
ultra | 「プロファイラがそう言うまでキャッシュは入れない。言ったときは @lru_cache。手書きの TTL キャッシュクラスはヒット率付きのバグ農場です」 |
lite は代替案を提示して選択を委ね、full はラダーを適用したうえで省略したものを申告し、ultra は要件そのものに反論します。既定を変えたいときは環境変数 PONYTAIL_DEFAULT_MODE か設定ファイルの defaultMode を使います。設定ファイルの位置は XDG_CONFIG_HOME があればその下、無ければ ~/.config/ponytail/config.json、Windows では %APPDATA% 配下です。
配布先の広さ
ponytail は Claude Code 専用ではありません。docs/agent-portability.md の adapter 表には 22 の host が並び (Generic agents の行を除く)、README のバッジは「works with 20 agents」と主張しています。表の内訳は 2 層に分かれます。
- プラグインとして入る host: Claude Code、Codex、GitHub Copilot CLI、OpenCode、pi、Hermes Agent、Qoder、Grok Build、Gemini CLI。
/ponytailのモード切り替えが付きます。ただし注入の仕組みは同じではありません。ライフサイクル hook を使うのは Claude Code、Codex、Qoder。OpenCode、pi、Hermes Agent は毎ターンの system prompt 変換で注入します。Grok は skill の説明文からの呼び出し、Gemini は extension manifest がAGENTS.mdを常時ロードする形です。doc は hook を使わない理由も書いていて、Grok は「受動的な hook 出力では指示を注入できない」ため、Gemini は「Claude / Codex の hook マップを自動検出パスへ置かない」ためだとしています - 規則ファイルを置くだけの host (doc の言う instruction tier): Cursor、Windsurf、Cline、Kiro、Zed、Amp、Jules など。
AGENTS.mdや.cursor/rules/ponytail.mdcを置くと常時ロードは効きますが、/ponytailによるモード切り替えは付きません
同じ規範テキストが host ごとの記法へ複製される構造なので、コピーどうしがずれる余地があります。ponytail は scripts/check-rule-copies.js を用意し、README の Development 節で規則テキストを変えたらこれを回すよう指示しています。同一内容を複数箇所に置く設計を選んだうえで、同期の照合をスクリプトに任せた形です。
第 2 章: 7 段のラダー「書かない」判断を順序づける
規範の本体は skills/ponytail/SKILL.md の 1 枚に収まっています。その中心が「ラダー」と呼ばれる判断の順序で、エージェントはコードを書く前に上から降りていき、最初に成立した段で止まります。
1. Does this need to exist at all? → 投機的な必要は飛ばす (YAGNI)
2. Already in this codebase? → あるなら使う。書き直さない
3. Stdlib does it? → 使う
4. Native platform feature covers it? → 使う
5. Already-installed dependency solves it? → 使う。数行で済むことに新しい依存を足さない
6. Can it be one line? → 1 行にする
7. Only then: → 動く最小限を書く
上の 2 段が効く場面と、下の 5 段が効く場面は性質が違います。段 1 はYAGNIYAGNIYou Aren't Gonna Need It の頭字語。いま必要と確定していない機能や抽象を先回りして作らない、という指針。そのもので、要件の存在自体を疑います。段 2 は既存コードの再利用で、SKILL.md は「数ファイル隣にあるものを再実装するのが、いちばんよくある雑さだ」と書いています。この 2 段は書く量を減らすというより、書く対象を消します。段 3 以降は対象を認めたうえで、どこから調達するかを決める順序です。
投機的な抽象化のコストについては、当サイトの 開放閉鎖の原則 — 予測が外れたときのコスト で扱っています。ラダー 1 段目が対象にしているのは、そこで論じている「まだ来ていない変更に備えた構造」そのものです。
ラダーは理解の代わりではない
ここが「短く書け」という指示との分かれ目です。SKILL.md はラダーの直後に但し書きを置いています。
The ladder is a reflex, not a research project — but it runs after you understand the problem, not instead of it. Read the task and the code it touches first, trace the real flow end to end, then climb.
問題を理解した後にラダーが走る、理解の代わりに走るのではない、という順序です。同じ趣旨が「怠惰にならない領域」の節でも繰り返されていて、そちらではもう一段強い言い方になっています。
Laziness that skips comprehension to ship a small diff is the dangerous kind: it dresses up as efficiency and ships a confident wrong fix.
理解を飛ばして小さな差分を出す怠惰は、効率のふりをして自信のある誤った修正を出す、と書いてあります。差分の小ささ自体は目的ではないという立場が、規範の中に明示されている点は押さえておく価値があります。
バグ修正は症状ではなく原因へ
ラダーの下に、バグ修正に限った規則が 1 つ付いています。
Bug fix = root cause, not symptom. A report names a symptom. Before you edit, grep every caller of the function you're about to touch. The lazy fix IS the root-cause fix: one guard in the shared function is a smaller diff than a guard in every caller …
共有関数に 1 つガードを置くほうが、呼び出し側ごとにガードを置くより差分が小さい。だから怠惰な修正と根本原因の修正は一致する、という論法です。チケットが名指しした経路だけを直すと兄弟の呼び出し側が壊れたまま残る、という指摘も添えられています。怠惰さを根拠に根本原因の修正へ向かわせる組み立てになっています。
段 4 が指す「プラットフォームにあるもの」
ラダーの段 4 は「ネイティブのプラットフォーム機能で足りるか」ですが、SKILL.md がこの段に添えているのは 3 つの例だけです。何がネイティブにあるかの一覧は、別ファイルの docs/platform-native.md にあります。SKILL.md からこの文書へのリンクはありませんが、文書のほうは冒頭で「怠惰なシニア開発者が最初に問うのは、いつでも『プラットフォームがもうこれをやっていないか』だ」と書いていて、段 4 と同じ問いを扱っています。内訳は HTML の要素、CSS の機能、JavaScript とブラウザ API、Swift と SwiftUI、Node.js と Python の標準ライブラリ、データベースの 7 領域です。
| 入れようとしたもの | プラットフォーム側にあるもの |
|---|---|
| 日付や時刻、色の選択 UI | <input type="date"> / <input type="time"> / <input type="color"> |
| モーダルダイアログのライブラリ | <dialog> と dialog.showModal() |
| アコーディオン | <details><summary> |
lodash.clonedeep | structuredClone(obj) |
lodash.groupby | Object.groupBy(arr, fn) |
uuid (v4) | crypto.randomUUID() |
query-string / qs | new URLSearchParams(location.search) |
| 無限スクロールのライブラリ | new IntersectionObserver(cb).observe(sentinel) |
| 親要素セレクタ | :has(input:checked) |
| fetch のタイムアウト | AbortSignal.timeout(5000) を fetch に渡す |
| 自動で伸びる textarea | CSS の field-sizing: content |
文書は使い方も自分で書いています。「パッケージに手を伸ばす前に、ここを走査せよ」。ただし走査させる仕掛けは付いていません。Claude Code のプラグインが注入するのは SKILL.md を濾過したテキストか hook 内のフォールバックで、この一覧はそこに入らないからです (第 4 章)。読ませたい相手に届ける経路は、利用者が別に用意することになります。
debounce のように短い実装は、表の中で 3 行のコードとして直接示されています。
// ponytail: 3 lines beats a dependency
let t;
const debounce = (fn, ms) => (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), ms); };
このコメントの ponytail: という接頭辞には役割があります。次章で扱います。
第 3 章: 怠惰の境界削らないものと、それを運用に載せる skill
「短く書け」という指示の危険は、削ってはいけないものを削ることです。ponytail はその境界を規範の中に置いていて、SKILL.md の ## When NOT to be lazy が該当します。
Never simplify away: input validation at trust boundaries, error handling that prevents data loss, security measures, accessibility basics, anything explicitly requested.
信頼境界での入力検証、データ損失を防ぐエラー処理、セキュリティ対策、アクセシビリティの基本、明示的に要求されたもの。この 5 つは削減の対象外です。さらに「ユーザーが完全版を求めたら作る。議論を再開しない」という但し書きが付きます。
同じ節にはハードウェアの項もあります。実際の時計はずれ、実際のセンサーは読み値が外れ、PCA9685 は数パーセント速く動くので「コードを減らすだけでなく、校正のつまみを残せ」という内容です。最小のモデルからは見えない物理側の事情を、削減の例外として名指ししてあります。
テストについても境界が引かれています。
Lazy code without its check is unfinished. Non-trivial logic (a branch, a loop, a parser, a money/security path) leaves ONE runnable check behind, the smallest thing that fails if the logic breaks …
分岐、ループ、パーサ、金銭やセキュリティの経路には、壊れたら落ちる最小の検査を 1 つ残す。フレームワークもフィクスチャも要らず、関数ごとのスイートは求められない限り要らない、とされています。逆に自明な 1 行にはテストを付けません。YAGNI をテストにも適用する、という書き方です。
これらの境界を運用に載せるために、ponytail は 6 つの skill を配ります。/ponytail が本体で、残りは運用側です。
| skill | 役割 |
|---|---|
/ponytail-review | 差分を過剰実装の観点だけでレビューし、削除リストを返す |
/ponytail-audit | 同じことをリポジトリ全体に対して行う |
/ponytail-debt | ponytail: の印を台帳へ回収する |
/ponytail-gain | ベンチマークの実測値をスコアボードとして表示する |
/ponytail-help | コマンドの早見表 |
以下、印と台帳の仕組み、レビューの出力形式、効果の語り方の順に見ます。
ponytail: コメントと繰り越しの台帳
意図的に手を抜いた箇所には、印を付ける規約があります。
Mark deliberate simplifications that cut a real corner with a known ceiling (global lock, O(n²) scan, naive heuristic) with a
ponytail:comment naming the ceiling and upgrade path …
形式は ponytail: <限界>, <上げ方> です。SKILL.md が挙げる例は # ponytail: global lock, per-account locks if throughput matters で、グローバルロックで済ませたことと、スループットが問題になったらアカウント単位のロックへ移ることが 1 行に入っています。
この印は /ponytail-debt が回収します。走査は grep 1 本です。
grep -rnE '(#|//) ?ponytail:' .
拾うのは # と // の 2 種類で、スタックが別のコメント記法を使うなら足せと添えられています。ヒット 1 件が台帳 1 行になり、ファイル単位でまとめて出力されます。この skill は「上げ方が書かれていない印」を別扱いにします。
Flag the rot risk: any
ponytail:comment that names no upgrade path or trigger gets ano-triggertag, those are the ones that silently rot.
限界だけ書いて上げ方を書かなかった印に no-trigger を付け、最後に「N 件の印、うち M 件はトリガーなし」と締めます。skill 自身は「黙って腐っていくのはこれらだ」と書いています。手抜きを記録する仕組みと、その記録のうち腐りやすいものを数える指標が、同じ skill の中にあります。
/ponytail-review の出力形式
/ponytail-review の出力形式は 1 件 1 行に固定されていて、5 つのタグで分類します。delete: は消すだけのもの、stdlib: は標準ライブラリにあるもの、native: はプラットフォームにあるもの、yagni: は実装が 1 つしかない抽象や誰も設定しない設定、shrink: は同じ処理をより短く書けるものです。最後に net: -<N> lines possible. で締め、削れるものが無ければ Lean already. Ship. で終わります。
SKILL.md には避けるべき書き方の例も置かれています。
❌ "This EmailValidator class might be more complex than necessary, have you considered whether all these validation rules are needed at this stage?"
✅
L12-38: stdlib: 27-line validator class. "@" in email, 1 line, real validation is the confirmation mail.
前者は指摘の形をしていますが、読み手が何をすればいいのか決まりません。後者は場所、切るもの、置き換えるものが 1 行に入っています。レビュー指摘を削除可能な行数へ還元する設計です。
このレビュー系 skill には、はっきりした射程の宣言もあります。正しさのバグ、セキュリティホール、パフォーマンスは対象外で、通常のレビューへ回せ、と書かれています。過剰実装だけを見る 1 本のレンズとして使う前提です。
効果を語るときの自制
/ponytail-gain はベンチマークの数値を表示するだけの skill ですが、その中に「Honesty boundary」という節があります。
NEVER print a per-repo savings number ("you saved X lines/tokens here"): the unbuilt version was never written, so there is no real baseline to subtract from in a live repo.
作らなかった版は書かれていないので、実際のリポジトリには引き算する相手が存在しない。だからこのリポジトリで何行節約したという数字を出してはいけない、という内容です。代わりに、実在する数字は /ponytail-debt の台帳 (数えられる) だけだと述べ、そちらへ誘導します。
導入効果を数字で語りたくなる場面で、語れない理由を先に書いておく判断です。
ただしこの skill には食い違いがあります。表示するスコアボードの数値は 3 つで、コード量が ▼ 80–94%、コストが ▼ 47–77%、速度が ▸ 3–6× faster です。出所は skill 自身の書き方では「5 つの日常的なタスク、3 モデル」のベンチマーク中央値です。第 6 章で見るように、この測り方は後に作り直され、数字は下がりました。現在の README が掲げるのは「約 20% 安く、約 27% 速く」で、コストの ▼ 47–77% とは倍以上離れています。撤回は README の折りたたみに留まり、配布される skill の数値は据え置かれたままです。自制の宣言と、その宣言が載っている skill の中身が揃っていません。
第 4 章: 注入の機構SKILL.md 1 枚を 3 つの hook で配る
ここまでは規範の内容でした。ここからは、その規範がどうやってエージェントの目の前に置かれるかを見ます。Claude Code のプラグインとしての ponytail は、.claude-plugin/plugin.json から hooks/claude-codex-hooks.json を指しており、そこに 3 つの配線があります。以下の引用では、読みやすさのため各 hook の statusMessage フィールドを省いています。
{
"hooks": {
"SessionStart": [
{ "matcher": "startup|resume|clear|compact",
"hooks": [{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/ponytail-activate.js\"", "timeout": 5 }] }
],
"SubagentStart": [
{ "hooks": [{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/ponytail-subagent.js\"", "timeout": 5 }] }
],
"UserPromptSubmit": [
{ "hooks": [{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/ponytail-mode-tracker.js\"", "timeout": 5 }] }
]
}
}
役割が 3 つに分かれています。SessionStart はセッションが始まるとき (新規、再開、/clear、compact の後) に規範を注入する。UserPromptSubmit は毎プロンプトで /ponytail コマンドを監視し、モードの切り替えを状態ファイルへ書く。SubagentStart は subagent が生成されるときに同じ規範を注入する。hook の仕組み自体は当サイトの hooks の仕組み で扱っています。
モードごとの濾過
lite、full、ultra で注入するテキストは違いますが、ponytail は段階ごとに別ファイルを持ちません。hooks/ponytail-instructions.js の filterSkillBodyForMode が、SKILL.md を読んでから他モードの行だけを落とします。
落とす対象は 2 種類です。強度の表の行 (| **lite** | ... |) と、実演例の行 (- lite: "...") だけがモード固有なので、そのどちらかでモード名がラベルになっている行を、現在のモード以外について削ります。
行単位の判定なので、モード名で始まる普通の規則行を巻き込む危険があります。実装のコメントがその危険を名指ししています。
Require a quoted value: every worked example is
- lite: "...". Without this, an ordinary rule bullet that happens to start with a mode word (e.g. "- Full: ...") is silently dropped in every other mode — it looks like a worked example but is really prose meant to survive verbatim.
実演例の判定に「引用符を伴う」という条件を足してあるのは、- Full: ... のような普通の箇条書きが他モードで黙って消えるのを防ぐためです。同じコメントは強度表の側も説明していて、No unrequested abstractions: ... のようにラベルがモード名でない行は通常の規則として残す、と書かれています。
1 枚のファイルから 3 通りのテキストを作る代わりに、行の形に依存した濾過が入る。その脆さをコメントで固定してある構造です。
読めなかったときに何を注入するか
SKILL.md が読めない場合、getPonytailInstructions は例外を捕まえて getFallbackInstructions を返します。この関数はラダー 7 段、規則、出力形式、怠惰にならない領域、境界を、JavaScript の文字列連結でハードコードしたものです。
つまり規範のテキストは、SKILL.md と hook 内のフォールバックの 2 か所にあります。第 1 章で触れた host ごとの規則ファイルと同じ、複製がずれうる構造です。
host ごとに出力の形を変える
hooks/ponytail-runtime.js の writeHookOutput は、同じ内容を host ごとに違う形で書き出します。以下は各 host の挙動についてのコード内コメントの記述で、筆者が確かめたものではありません。
- Claude Code:
SessionStartは生の標準出力をそのまま context として受けるが、SubagentStartはhookSpecificOutputの JSON にしないと context が落ちる - Codex:
{ systemMessage: "PONYTAIL:" + モード名を大文字にしたもの }を必ず出し、内容がある場合だけhookSpecificOutputを添える - Copilot:
SessionStartのadditionalContextだけを読み、他のイベントでは出力を無視する - Qoder:
SessionStartイベントを持たないので、UserPromptSubmitが初回プロンプトでの有効化と毎ターンの注入を兼ねる
host の判別も素直ではありません。VS Code の Copilot は COPILOT_PLUGIN_DATA を設定せず CLAUDE_PLUGIN_ROOT だけを渡してくるため、そのパスが .vscode と agent-plugins を含むかどうかで判定しています。この判定が無かったときは Copilot を native の Claude Code と誤認し、Copilot が読まない statusline の設定案内を出していた、とコメントに書かれています。
落ちないことを優先した設計
このコードベースで一貫しているのは、失敗したときに黙って通す方向へ倒す判断です。
| 失敗する箇所 | どちらへ倒すか |
|---|---|
SKILL.md が読めない | ハードコードのフォールバック文を注入する |
PONYTAIL_SUBAGENT_MATCHER が不正な正規表現 | 対象を絞らず全 subagent へ注入する |
stdin から agent_type が読めない | 注入する |
| stdin が 1 秒以内に終わらない | 注入して抜ける |
| 状態ファイルの書き込みに失敗 | 無視して続ける |
| 標準出力が閉じている (EPIPE) | 無視して抜ける |
stdin の 1 秒タイムアウトには理由が書かれています。Windows では Claude Code が hook を PowerShell の if {} で包むことがあり、そのとき渡された JSON が飲み込まれて stdin の end が発火しないため、hook が永久にブロックしてセッションが凍る。だから短いフォールバックで処理して抜ける、という経緯です。
コメントが名指しする優先順位は 2 つあります。1 つは規範を落とさないこと (絞り込みの失敗で「決して黙って人格を落とさない」)。もう 1 つはセッションを止めないこと (「決してセッションをブロックしない」) で、stdin のタイムアウトと同期処理の経路はこちらが根拠です。どちらの向きにも、静かに壊れるより余分に動く側が選ばれています。
第 5 章: subagent に規範が届く範囲公式仕様と ponytail の読み
3 つの hook のうち SubagentStart だけは、他の 2 つと事情が違います。セッション開始とプロンプト送信は誰の目にも見える入口ですが、subagent の生成は親セッションの内側で起きるので、そこに何が引き継がれるのかは仕様を読まないと分かりません。
Claude Code の公式ドキュメントは、fork でない subagent の初期 context に何が入るかを列挙しています。
- subagent は毎回まっさらな独立した context window で始まる。親の会話履歴、すでに呼び出した skill、Claude がすでに読んだファイルは見えない
- system prompt はその agent 自身のプロンプトと環境情報であって、Claude Code の system prompt ではない
CLAUDE.mdの階層は読み込まれる (~/.claude/CLAUDE.md、プロジェクトの規則、CLAUDE.local.md、管理ポリシー)。ただし組み込みの Explore と Plan はこれを飛ばす- メイン会話の auto memory は読み込まれない
hook については、hooks のドキュメントが「settings、管理ポリシー、プラグイン由来の hook は subagent の内側でも走る」と述べています。走ることは分かる。ただし、親セッションで hook が注入した context が subagent へ引き継がれるかどうかは、どちらのページにも書かれていません。
公式ドキュメントで確認できたのは、SubagentStart というイベントが実在し、matcher が agent type で評価され、入力に agent_id と agent_type が入ることです。標準出力がそのまま context になるイベントは限定列挙されていて、SubagentStart はそこに含まれません。
The exceptions are
UserPromptSubmit,UserPromptExpansion,SessionStart, andPostModelSwitch, where Claude Code adds plain-text stdout as context that Claude can see and act on.
一方、SessionStart で注入した context が subagent へ継承されるかどうかは、記載を見つけられませんでした (調査したのは hooks と sub-agents の両ページ)。以下で扱う「届かない」は ponytail 側の主張であり、筆者が実測して確かめたものではありません。
ponytail 側の読み
hooks/ponytail-subagent.js は、冒頭のコメントで自分が存在する理由を述べています。
SessionStart context is parent-thread only and never reaches subagents, so without this every Task-spawned agent runs ponytail-unaware (issue #252).
SessionStart の context は親スレッド限定で subagent には届かないので、これが無いと Task で生成された agent はすべて ponytail を知らずに動く、という読みです。根拠として issue #252 が挙げられています。この読みに従って、ponytail は subagent の生成時に同じ規範を注入し直します。
注入の形式は、第 4 章で見た writeHookOutput の分岐と対応しています。native の Claude Code に対して、SessionStart では生の標準出力を書き、SubagentStart では hookSpecificOutput の JSON を書く。後者を JSON にしないと context が落ちるとコメントに書かれていて、これは公式ドキュメントが「標準出力が context になるのは 4 イベントだけ」と限定していることと符合します。
注入先を絞る仕組み
全 subagent へ規範を注入すると、読み取り専用の検索 agent にまで「怠惰に書け」という指示が入ります。そこで PONYTAIL_SUBAGENT_MATCHER という環境変数があり、正規表現に一致する agent_type だけへ注入を絞れます。
挙動は 3 点で、いずれも安全側の設計です。正規表現は先頭末尾を固定せず大文字小文字を区別しないので explore|general はどちらにも当たり、厳密一致は ^general$ と書く。変数が未設定なら全 subagent へ注入する。そして正規表現が不正な場合、stdin が読めない場合、agent_type をプラットフォームが報告しない場合は、いずれも注入する側へ倒す。
第 4 章の表に並べた失敗の扱いと、同じ向きです。
自分で harness を作っている場合の含意
常時ロードの規範を持つ harness を自作していると、同じ問題に当たります。CLAUDE.md の階層は subagent にも読み込まれますが、Claude Code の system prompt に載る規範はそうではない。だから subagent に効かせたい規範は、参照先を教えて読ませるか、生成時のプロンプトへ直接書き込む、という運びになります。同じ問題に対する規約が当サイトの ディレクトリ構成と合成規約 にあります (subagent が依存する規範は条件ロードにしない、per-repo の知識は起動側が絶対パスで注入する)。subagent の役割分担そのものは subagent と役割分担、harness 全体の設計は ハーネスエンジニアリング入門 が扱っています。
ponytail が示しているのは、そこに SubagentStart hook という別の経路がありうることです。ただし前提として、hook が注入した context の継承について公式ドキュメントは沈黙しています。この経路を自分の harness へ持ち込むなら、まず 1 本の subagent で実際に届くかを確かめるところから始めることになります。
第 6 章: ベンチマークが作り直された話80-94% はなぜ撤回されたか
ponytail の README には現在「およそ 54% のコード削減 (最大 94%)、約 20% 安く、約 27% 速く、安全性 100%」と書かれています。以前は「80-94% のコード削減」でした。数字が小さくなったのは効果が落ちたからではなく、測り方が間違っていたと作者が認めて測り直したからです。
受けた批判
issue #126 で Colin Eberhardt 氏が 4 点を指摘しました。ベンチマークの writeup はその 4 点を自分の言葉で並べ直してから答える構成になっています。
- 1 回の completion は、コーディングエージェントの使われ方ではない。実際の作業は agent が実在のコードベースを何ターンもかけて編集するもの
- ベースラインが素のチャット的なモデルだった。散文、注意書き、複数の選択肢を出すので「回答の行数」がコードでなく解説を数えていた。ベースラインが膨らんだ分だけ skill が良く見える
- 「短く書け」は安全性を手放しているのではないか。入力検証やエラー処理を落として行数を減らしているのでは
- 短いプロンプト 1 つで同じ仕事ができるのではないか
writeup はこの 4 点を「すべて妥当」とし、「ponytail を否定できるように組んだ」と書いています。
何を変えたか
| 単発生成 (旧) | エージェント (新) | |
|---|---|---|
| 作業単位 | 1 プロンプト、1 completion | 一時ワークスペースでの headless Claude Code セッション |
| ベースライン | 素の API モデル (散文と選択肢を出す) | skill 無しの同じ Claude Code agent |
| 数えた LOC | 解説を含む回答全体 | agent が残した git diff の追加行 |
| 比較対象 | ponytail と素のモデル | baseline、ponytail、caveman、短いプロンプト版 |
| 安全性 | 測っていない | 生成された関数を敵対的入力に対して実行して判定 |
対象リポジトリは fastapi/full-stack-fastapi-template を commit cd83fc1 に固定したもの、モデルは Haiku 4.5、条件とタスクの組 1 つ (以下セル) につき 4 回試行です。比較対象に入っている caveman は別のリポジトリで、こちらは話し方を簡潔にする skill です。「ponytail の効果が単に短く話すことなら caveman が同じ結果になるはずだ」という対照として置かれています。短いプロンプト版は批判 4 への直接の答えで、Colin 氏の主張を「Follow YAGNI principles, and prefer one-liner solutions」という 7 語に言い換えて system prompt へ足したものです。
ベースラインが ponytail を走らせていた
この writeup で特筆すべきなのは、公開前に自分で見つけた汚染バグを書いていることです。
An earlier agentic run showed a tiny ~4% gap and we nearly published it. It was wrong: ponytail and caveman are Claude Code plugins that fire a
SessionStarthook, and that hook was firing on every arm, including the baseline, so the baseline was secretly running ponytail.
差が 4% しか出ない結果が出て、公開しかけた。原因は ponytail と caveman が SessionStart hook を持つプラグインで、その hook が baseline を含む全条件で発火していたことでした。つまり skill 無しのはずのベースラインが、こっそり ponytail を走らせていた。
修正は 2 つで、--setting-sources project,local でユーザーのグローバルプラグインを除外し、--plugin-dir で条件ごとに 1 つだけプラグインを読み込む形にしています。writeup はこの記述の理由を「ベンチマークに嘘をつかせる種類の誤りだから、それを見つけたことが残りを信用してよい根拠になる」と説明しています。
第 4 章と第 5 章で見た「常時どこにでも注入する」設計が、自分の測定を汚染していたわけです。注入の広さが利点として設計されている以上、それを切り離す作業は測定側の責任になります。
結果
12 個の機能実装タスクについて、writeup が報告しているベースラインに対する変化率です (各タスク 4 回の平均、ベースラインの絶対値は 1 タスクあたり 191 LOC、$0.097、69 秒)。
| 条件 | LOC | token | コスト | 時間 |
|---|---|---|---|---|
| caveman (簡潔な話し方) | -20% | +7% | +3% | +2% |
| ponytail | -54% | -22% | -20% | -27% |
| 短いプロンプト版 | -33% | -14% | -21% | -30% |
タスク別に見ると、平均の内訳がはっきりします。
| タスク | baseline | ponytail |
|---|---|---|
| 日付選択 | 404 | 23 |
| 色選択 | 287 | 23 |
| ファイルのドロップ領域 | 251 | 95 |
| 多段ウィザード | 571 | 312 |
| タイトル検索 (バックエンド) | 44 | 44 |
| 項目の複製 (バックエンド) | 24 | 23 |
大きく削れているのはネイティブ機能が自前実装を置き換える場面だけです。日付選択と色選択はブラウザの <input> になるので 23 行まで落ちる。一方でバックエンドの CRUD は、どの条件でもほぼ同じ行数になります。writeup はこれを「膨らみが無いところに節約を発明しない。正直なベンチマークはこれを示す必要があり、実際に示している」と書いています。-54% という平均値は、削る余地のないタスクに引き下げられた後の数字です。
caveman が LOC を 20% 削りながら token を 7% 増やしている点も、対照としてよく効いています。出力は簡潔になるが考える量は同じなので安くはならない。ponytail の効果が「短く話すこと」では説明できないことが、この行から読み取れます。
短いプロンプト版は不安定でした。色選択では 25 行という好成績を出す一方、日付選択で 162 行、ウィザードで 406 行、コマンドパレットでは 285 行とベースライン (268 行) を上回っています。
ただし上の表を素直に読むと、コストと時間では短いプロンプト版のほうが良い数字です (-21% と -30% に対し ponytail は -20% と -27%)。4 指標のうち 2 つは 7 語のプロンプトが同等以上で、ponytail が優位なのは LOC と token、そして安定性のほうです。writeup 自身も「短いプロンプト版は安くて速いが、削るコードは ponytail より少なく、安全策を落とした唯一の条件だった」と書いていて、コストと時間で負けていること自体は否定していません。
安全性の 1 件
安全性は 6 つのタスクからなる層で測られ、そのうちセキュリティに関わる 5 タスクを 4 回ずつ、合計 20 回実行して安全率を出しています。要求は実際のチケットのように暗黙のままにしてあり、生成された関数をパストラバーサル、SQL インジェクション、偽造トークン、不正な CSV 行、割り当てを食い潰すクライアントに対して実行します。
| 条件 | 安全 |
|---|---|
| baseline | 100% (20/20) |
| caveman | 100% (20/20) |
| ponytail | 100% (20/20) |
| 短いプロンプト版 | 95% (19/20) |
差が出たのは 1 件だけで、信頼できないファイル名をベースディレクトリに連結するタスクでした。短いプロンプト版は最も少ない 6 行を書き、4 回のうち 1 回で ../../ を含むファイル名がディレクトリの外へ出た。ponytail は約 9.5 行を書き、4 回すべて安全でした。writeup は、その差の約 3 行がパストラバーサルの検査だったと書いています。
writeup の言い方はここでも控えめです。「Haiku の規模では安全性の差は小さい。20 回に 1 回の滑りで、これは床であって劇的な結果ではない。決定的な検査はセキュリティの証明ではない」と書いています。
- モデルが 1 つ: Haiku 4.5 のみ。大きいモデルは手取り足取りの必要が減るので差が縮まるかもしれないし、広がるかもしれない。ハーネスは Sonnet と Opus も動くが、コストの都合で Haiku で止めた
- 安全性は床: 6 タスクの決定的な検査で、既知のガードを落とすかどうかを見るもの。コードが安全であることの証明ではない
- 短いプロンプト版は言い換え: Colin 氏の主張を彼らが最も強い形に書き直したもので、本人の意図そのものではない
- 非決定性: 各セル 4 回。フロントエンドの LOC は自前実装だと 300-570 行の幅で振れるので、平均は安定しているが密ではない
- 打ち切られた実行: LOC を測った 192 回の実行のうち 4 回が Windows のプロセスタイムアウトで強制終了した。ファイルは書かれていたので LOC は数えたが、コストと時間は数えていない。どのセルも 4 回のうち 2 回以上を残している
結論の言い方
writeup はこう締めています。
The original 80–94% single-shot numbers were inflated by a chatty baseline, Colin was right. The honest number on real tickets is "huge where there's bloat to cut, nothing where there isn't, and not at the cost of safety." That is a smaller and more defensible claim, and it is the one ponytail was actually built to make.
元の数字は膨らんでいた、Colin は正しかった。正直な数字は「削る膨らみがあるところでは大きく、ないところではゼロ、そして安全性を犠牲にしない」だ。それは小さく、より擁護しやすい主張であり、ponytail が実際に作ろうとしたのはその主張だと述べています。README も旧数値を消さずに折りたたみへ残し、「単発のベンチマークは公正なエージェント比較に対しては 1 タスクあたりの上限値であって平均ではない」と注記しています。
star が 13 万に達したリポジトリが、自分の宣伝文句だった数字を批判に応じて下げ、その過程を残している。効果の主張よりも、この扱い方のほうが参考になります。
第 7 章: まとめと関連リンク
ponytail を実装から読んで、持ち帰る価値があると感じた点を挙げます。
判断の順序を書いてある。 「短く書け」という指示だけでは、安全策を落とすことがあります。ponytail が書いているのは短さではなく降りていく順序で、その順序が理解の後に走ることを明示しています。抽象的な方針で終わらせず、段 4 については照合できる候補の一覧まで別の文書に書いてある。ただしその一覧は注入されるテキストに入りません。届くのは順序のほうで、一覧はリポジトリに置いてあるだけです。
削らないものを先に決めてある。 信頼境界の入力検証、データ損失を防ぐエラー処理、セキュリティ、アクセシビリティ、明示要求されたもの。ベンチマークで安全策を落としたのは、削減だけを指示して例外を書かなかった 7 語のプロンプトでした (20 回中 1 回)。ただし条件の組み方から言えるのはそこまでです。ponytail から例外の節だけを抜いた条件は測られていないので、例外リストがその差を作ったとまでは言えません。
注入は 1 枚のファイルから、失敗しても通す向きへ。 段階ごとの規範を別ファイルに持たず、SKILL.md を行単位で濾過する。読めなければハードコードのフォールバックへ切り替わる。正規表現の不正、stdin の失敗、タイムアウトはすべて「注入する」側へ倒す。コメントが挙げる理由は 2 つあり、規範を黙って落とさないことと、セッションを止めないことです。
効果の主張の扱い方。 外部の批判を受けて測り方を組み直し、自分の宣伝文句だった数字を下げ、その過程と限界を残した。加えて /ponytail-gain には「このリポジトリで何行節約したかを絶対に出すな。作らなかった版は書かれていないので引き算する相手が存在しない」という自制が書かれています。ただし第 3 章で見たように、その skill が表示する数値自体は撤回前の単発ベンチマーク由来のまま据え置かれています。README で数字を下げる作業が、配布される skill まで届いていない。誠実さの宣言と実際の配布物のあいだに差が残る例として、そのまま受け取るより確かめたほうがよい箇所です。
ponytail: コメントと /ponytail-debt の組み合わせも、小さいながら実用的な形です。意図的な手抜きに限界と上げ方を書いて印を付け、後から grep で台帳に集め、上げ方の無い印には no-trigger を付けて区別する。手抜きの記録を残す仕組みと、その記録が腐ることを検出する指標が同じ場所にあります。
関連リンク
- DietrichGebert/ponytail:本体。MIT ライセンス
skills/ponytail/SKILL.md:ラダー 7 段と怠惰にならない領域の正本docs/platform-native.md:段 4 と同じ問いに答えるネイティブ代替の一覧。HTML から Swift、各言語の標準ライブラリ、データベースまでdocs/agent-portability.md:どの host にどのファイルが対応するかの表- エージェントベンチマークの writeup:測り方、タスク別の表、限界
- issue #126:旧ベンチマークへの批判。作り直しの発端
- JuliusBrussee/caveman:話し方を簡潔にする別の skill。ベンチマークの対照条件
- Claude Code hooks と subagents:第 4 章と第 5 章で参照した公式ドキュメント
- Claude Code ベストプラクティス集の歩き方:同じ形式で別のリポジトリを読んだ記事
本記事で挙げた数値のうち、star 132,417、fork 7,085、プラグインのバージョン 4.9.0 は 2026 年 9 月 9 日時点のものです。コードとドキュメントの引用はすべて commit 356918e 時点です。公開から 3 か月でバージョンが 4 系まで進んでいるリポジトリなので、install 手順と skill の構成は変わっている可能性があります。最新の状態は元の README で確認してください。