← Home← ホーム / Mechanism仕組み
Deterministic hooks + ODD決定論的 hook + ODD

Not a promise. A hook.約束ではなく、hook が強制する。

This page goes into how the hooks are built, and uses Claude Code's own vocabulary (tool calls, lifecycle events) as-is. For the wider picture first, start from Home. A rule only tells Claude "don't push to main." A hook stops it. A hook is a small script that reads the tool call before it runs. When it judges the call unsafe, it exits with code 2 and blocks the call. Banto registers 49 of them. Each skill also declares its own operating range in odd.yaml. This is its ODD (short for Operational Design Domain: the range in which that skill may act on its own). This page maps each declared boundary to the hook that actually enforces it.

このページは hook の実装に踏み込む。Claude Code の内部で使う言葉(tool call・ライフサイクルイベント)がそのまま出てくる。まず全体像を知りたい場合はホームから読んでほしい。rule は「main へ直接 push するな」と Claude に告げるだけだ。実際に止めるのは hook である。hook とは、tool call を実行前に読み取る小さなスクリプトのことだ。危険だと判定すると、終了コード 2 でプロセスを終え、その call を遮断する。Banto にはこの hook が 49 本ある。各 skill は自分の運転可能な範囲も odd.yaml に宣言する。これが ODD(Operational Design Domain の略、その skill が自分だけで動いてよい範囲の宣言)だ。このページでは、宣言した境界それぞれが、実際にどの hook に受け止められるかを突き合わせる。

Claude tool calltool call Bash · Write · Edit … PreToolUse the gate — before it runs実行前の関所 odd-kill-switch prod-guard egress-guard … 19 entries on this event… このイベントだけで 19 エントリ allow許可 exit 0 runs, untouchedそのまま実行される block遮断 exit 2 blocked, reason shown理由つきで止まる + named escape+ 名前つき escape

one tool call, one deterministic verdict. No model sits in the gate.1 回の tool call に 1 つの決定論的な判定。関所にモデルはいない。

1
hooks.json

The whole picture全体像

Every hook is wired to one of Claude Code's lifecycle events. Banto uses all ten.すべての hook は Claude Code のライフサイクルイベントのいずれかに配線される。Banto は 10 種類すべてを使う。

Registered by the numbers登録数の実数

  • 49 distinct hook scripts wired in hooks.jsonhooks.json に配線された固有スクリプト 49
  • 56 total command entries across 10 lifecycle events (some scripts run on more than one matcher)10 種類のライフサイクルイベント全体でのべ 56 エントリ(一部のスクリプトは複数 matcher に登録)
  • events: SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, Stop, SessionEnd, PreCompact, SubagentStart, SubagentStop, UserPromptExpansionイベント種別:SessionStartUserPromptSubmitPreToolUsePostToolUseStopSessionEndPreCompactSubagentStartSubagentStopUserPromptExpansion
  • PreToolUse carries the heaviest load: 19 of the 56 entries. This is the only point where a dangerous action can still be stopped before it runs最も比重が重いのは PreToolUse(56 件中 19 件)。危険な操作を、実行前に止められる唯一の地点だからだ

Rules get skipped. Hooks don't.rule は読み飛ばされる。hook は読み飛ばせない。

CLAUDE.md, the ten behavioral rules, and each skill's own instructions are all text that Claude reads inside its context window. In a long session, that text gets skipped, misremembered, or overridden by a more recent instruction. A hook is different: it is a real process, not text. It starts the moment Claude calls a tool. It reads that tool call's raw payload directly, and when it decides to block, it exits with code 2 before the action runs. There is no context window for a hook to fall out of.CLAUDE.md も、10 の行動規範も、各 skill 自身の指示も、Claude がコンテキストの中で読むテキストにすぎない。長いセッションでは、このテキストは読み飛ばされたり、記憶が薄れたり、後から来た指示に上書きされたりする。hook はテキストではない。実際に動くプロセスであり、Claude が tool を呼んだ瞬間に起動する。その tool call の生の payload を直接読み、block すると判断したときは、実行前に exit 2 で終了する。hook にはコンテキストウィンドウ自体が無い。だから、そこから内容が落ちるという事態が、そもそも起こらない。

