1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Claude Code の hook はパスの打ち間違いで素通しになる — onFailure で止める

1
Posted at

この記事で分かること

  • 守りの hook は、登録したパスを 1 文字間違えるだけで動かなくなり、止めたいコマンドがそのまま実行されること
    • 自分の設定が今その状態になっていないか、この記事の手順で数分で確かめられます
  • 設定に 1 行足すと、hook が動けないときは「止める」側に倒せること
    • パスを打ち間違えても、鍵ファイルが消されずに済みます
  • 30 行ほどのスクリプトで、打ち間違えたパスの場所を名指しで見つけられること
    • 止まったときに、どこを直せばよいかがすぐ分かります
  • 弱い点: 「止める」側に倒すと、パスを直すまで Bash がまったく使えなくなること
    • どの hook に付けるかを決める材料になります

想定読者

  • Claude Code の hook で、危ないコマンドを止める設定をしている人、またはこれからする人
  • 前提: settings.json(Claude Code の設定ファイル)を自分で書き換えたことがある

検証環境

次の表は、この記事の実験に使った環境です。hook の仕様は版で変わるので、claude --version で自分の版を確かめてから試してください。

道具 版
Claude Code 2.1.307
Node.js 22.22.2
OS WSL2(Linux 6.6)

1. 先に結論 — やることは 2 つ

hook(フック)は、Claude Code がコマンドを実行する直前などに、自分で用意したスクリプトを自動で走らせる仕組みです。
鍵ファイルを守る hook を使っているなら、やることは次の 2 つです。

やること 1: 守りの hook に onFailure を 1 行足す

次の JSON は、Claude Code の設定ファイル settings.json のうち、hook を登録している部分です。最後の 1 行が、足すところです。

.claude/settings.json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/hooks/guard.mjs",
            "onFailure": "block"
          }
        ]
      }
    ]
  }
}

これで、hook のパスを打ち間違えて hook が動けないときも、Claude Code が実行しようとしたコマンドは実行されずに止まります。
Claude Code 2.1.295 以降で使えます(公式: Block the action when a hook fails)。

やること 2: 登録したファイルが本当にあるかを確かめる

6 章のコードを、ホームフォルダに check-hooks.mjs という名前で保存します。
そのあと、ターミナル(コマンドを打ち込む画面)で次のコマンドを打ちます。

まず、パソコン全体で使う設定(ホームの .claude/settings.json)を調べるコマンドです。

node ~/check-hooks.mjs ~/.claude/settings.json

次に、プロジェクトごとの設定を調べるコマンドです。
1 行目の cd は「そのフォルダへ移動する」コマンドで、~/my-app の所は自分のプロジェクトのフォルダに置き換えます。

cd ~/my-app
node ~/check-hooks.mjs

パスの打ち間違いがあれば、そのパスが名指しで表示されます。

確かめた結果

次の表は、私が Claude Code に「偽の鍵ファイルを消して」と頼んだときの結果です(試し方は 3 章)。

設定 偽の鍵 何が起きたか
パスが正しい ✅ 残った hook が止めた
パスを 1 文字間違えた ❌ 消えた 削除が実行された
間違えたまま onFailure を足した ✅ 残った Claude Code が止めた

ここから先は、onFailure の 1 行と検査スクリプトの 2 つがなぜ必要なのかと、私がどう確かめたかを順に説明します。

2. 困っていたこと — 打ち間違いに気付く手段が無かった

前回の記事では、Claude Code が .env や鍵ファイルを消すのを hook で止めました(前回の記事)。
その記事を書き終えたあと、ふと自分に問いかけました。
「登録したパスを打ち間違えたら hook は動かない。では、動かないとき、実際には何が起きるのか?」
前回の記事には、その答えを書いていませんでした。

答えは公式ドキュメントにありました。
「settings.json のパスを打ち間違えると、その hook は一度も動かない」(Other exit codes の節)。
「パスの間違った守りの hook は、すべてを素通しにする」(Block the action when a hook fails の節)。

困るのは、hook で鍵ファイルを守っているつもりの人です。
settings.json に guard.mjs を gaurd.mjs と 1 文字入れ替えて登録しても、今回の実験では、Claude Code は起動したときに何も知らせてくれませんでした。
そのまま使い続けると、「守りがある」という安心だけが残り、実際には何も守られていない状態が続きます。

