この記事で分かること
- 守りの 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 行が、足すところです。
{
"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/ が入っていたら、その実行を止めます。
#!/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 つのまとまりに分かれています。
#!/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 回走らせてみてください
参考
- Hooks reference — Exit code output: 終了コード 0・2・それ以外の扱い(3 章・4 章)
- Hooks reference — Other exit codes: パスを打ち間違えると hook が一度も動かないこと(2 章・4 章)
-
Hooks reference — Block the action when a hook fails:
onFailure: "block"、失敗に入るもの、必要な版(1 章・5 章) -
Hooks reference — Debug hooks:
--debug-fileでログを残す方法(4 章) - 前回の記事(鍵ファイルを守る hook): 守りの hook の作り方(2 章)