Fail-open by design. A missing dependency must never break a session. This is fail-open: without a way to judge, a hook lets the action through rather than stopping it. If jq is absent, every hook exits 0 silently. The plugin disables itself instead of blocking work on a broken environment. The reverse case is fail-safe: once a hook does detect real danger, it blocks with exit 2, no matter what any other plugin's hooks would allow.判定材料が無ければ黙って通す(fail-open)。依存が欠けているだけで、セッションを壊してはならない。jq が無ければ、全 hook は黙って exit 0 とする。壊れた環境で作業を止めるのではなく、機能そのものを自ら無効化する。逆に危険を検知できたときは fail-safe になる。安全系の hook が block を決めたら、他プラグインの hook が何を許していようと exit 2 で止める。
2
7 hooks

The seven that carry the weight実質を担う 7 つの hook

Most of the 49 inject a few lines of context or track telemetry silently. These seven actually block, gate, or auto-trigger something. Each description below is read straight from the script's own header comment.49 本の大半は、数行のコンテキスト注入や無音のテレメトリ記録にとどまる。この 7 つだけが実際に block・gate・自動発火のいずれかを行う。以下の説明は、各スクリプト冒頭のコメントからそのまま起こしたものだ。

odd-kill-switch PreToolUse

Blocks four irreversible git / filesystem actions outright: pushing straight to main/master, --no-verify, a hard --force push (--force-with-lease still passes), and rm -rf on root or home. Each one has its own named escape hatch, for example ODD_ALLOW_MAIN_PUSH=1. The block is on by default; it is never silently switched off.4 つの不可逆な git / ファイルシステム操作を全遮断する:main/master への直接 push、--no-verify、素の --force push(--force-with-lease は通過する)、root や home への rm -rf。それぞれに専用の名前つきエスケープがある(例:ODD_ALLOW_MAIN_PUSH=1)。block は既定で有効であり、黙って無効化されることはない。

egress-guard PreToolUse: Write/Edit

Before a Write or Edit lands in a client deliverable, it checks the content against a private, user-scope name registry (~/.claude/banto-name-registry, never committed). This catches internal member names, other-project names, and PII before they leak across an NDA boundary (NDA: non-disclosure agreement). If the registry is absent or empty, the hook does nothing. Escape hatch: BANTO_ALLOW_NAMES=1.Write や Edit の内容を、客先成果物に書き込まれる前に確認する。照合先は非公開のユーザー単位の名前レジストリ(~/.claude/banto-name-registry、絶対にコミットしない)だ。これにより、内部メンバー名・他案件名・個人情報が、NDA(秘密保持契約)の境界を越える前に捕捉できる。レジストリが無い、または空の場合は何もしない。エスケープは BANTO_ALLOW_NAMES=1

verify-claim-guard Stop

This hook fires when the final response asserts completion ("done," "fixed," "tests pass") while recent evidence says otherwise. It checks two narrow, structural conditions. B1: the most recent full verify-run is recorded RED. B2: the very last tool result in the transcript has is_error: true (one successful call after a failure counts as an already-resolved retry, not a false completion). It allows at most one re-verify prompt per stop, guarded by stop_hook_active so it can never loop.この hook は、最終応答が完了を断定している(「完了しました」「修正しました」「テストが通った」など)のに、直近の証跡がそれと矛盾するときに発火する。判定条件は 2 つの狭い構造チェックだけだ。B1:直近のフル verify-run が RED として記録されている。B2:transcript 内で最後の tool result が is_error: true になっている(失敗の後に成功呼び出しが 1 件あれば、それはリトライで解消済みとみなし、誤った完了断定とはしない)。再検証を促すのは 1 stop につき最大 1 回で、stop_hook_active というフラグで無限ループを防ぐ。

idle-checkpoint Stop / SessionEnd