次の章で、この素通しを私の手元で本当に起こしてみます。

3. 確かめ方 — 偽の鍵と 1 文字違いのパス

私が用意したのは、偽の鍵ファイル secrets/fake.p8(中身は「FAKE KEY」という文字だけ)と、守りの hook hooks/guard.mjs の 2 つです。

次のコードが、守りの hook の全部です。
このスクリプトは、Claude Code が実行しようとするコマンドに secrets/ が入っていたら、その実行を止めます。

hooks/guard.mjs
#!/usr/bin/env node
import { readFileSync } from 'node:fs';

const input = JSON.parse(readFileSync(0, 'utf8'));
const command = input.tool_input?.command ?? '';

if (command.includes('secrets/')) {
  console.error(`secrets/ を含むコマンドは止めます: ${command}`);
  process.exit(2);
}
process.exit(0);
  • Claude Code は、実行しようとしているコマンドを JSON にして渡してきます。readFileSync(0) で受け取り、tool_input.command にコマンドの文字列が入っています
  • 終了コード(スクリプトが終わるときに返す番号)が 2 なら止め、0 なら通します(Exit code output の節)
  • パスだけで起動するので、chmod +x hooks/guard.mjs で実行できる印を付けておきます

settings.json に登録するパスだけを変えて、Claude Code に rm secrets/fake.p8(偽の鍵を消すコマンド)の実行を 1 回だけ頼みました。
次のコマンドが、1 回分の実行です。

claude -p 'Run exactly this one Bash command and nothing else: rm secrets/fake.p8' \
  --setting-sources project \
  --allowedTools 'Bash(rm *)'
  • -p: 対話画面を開かずに、1 回だけ頼んで結果を出す
  • --setting-sources project: 実験用フォルダの設定だけを読み、ふだんの hook を混ぜない
  • --allowedTools 'Bash(rm *)': rm を確認なしで許す(hook だけが止め役になるようにするため)

4. 結果 — 打ち間違えると鍵が消え、onFailure を足すと残った

パスを間違えると、hook は失敗して素通しになった

settings.json に hooks/gaurd.mjs と打ち間違えて登録したときは、rm がそのまま実行され、偽の鍵は消えました。
Claude Code は英語で「実行しました。出力はありません」と返し、claude 自体も成功の 0 で終わりました。

hook の記録(--debug-file で残せるログ。Debug hooks の節)には、次の行だけが残っていました。

Hooks: PreToolUse:Bash ["$CLAUDE_PROJECT_DIR"/hooks/gaurd.mjs]
finished with status 127
/bin/sh: 1: <作業フォルダ>/hooks/gaurd.mjs: not found

127 は、シェルが「そのファイルが見つからない」ときに返す番号です。
Claude Code は 0 と 2 以外の番号を「止めない失敗」として扱い、コマンドを先に進めます(Exit code output の節)。

公式によると、対話画面では会話の中に hook error の通知が出ます(Other exit codes の節)。
ただ、作業の流れの中で、会話にまぎれた hook error の 1 行に気付けるかは、人の注意しだいです。

onFailure を足すと、間違えたままでも止まった

同じ打ち間違いのまま "onFailure": "block" を足すと、rm は実行されず、偽の鍵は残りました。
Claude Code には、コマンドを止めた理由として、次の文が返ってきました。

PreToolUse:Bash hook error: ["$CLAUDE_PROJECT_DIR"/hooks/gaurd.mjs]:
failed; blocking because onFailure is "block"
/bin/sh: 1: <作業フォルダ>/hooks/gaurd.mjs: not found

"onFailure": "block" の 1 行で、守りは「壊れたら開く扉」から「壊れたら閉まる扉」に変わります。
次の章で、この設定の付け方と、引き換えに失うものを説明します。

5. onFailure の付け方 — 守りの hook にだけ付ける

項目の意味

1 章の JSON の項目は、次の意味です。

  • matcher: "Bash": Bash の実行の前にだけ走らせる
  • command: 走らせるスクリプト。$CLAUDE_PROJECT_DIR は今開いているフォルダの場所に置き換わる。フォルダ名に空白があっても切れないよう \" で囲んでいる
  • onFailure: "block": hook が失敗したら、Bash の実行を止める

引き換えに、Bash が全部止まる

