スキルに検査スクリプトを同梱するなら、そのスクリプト自体が欠けた場合も気になります。配布漏れやパスの間違いで検査が動かないのに、後続のコマンドだけ動いてしまうと困ります。

Claude Code v2.1.295のリリースノートに、フックの失敗時に処理を止めるonFailure: "block"が追加されていました。今回は小さなスキルを作り、検査ファイルがある場合とない場合を比べてみました。

PreToolUseとonFailureを確認する

PreToolUseはツール実行前のフックです。今回はBashを対象にして、コマンドが動く前にNode.jsの検査スクリプトを呼び出します。

onFailure: "block"は、フック自体が失敗した場合の扱いを指定します。v2.1.295で追加された設定です。今回は、この設定をスキルのSKILL.mdに書きました。v2.1.295のリリースノート

検証日は2026年10月15日です。手元の環境は次のとおりでした。

Claude Code 2.1.295
Node.js v25.4.0
macOS 26.6.2 / Apple Silicon

試すのは、固定の文字列を標準出力に出すだけのコマンドです。AWSや本番環境への操作は含めていません。

printf 'HOOK_PROBE_EXECUTED\n'

検査スクリプト付きのスキルを作る

検証用の一時ディレクトリに、次の構成を用意しました。

.claude/
└── skills/
    └── hook-probe/
        ├── SKILL.md
        └── scripts/
            └── check-command.cjs

SKILL.mdのフロントマターは次の内容です。onFailureはPreToolUseの直下ではなく、個々のフックに付けています。

---
name: hook-probe
description: Run exactly one harmless command to measure skill-hook failure handling.
disable-model-invocation: true
hooks:
  PreToolUse:
    - matcher: "Bash"
      hooks:
        - type: command
          command: "node"
          args: ["${CLAUDE_PROJECT_DIR}/.claude/skills/hook-probe/scripts/check-command.cjs"]
          onFailure: "block"
          timeout: 10
---

本文には、先ほどのprintfをBashで1回だけ実行する指示を書きました。ブロックされたら止まり、スクリプトの修復、再試行、別ツールへの切り替えはしないようにしています。今回は自動修復の能力ではなく、1回の呼び出しを止められるかを見たいからです。

検査スクリプトは次のとおりです。標準入力のJSONからコマンドを読み、今回のprintfと一致するかを調べます。検査が実際に走った証拠として、ローカルのhook-audit.jsonlにも記録します。

// Deliberately tiny checker: allow only our harmless marker command.
let input = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', chunk => input += chunk);
process.stdin.on('end', () => {
  const event = JSON.parse(input);
  const expected = "printf 'HOOK_PROBE_EXECUTED\\n'";
  const allowed = event.tool_name === 'Bash' && event.tool_input.command === expected;
  require('fs').appendFileSync('hook-audit.jsonl', JSON.stringify({command: event.tool_input.command, allowed}) + '\n');
  if (!allowed) { console.error('Unexpected command in probe'); process.exit(2); }
});

これは比較実験用の小さな検査です。任意のシェルコマンドを安全に判定するスクリプトではありません。

4つの条件を別セッションで実行する

比較したのは、スクリプトの有無とonFailureの有無を組み合わせた4条件です。欠落条件ではSKILL.md内のパスはそのままにして、参照先のcheck-command.cjsだけを配置しません。

条件ごとに新しい一時ディレクトリとセッションを使いました。スキル内フックは呼び出し後もセッションに残る仕様なので、前の条件が混ざるのを避けるためです。フックの公式リファレンス

各ディレクトリをカレントにして、次のように実行しています。onFailureなしの条件では、その1行を取り除いています。

claude -p '/hook-probe' \
  --output-format stream-json \
  --verbose \
  --no-session-persistence \
  --setting-sources project \
  --strict-mcp-config \
  --mcp-config '{"mcpServers":{}}' \
  --tools 'Bash,Skill' \
  --allowedTools 'Skill' 'Bash(printf *)' \
  --max-budget-usd 1 \
  --debug-file "$PWD/debug.log" \
  > stream.jsonl 2> stderr.txt