On every Stop event, this hook arms a detached watcher process. What it watches is the transcript, the running log of that session. If nothing moves for BANTO_IDLE_CHECKPOINT_MIN minutes (default 5), it fires a headless — screenless — claude -p --resume --fork-session command to auto-run /save-checkpoint. That 5-minute default matches the prompt-cache TTL, the window in which an already-sent prompt can be reused cheaply. A SessionEnd event kills the watcher, so a normally-finished session never triggers a wasted headless run. The motive: once the cache expires, resuming an idle session re-reads a huge context at full, uncached price. Checkpointing first keeps the eventual /clear and resume cheap.毎回の Stop イベントで、この hook は切り離されたウォッチャープロセスを起動する。ウォッチャーが見張るのは transcript、つまりそのセッションの会話ログである。BANTO_IDLE_CHECKPOINT_MIN 分(既定 5 分)動きがなければ、画面を持たない claude -p --resume --fork-session を起動し、/save-checkpoint を自動実行する。既定の 5 分は、プロンプトキャッシュの TTL に合わせてある。TTL とは、送信済みのプロンプトを安く再利用できる時間のことだ。SessionEnd イベントはこのウォッチャーを kill するので、正常終了したセッションで無駄なヘッドレス実行が走ることはない。動機はこうだ。キャッシュが切れた後に放置セッションへ復帰すると、巨大なコンテキストを割高な非キャッシュ価格で読み直すことになる。先にチェックポイントを切っておけば、その後の /clear と再開が安く済む。

webfetch-deny PreToolUse: WebFetch

Denies WebFetch outright, via the permissionDecision JSON contract. The rationale is the evidence-first rule: WebFetch returns a small model's summary of the page, so the main model can never verify the original text. Reading a URL's full content is consolidated into webread instead (full-text extraction via trafilatura, a text-extraction library). Finding a URL in the first place still uses WebSearch. Escape: BANTO_ALLOW_WEBFETCH=1.permissionDecision の JSON 契約で WebFetch を全遮断する。根拠は evidence-first rule だ。WebFetch は小型モデルによるページ要約を返すため、本体モデルは原文を検証できない。URL の精読は webread(trafilatura という全文抽出ライブラリを使う)に一本化し、URL の発見には引き続き WebSearch を使う。エスケープ:BANTO_ALLOW_WEBFETCH=1

odd-gate PreToolUse: Write/Edit

This hook is a circuit breaker for self-driving implementation loops. A session-global counter, written by the test runner and reset on a pass, tracks how many times in a row the project's own tests have failed. When that count reaches ODD_TEST_FAILURE_THRESHOLD (default 3), the hook blocks further edits and tells the model to hand the root cause to a debugger agent instead of repeating the same fix. It fires regardless of which skill is active, because a runaway edit-then-test-fail loop can happen in the plain main loop too. Escape: ODD_ALLOW_TEST_FAILURES=1.この hook は、自走実装ループのための回路遮断器だ。セッション横断のカウンタ(テストランナーが書き、成功時にリセットされる)が、プロジェクト自身のテストの連続失敗回数を数える。この回数が ODD_TEST_FAILURE_THRESHOLD(既定 3 回)に達すると、それ以上の編集を止め、同じ修正を繰り返す代わりに debugger エージェントへ根本原因を委ねるよう促す。どの skill が動いていても発火する。暴走した「編集 → テスト失敗」の繰り返しは、skill を使わない素の main loop でも起こるからだ。エスケープ:ODD_ALLOW_TEST_FAILURES=1

prod-guard PreToolUse: Bash

Blocks production-environment operations by default: kubectl against a prod context, terraform apply/destroy, a --prod deploy, or ssh to a prod host. It only lets the action through with a conversation-time approval escape (BANTO_ALLOW_PROD=1) or a standing per-repo grant (prod_ops: allow in {base}/meta/grants.json).本番環境の操作を既定で block する:prod context への kubectlterraform apply/destroy--prod デプロイ、prod ホストへの ssh。通すのは会話内承認によるエスケープ(BANTO_ALLOW_PROD=1)か、リポジトリ単位の常設許可({base}/meta/grants.jsonprod_ops: allow)のみ。