「失敗」には、パスの打ち間違いのほか、スクリプトが途中で落ちる場合や時間切れも入ります(同じ節)。
そのあいだは rm だけでなく、ls のような無害なコマンドも含めて Bash がすべて止まります。

元に戻すときは、自分でエディタ(メモ帳や VS Code など)を開いて settings.json のパスを直し、Claude Code を開き直します。

そのため、onFailure は鍵を守る hook のような「止まってでも守りたいもの」にだけ付けます。
整形などの hook に付けると、失敗のたびに作業が止まります。

全部止まれば「何かおかしい」とすぐ気付けますが、原因がパスの打ち間違いだと分かるまでに時間がかかります。
次の章で、その場所を教えてくれる検査を作ります。

6. 検査スクリプト — 登録したファイルがあるかを確かめる

次のスクリプトが、検査の全部です(30 行ほど)。
空行で区切った 3 つのまとまりに分かれています。

check-hooks.mjs
#!/usr/bin/env node
import { existsSync, readFileSync } from 'node:fs';
import { homedir } from 'node:os';
import { resolve } from 'node:path';

const settingsPath = resolve(process.argv[2] ?? '.claude/settings.json');
const projectDir = process.cwd();
const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));

const missing = [];
for (const groups of Object.values(settings.hooks ?? {})) {
  for (const group of groups) {
    for (const hook of group.hooks ?? []) {
      const words = [hook.command ?? '', ...(hook.args ?? [])].join(' ').split(/\s+/);
      for (const word of words) {
        if (!/\.(mjs|js|sh|py)["']?$/.test(word)) continue;
        const path = word
          .replace(/["']/g, '')
          .replace(/^\$\{?CLAUDE_PROJECT_DIR\}?/, projectDir)
          .replace(/^(~|\$\{?HOME\}?)(?=\/)/, homedir());
        if (!existsSync(resolve(projectDir, path))) missing.push(word);
      }
    }
  }
}

if (missing.length > 0) {
  console.error('登録先のファイルが見つからない hook:');
  for (const word of missing) console.error(`  ${word}`);
  process.exit(1);
}
console.log('hook の登録先はすべて実在します');
  • 1 つ目(const settingsPath から 3 行): 読む設定ファイルを決める。引数が無ければ、今いるフォルダの .claude/settings.json を読む
  • 2 つ目(const missing から始まる繰り返し): 登録した hook のコマンドを空白で区切り、.mjs などで終わる言葉をパスとみなす。$CLAUDE_PROJECT_DIR と ~ を実際の場所に置き換え、ファイルがあるかを確かめる
  • 3 つ目(最後の if): 見つからないものがあれば一覧を出して終了コード 1、無ければ 0 で終わる

次の出力は、パスを打ち間違えた設定のプロジェクトで、検査スクリプトを走らせた結果です。

$ node ~/check-hooks.mjs
登録先のファイルが見つからない hook:
  "$CLAUDE_PROJECT_DIR"/hooks/gaurd.mjs

正しいパスの設定では「hook の登録先はすべて実在します」と出て、終了コードは 0 でした。
ホームに置いた hook(~/.claude/hooks/ など)の打ち間違いも、同じように名指しで出ました。

settings.json の hook を書き換えたら、その場で検査スクリプトを 1 回走らせます。

7. 弱点と限界

  • 検査は「ファイルがあるか」しか見ません。スクリプトが途中で落ちる場合や、止める条件そのものの間違いは、偽の鍵で実際に試して確かめます
  • .mjs .js .sh .py 以外のスクリプト、空白を含むパス、シェルの別名を使う書き方は、検査の対象外です
  • 今回は -p で頼む形だけを試し、対話画面での hook error の見え方は撮っていません

8. まとめ

  • パスを 1 文字打ち間違えると、守りの hook は動かず、止めたい rm がそのまま実行された
  • "onFailure": "block" を 1 行足すと、打ち間違えたままでも実行が止まり、鍵は残った
  • 30 行ほどの検査で、打ち間違えたパスを名指しで見つけられた
  • これで、打ち間違えても鍵は消えず、止まったときもすぐ直せる
  • 引き換えに、block の間は Bash がすべて止まる。だから守りの hook にだけ付ける
  • 次の一歩: 自分の守りの hook に onFailure を足し、node ~/check-hooks.mjs ~/.claude/settings.json を 1 回走らせてみてください

参考

1
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?