実行にはClaude Codeの認証と利用枠が必要です。projectだけを設定の読み込み元にし、MCPサーバーは空にしました。Bashの許可も、この実験ではprintfに絞っています。

ここで一度詰まりました。最初は実行された証拠を残すため、printfの出力をファイルへリダイレクトしていました。ところが、そちらはファイル書き込みの承認で止まりました。検査スクリプトが正常でも止まるため、フックの比較になりません。

そこでファイル出力をやめ、Bashのtool_resultに返る標準出力で判定しました。以降の表は、この修正後に各条件を1回ずつ実行した結果です。

ファイルが欠けたときだけ差が出た

検査スクリプトonFailureBashの結果claude -pの終了コード
正常省略実行された0
正常block実行された0
欠落省略実行された0
欠落block実行前にブロック0
実行ログから作成した4条件の比較レポート。ローカルHTMLを撮影した画像で、Claude Codeの製品画面ではありません。
実行ログから作成した4条件の比較レポート。ローカルHTMLを撮影した画像で、Claude Codeの製品画面ではありません。

正常な2条件では、検査ログにallowed: trueが残り、Bashの結果としてHOOK_PROBE_EXECUTEDが返りました。

欠落させてonFailureを省いた条件でも、同じ文字列が返りました。デバッグログにはNode.jsのMODULE_NOT_FOUNDと終了コード1が記録されています。検査が成功したのではなく、検査が失敗したままBashが動いた状態です。

一方、欠落させてonFailure: "block"を指定した条件では、Bashのtool_result.is_errorがtrueになりました。エラーには次の文言が含まれていました。

failed; blocking because onFailure is "block"

この条件では、成功したツール結果としてのHOOK_PROBE_EXECUTEDは返っていません。Claudeの最終回答にも止まった旨は書かれていましたが、判定にはその説明ではなく、Bashのツール結果を使いました。

ブロック時のtool_resultを抜粋した実行ログのレポート。スタックトレースを省略しており、Claude Codeの製品画面ではありません。
ブロック時のtool_resultを抜粋した実行ログのレポート。スタックトレースを省略しており、Claude Codeの製品画面ではありません。

止まったのに、CLIの終了コードは0だった

少し気になったのは、ブロックされた条件でもclaude -p自体の終了コードが0だった点です。今回の実行では、Claudeがブロックを受け取り、その理由を回答してセッションを終えました。

つまり、この比較ではCLIの終了コードだけを見ても、Bashが動いたかどうかは分かりませんでした。自動検証に使うなら、ツール呼び出しと対応するtool_resultを確認する必要があると思います。プロンプトにも同じ文字列があるため、ログ全体からHOOK_PROBE_EXECUTEDを検索するだけでも判定できません。

また、今回止まったのは、そのフックが対象にしたBashの呼び出しです。以後のセッション全体を強制終了したわけではありません。matcher: "Bash"で別のツールまで制御できる、とも考えない方がよさそうです。

スクリプト本体が届いているかも試しておく

自分の環境では、スキル内のPreToolUseでもonFailure: "block"が効きました。正常な検査は通り、検査ファイルが欠けた場合にはコマンドの手前で止まりました。

検査スクリプトの判定ロジックだけ確認して満足しそうでしたが、配布するならファイル欠落も試しておきたいと感じました。設定を1行省いた場合との違いを、実際に見られたのはよかったです。

今回はファイル欠落だけの検証です。タイムアウト、HTTPフック、異常なJSON出力、exit 2による意図的な拒否、旧バージョンとの比較は行っていません。次は処理が返らない検査スクリプトでも、止まり方を確認してみたいと思います。

ここまで読んでいただき、ありがとうございます。もしこの記事の技術や考え方に少しでも興味を持っていただけたら、ネクストのエンジニアと気軽に話してみませんか。

  • 選考ではありません
  • 履歴書不要
  • 技術の話が中心
  • 所要時間30分程度
  • オンラインOK

エンジニアと話してみる

関連リンク

AI・クラウド・データ分析のご相談はネクスト株式会社までお問い合わせください。