verify-run / verify-detect helpers, not hooks

verify-detect.sh does pure file-based detection; it never runs the project's own tools. Both the per-edit test runner and the full verify share it, so there is one source of truth for what "build," "test," and "API smoke" mean in this project. verify-run.sh is the loop's own full verify: build, then the full test suite, then an API smoke test (against staging, with NODE_ENV=test, never production). It writes the green/red state that verify-claim-guard's B1 check reads, and it bumps the same failure counter that odd-gate watches. Neither script is registered in hooks.json; the build-and-verify loop invokes them directly. Even so, they are the ground truth both flagship hooks above depend on.verify-detect.sh は純粋なファイル判定であり、プロジェクト自身のツールは決して実行しない。1 回ごとのテスト実行とフル verify の両方がこのスクリプトを共有するので、このプロジェクトでの「build」「test」「API smoke」の定義は一本化されている。verify-run.sh はループ自身のフル verify であり、build → フルテスト → API smoke(ステージング向け・NODE_ENV=test・本番には決して当たらない)の順に実行する。実行結果として、verify-claim-guard の B1 判定が読む green/red 状態を書き込み、odd-gate が監視するのと同じ失敗カウンタを更新する。どちらのスクリプトも hooks.json には未登録で、build-and-verify ループから直接起動される。それでも、上記 2 つの目玉 hook が依存する事実の根拠そのものは、この 2 本のスクリプトにある。

3
CONTRACT.md

Design principles設計原則

Every hook in the directory follows the same compatibility contract, documented in hooks/CONTRACT.md. This is not a new mechanism. It is simply the set of rules that keeps 49 independently-written scripts from becoming unpredictable when they run together.このディレクトリの全 hook は同じ互換性契約に従う(hooks/CONTRACT.md に記載)。新しい仕組みを足したわけではない。独立に書かれた 49 本のスクリプトが、まとまったときに予測不能にならないための規律にすぎない。

fail-openfail-open one stop, one re-check1 stop 1 回 narrow, structural judgmentnarrow な構造判定 named escape hatch名前つき escape

Narrow over clever賢さより narrow

verify-claim-guard used to grep the last three tool outputs for words like "fatal:". That also matched a hook script's own source code when it happened to sit in the transcript. This was a real source of false positives. The fix was not a smarter heuristic. It was reading the structured is_error flag on exactly the last tool_result. This same pattern repeats across the hook set: the moment a broad text match misfires, replace it with a structural check instead.verify-claim-guard は以前、直近 3 件の tool 出力を "fatal:" のような語で grep していた。これは、たまたま transcript 内にあった hook スクリプト自身のソースコードにも一致してしまう、実際の誤検知源だった。修正は「賢いヒューリスティック」を足すことではなかった。最後の tool_result 1 件だけが持つ、構造化された is_error フラグを読むことだった。hook 群全体でこのパターンが繰り返される。広い文字列一致が誤爆した瞬間、構造判定に置き換えるのだ。

One stop, one re-check1 stop に 1 回だけ

A Stop hook may request exactly one extra turn, by exiting with code 2. Both verify-claim-guard and the checkpoint-arming logic check the stop_hook_active flag before firing, so a re-prompt can never chain into a second re-prompt. This is what keeps a safety valve from turning into an infinite loop the moment its own trigger condition reappears right after the retry.Stop hook が要求できる追加ターンは、exit 2 によってちょうど 1 回だけだ。verify-claim-guard もチェックポイント起動ロジックも、発火する前に必ず stop_hook_active フラグを確認する。これにより、リトライ後に同じ発火条件が再び現れても、再プロンプトが二重に連鎖することはない。

Escape hatches are named, not global. Every deterministic block ships its own environment-variable override: BANTO_ALLOW_NAMES, ODD_ALLOW_MAIN_PUSH, ODD_ALLOW_TEST_FAILURES, BANTO_ALLOW_WEBFETCH, and so on. This lets a legitimate exception be granted precisely, without disabling the guard's other three rules along with it.逃し方は個別に名前がつく。全体スイッチではない。決定論的な block はどれも、専用の環境変数エスケープを備える。例えば BANTO_ALLOW_NAMESODD_ALLOW_MAIN_PUSHODD_ALLOW_TEST_FAILURESBANTO_ALLOW_WEBFETCH だ。正当な例外は、そのガードの他 3 ルールまで一緒に無効化することなく、狙って許可できる。
4
odd.yaml

Autonomy by declaration自律性は宣言する

A one-click utility, like kit, and a multi-file autonomous loop, like dev-loop, cannot share one autonomy rule. A mistake in kit only costs a re-run. A mistake in dev-loop can cost an entire branch. So each skill states its own operating range in plugins/banto/skills/<name>/odd.yaml. A JSON Schema validates that file, but it only checks the shape (the fields are present and well-formed), not whether the skill actually behaves as declared.ワンショットの utility(kit)と、複数ファイルを跨ぐ自走ループ(dev-loop)を、同じ自律性ルールで扱うことはできない。kit の間違いはやり直しで済む。だが dev-loop の間違いは、ブランチ 1 本分のコストになる。そこで各 skill は、自分の運転可能な範囲を plugins/banto/skills/<name>/odd.yaml に書く。この宣言は JSON Schema で検証されるが、見るのは項目が揃っているかという形だけで、実際にその通り振る舞うかどうかは検証しない。

Levelレベル Meaning意味 Adopted interpretations (spec-fidelity)採用解釈の扱い(spec-fidelity)
L0Manual — information display onlyManual — 情報表示のみMay proceed, disclosed in the final report採用解釈で進行可・最終報告で開示
L1Suggest — user approves each outputSuggest — 出力ごとにユーザーが承認May proceed, disclosed in the final report採用解釈で進行可・最終報告で開示
L2Copilot — partial execution, per-step approvalCopilot — 部分実行・手順ごとに承認May proceed, disclosed in the final report採用解釈で進行可・最終報告で開示
L3Autopilot — continuous execution, owner is asked only on exceptionsAutopilot — 継続実行・例外時のみ owner へ確認May proceed, but medium scale or larger MUST disclose interpretation / alternatives / verification進行可。ただし中規模以上は「採用解釈・代替案・検証内容」の開示が必須
L4+Supervised-Auto / Self-improving — explicit stop conditions, little to no human loopSupervised-Auto / Self-improving — 明示停止条件・人間の関与がほぼ無いAdvance confirmation required — adopted interpretations not allowed. Out of Banto's scope.事前確認が必須・採用解釈は不可。Banto の対象外。

A skill with no odd.yaml at all is treated as provisional L0 until one is written. Banto ships levels L0 through L3. L4 and L5 belong to a separate, not-yet-public plugin called banto-autonomy.odd.yaml が無い skill は、書かれるまで暫定的に L0 として扱う。Banto が実装するのは L0〜L3 のみだ。L4・L5 は別プラグイン(banto-autonomy、未公開)が担当する。

plugins/banto/skills/dev-loop/odd.yaml

A real odd.yaml, field by field実例で見る odd.yaml の中身

dev-loop is Banto's autonomous implementation loop (L3), so its odd.yaml is the most exercised one in the tree. Six fields carry the actual declaration.dev-loop は Banto の自走実装ループ(L3)であり、ツリー内でも odd.yaml が最も実働している skill だ。宣言の本体は 6 つのフィールドが担う。

in_scope

What the skill owns: task decomposition, the implement → build-and-verify → fix loop, tasks.md progress, parallel fan-out judgment, exception detection.skill が担う範囲:タスク分解、実装 → 検証 → 修正の周回、tasks.md の前進、並列 fan-out 判定、例外検知。

out_of_scope

Spec generation and branch/worktree operations are delegated elsewhere. Push, PR, and merge to main stay a human gate by default — standing per-repo grants can lift each one.仕様生成やブランチ / worktree 操作は他 skill へ委譲する。push・PR・main マージは既定で人間ゲート — repo 単位の常設許可で個別に解除できる。

safety_boundaries

Commits stop at the branch. Verification must reuse the existing verify-run.sh. It must never reimplement that script's detection logic. Fan-out caps at 5 parallel workers.commit はブランチで止まる。検証は既存の verify-run.sh を正本とし、その検出ロジックを再実装しない。並列 fan-out は最大 5 worker までだ。

kill_switch_conditions

4 conditions stop the loop and escalate to the owner. They're detailed below, in Where the declaration meets a hook.周回を止めて owner へ上げる条件は 4 つある。詳細は下の「宣言が hook と接続する場所」節にある。

approval_gates

Before entering the loop, the decomposition plan (task list plus parallel policy) is shown once for confirmation. An epic-scale target routes through ws epic first.自走に入る前に、分解プラン(タスク列 + 並列方針)を 1 度提示して確認する。epic 規模の対象なら、先に ws epic の提案を経由する。

metrics

test_failure_threshold: 3 and parallel_fanout_max: 5 are the numeric backing for the boundaries above. The schema reads them from the metrics object.test_failure_threshold: 3parallel_fanout_max: 5 が、上記の境界を支える数値だ。スキーマは metrics オブジェクトとしてこれを読む。

dev-loop's own autonomy_level line even names its enforcers inline: "explicit stop is a deterministic hook (odd-gate / verify-claim-guard); the human gate is the Phase-0 decomposition-plan confirmation plus push/PR/main." The declaration and its enforcement are cross-referenced from day one.dev-loop の autonomy_level 行そのものが強制側の名前まで書いている:「explicit stop は deterministic hook(odd-gate/verify-claim-guard)/ human gate は Phase0 分解プラン確認+push/PR/main」。宣言と強制は最初から相互参照している。

5
kill_switch_conditions → hooks

Where the declaration meets a hook宣言が hook と接続する場所

dev-loop's odd.yaml lists 4 kill-switch conditions. Three of them land on a deterministic script. The fourth is a goal fork (a point where the request could reasonably be read two different ways, each leading to a different outcome). That one is enforced by judgment, not a script. This gap is disclosed here on purpose.dev-loop の odd.yaml は 4 つの kill switch 条件を挙げる。このうち 3 つは決定論的スクリプトが受け止める。残る 1 つはゴール分岐(依頼を 2 通りに解釈でき、どちらを選ぶかで結果が変わる分かれ目)で、判断によって受け止める。この隙間は、あえてここで明示する。

odd.yaml declaration — readable text宣言 — 人が読むテキスト 3 consecutive test failuresテスト 3 連続失敗 "done" while evidence is redred 証跡のまま「完了」主張 a goal fork, no clear readingゴール分岐で解釈が立たない irreversible / outward ops不可逆・外向き操作 deterministic hook決定論的 hook が受ける judgment, disclosed判断で受ける(明示開示) odd-gate.sh PreToolUse counter · threshold 3PreToolUse カウンタ · 閾値 3 verify-claim-guard.sh Stop hook · blocks the claimStop hook · 完了断定を止める spec-fidelity rulespec-fidelity ルール not a hook — a stop trigger by judgmenthook ではない — 判断による stop trigger odd-kill-switch.sh 4 blocking rules · named escapes4 つの block · 名前つき escape

3 of 4 conditions land on a script. The 4th is disclosed as judgment, on purpose.4 条件中 3 つはスクリプトが受ける。残る 1 つは「判断」だと、あえて明示する。

Not every declared condition has a matching hook. That gap is disclosed here on purpose. A kill switch that can't be mechanically detected still belongs in odd.yaml, as a human-readable boundary. It just needs a different kind of enforcement than the kind a script can catch: a rule, or a judgment call.宣言した条件のすべてに、対応する hook があるわけではない。その隙間は、ここであえて明示する。機械的に検知できない kill switch も、人が読める境界として odd.yaml に書く価値はある。ただし、スクリプトが拾える強制とは違う種類の強制、つまりルールや判断による強制が要る。