Claude Codeに危険コマンドをブロックさせたり、ファイル編集前にlint(自動チェッカー)を走らせたい人向け
Claude Codeに危険なコマンド(rm -rfやgit push --force)を打たせたくない時、ファイル編集の直前にlintやtypecheckを走らせて違反を機械的に止めたい時、特定のファイル(.envなど)をAIに触らせたくない時に、settings.jsonのhooks欄にPreToolUse設定を書いて使う
料理ブログ用のリポジトリで、Claude Codeに git commit を打たせる前に、毎回手動で npm run lint を回して整形ミスを潰している人は多いと思います。
PreToolUseフックは、Claude Codeが「次に何かを実行する」と決めた瞬間と、実際に動かす瞬間の間に、私が書いたスクリプトを差し込む仕掛けです。
料理ブログのMarkdownを編集する直前に lint を走らせる、 rm -rf を書いてきたら問答無用で止める、そんな「ツール実行直前の検問所」を私の手で作れます。
噛み砕くと
キッチンの入口に立っている味見係を想像すると分かりやすい。
料理人(Claude Code)が皿を出そうとすると、味見係が先に塩加減をチェックします。
OKなら通す、塩辛すぎたら「これは出させない」と止める、ちょっと足りないなら「お客に確認してから」と保留する。これがPreToolUseの3つの判定です。
料理人が皿を出した後ではなく、出す直前に介入する点がポイント。
大事な前提:PreToolUseは「決まったけど、まだ動いてない」瞬間に走る
公式docsの一文を引きます。
Runs after Claude creates tool parameters and before processing the tool call.
つまりClaudeが「Bashで npm run build を叩こう」と決めた直後、実際に叩く前。
この瞬間に私が書いたスクリプトを呼び、許可・拒否・保留・差し替えのいずれかを返せます。
逆に、ツールが動き終わった後の結果を見て何かしたいなら、それはPostToolUseの仕事で、PreToolUseの守備範囲ではないです。
画面に何が出るのか
設定は ~/.claude/settings.json (またはプロジェクトの .claude/settings.json )の hooks 欄に書きます。
料理ブログで「 content/recipes/ 配下の .md ファイルを編集する直前に lint を走らせる」例だとこうなる。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"if": "Edit(content/recipes/*.md)",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/lint-on-edit.sh"
}
]
}
]
}
}
Claude Codeが content/recipes/curry.md を編集しようとすると、画面はこう動きます。
[Claude] Editing content/recipes/curry.md ...
[Hook] PreToolUse: lint-on-edit.sh fired
[Hook] markdownlint: 0 errors
[Hook] permissionDecision: allow
[Claude] Edit applied (3 lines changed)
lintが通ればそのまま編集が進む、 curry.md に1行でもエラーがあれば、Claude側に「lintエラーで止めた」と伝わって編集はキャンセルされる仕組みです。
料理ブログ「cooking-blog」での再現手順
Hugoで作っている料理ブログのリポジトリで、レシピMarkdownの編集前にlintを走らせる流れを4ステップに分けて組みます。
ステップ1: settings.json にPreToolUseセクションを書く
プロジェクトルートで .claude/settings.json を開き、上の例の hooks ブロックを追記します。
matcher は Edit|Write 、 if は Edit(content/recipes/*.md) 、 command はこれから作るスクリプトのパスを指定する形。
ここで $CLAUDE_PROJECT_DIR を使うのがコツで、絶対パスをハードコードしなくて済みます。
ステップ2: ~/hooks/lint-on-edit.sh を作る
プロジェクトの .claude/hooks/lint-on-edit.sh に下記を書きます。
#!/bin/bash
# 入力JSONからファイルパスを取り出す
FILE_PATH=$(jq -r '.tool_input.file_path')
# markdownlint で対象ファイルだけチェック
if ! npx markdownlint "$FILE_PATH" 2>&1; then
jq -n --arg p "$FILE_PATH" '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: "markdownlint failed on \($p). Fix lint errors before editing."
}
}'
exit 0
fi
exit 0 # lint passed, allow edit
権限を実行可能にしておく。
$ chmod +x .claude/hooks/lint-on-edit.sh
ステップ3: 試す
Claude Codeを開いて、 content/recipes/curry.md を1行編集する指示を出します。
裏で lint-on-edit.sh が呼ばれ、lintが通れば編集が走る。
動作を確認したいなら claude --debug で起動すると、フックが何を返したかログで全部見えます。
ステップ4: わざとlintエラーを作って block 動作を確認
Claude Codeに「 curry.md の見出しを # # ## みたいな壊れた書式で書いて」と指示。
markdownlintが怒り、フックが permissionDecision: "deny" を返し、Claude側は「lintで弾かれた」と認識して別の書き方を試し直します。
これで「Claude Codeに無条件で書かせる」状態から「ルール違反は機械的に止める」状態に切り替わる。私はこの一段だけでもコミット前のヒヤヒヤが半分くらい減ると感じています。
入力と出力の早見表
PreToolUseのスクリプトには、Claude Codeが標準入力でJSONを渡してきます。中身は次の通り。
| 入力フィールド | 意味 |
|---|---|
session_id |
今のClaude Codeセッションを識別するID |
transcript_path |
会話ログのJSONLファイルの場所 |
cwd |
Claude Codeを起動したフォルダ |
permission_mode |
今の権限モード( default など) |
hook_event_name |
常に "PreToolUse" |
tool_name |
これから走るツール名( Bash 、 Edit など) |
tool_input |
そのツールに渡される予定の中身(コマンド本文、ファイルパス等) |
tool_use_id |
このツール呼び出しを識別するID |
返す側は hookSpecificOutput オブジェクトを標準出力にJSONで吐く形。
permissionDecision |
挙動 |
|---|---|
"allow" |
ツール実行を許可。ユーザーの確認画面を出さずに通す |
"deny" |
ツール実行を止める。 permissionDecisionReason に書いた理由がClaudeに伝わる |
"ask" |
ユーザーに確認画面を出す。 [Project] や [User] ラベル付きで「このフックが ask した」と表示される |
"defer" |
判断を保留。 -p モード(Agent SDK等の自動実行)でだけ意味があり、対話モードでは普段使わない |
複数のフックが違う返事をした時は、 deny > defer > ask > allow の順で強い返事が勝ちます。
つまり1つでも deny を返せばツールは止まる、ここがポリシー強制の要。
使いどころ3シナリオ
シナリオ1: レシピMarkdownの編集前にmarkdownlintを走らせる
料理ブログ cooking-blog の content/recipes/ 配下のMarkdownは、Hugoのfrontmatterや見出し階層が壊れるとビルドが通らなくなる代物です。
matcher Edit|Write + if Edit(content/recipes/*.md) でフックを仕込み、 markdownlint がエラーを返したら "deny" でEditを止めます。
Claude Codeが書いた壊れたMarkdownがそのままコミットされる事故は、これでほぼ消える。
シナリオ2: rm -rf や危険な git コマンドを問答無用でブロック
matcher Bash + if Bash(rm -rf *) でフックを仕込み、 permissionDecision: "deny" を返すスクリプトを置けば、Claude Codeが rm -rf node_modules を打ってきても通らなくなります。
git push --force や git reset --hard も同じパターンで止められる。
料理ブログのリポジトリで「気付いたら過去のコミットが消えてた」を構造的に防げます。
シナリオ3: クレデンシャルファイルの編集禁止
cooking-blog の .env や credentials.json をClaude Codeに触らせたくない時は、matcher Edit|Write|Read + if Edit(.env) 系で止めます。
permissionDecision: "deny" + permissionDecisionReason: "Secrets file. Edit by hand." を返せば、Claudeに「これは触らせない方針」が伝わる。
.gitignore での除外と組み合わせると、書き込み事故と読み取り事故の両方を潰せます。
初心者が踏みやすい落とし穴
- matcherを
"^Bash$"のつもりで"^Bashh"と書いて全Bashが素通り。matcherはJavaScript正規表現として評価される(記号を含む場合)ので、タイポすると「何にもマッチしない=ノーガード」状態になります。逆に"*"や省略は全ツールマッチで、ガバガバすぎる事故も起きる - exit code 1 はブロッキングではない。Unixの慣習だと「1=失敗」だが、Claude Codeは exit 1 を「フック自体がコケた」と扱って、ツールはそのまま実行されてしまう。ポリシーを強制したいなら
exit 2か、permissionDecision: "deny"をJSONで返す方を使う - stdoutとstderrの行き先が違う。
exit 0の時、stdoutに吐いたJSONだけがClaude Codeに読まれる。stderrはデバッグログ行きでClaudeのコンテキストには流れない。exit 2の時だけ、stderrの中身がClaudeにエラー文として渡る逆転仕様で、ここが地味に混乱しやすいポイント - jq が無い環境で詰む。フックスクリプトはJSON入出力なので、jqがインストールされてない環境(最小構成のDocker、CIランナー等)だとスクリプトがパース段階で死にます。
jq --versionで先に存在確認するか、Pythonのjson.loadsで書く逃げ道を持っておくと安全 - 非推奨の書式
{"decision": "block"}をまだ使っている記事が多い。これは{"hookSpecificOutput": {"hookEventName": "PreToolUse", "permissionDecision": "deny"}}に置き換わりました。古いサンプルをコピペすると今は動いても将来詰む可能性あり - 10,000文字を超える
additionalContextはファイルに退避される。lintのフルログをそのまま流し込むと「プレビュー+ファイルパス」に置換されてしまう。Claudeに伝えたい要点は最初の数百字に圧縮するのが現実解 - matcherを
"Bash|Edit"と書きたくて"Bash, Edit"や"Bash Edit"と書いて発火しない。区切り文字は|専用。これも--debugでフックが呼ばれてるかすぐ分かるので、最初は必ずdebug起動で動作確認するのが鉄則
関連するコマンド・ツールへの動線
- Hooks - フック全体の概念ページ。発火タイミングが8種類あって、PreToolUseはそのうちの1つ。仕組み全体を先に押さえるならこちら
- PostToolUse - ツール実行が「終わった直後」に発火する兄弟フック。整形後のテスト、保存後の通知など事後処理に使う
- settings.json - フックを書き込む設定ファイル本体。ユーザー全体・プロジェクト・ローカルの3層構造を理解しておくと、フックの効く範囲を制御しやすい
- Bash - PreToolUseで一番ブロックされる対象のツール。
rm -rfやgit push --forceをフックで止める時の発火元 - Edit - ファイル書き換えツール。lintやtypecheckをフックで挟む時の典型的な発火元
参考リンク
書き方
.claude/settings.json の hooks.PreToolUse 配列に、matcher(対象ツールの絞り込み)と command(呼び出すスクリプト)を書く。スクリプトは標準入力でJSONを受け取り、標準出力にhookSpecificOutput JSONを返すか、exit 2 でブロックする
やってみるとこうなる
入力
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"if": "Edit(content/recipes/*.md)",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/lint-on-edit.sh"
}
]
}
]
}
}
出力例
[Claude] Editing content/recipes/curry.md ...
[Hook] PreToolUse: lint-on-edit.sh fired
[Hook] markdownlint: 0 errors
[Hook] permissionDecision: allow
[Claude] Edit applied (3 lines changed)
このページに出てきた言葉
- フック
- プログラムが特定の動作をする直前や直後に、自前の処理を割り込ませる仕掛け
- matcher
- 「どのツールが呼ばれたら発火するか」を絞る条件。<code>Bash</code>、<code>Edit|Write</code>、<code>*</code>などを書く
- if フィールド
- matcherより細かい条件。<code>Edit(*.ts)</code>のようにファイル種別やコマンド種別まで見る
- permissionDecision
- フックがClaudeに返す判定。<code>allow</code> / <code>deny</code> / <code>ask</code> / <code>defer</code> の4値
- lint
- コードや文章のスタイル違反・タイポ・壊れた構文を機械的に検出するチェッカー
- 標準入力 / 標準出力
- プログラム同士でデータを受け渡しするための入口と出口。フックスクリプトはJSONをこの経路でやり取りする
- jq
- コマンドラインでJSONを切り貼りする定番ツール。フックスクリプトでよく使う
- exit code
- プログラム終了時に返す数字。Claude Codeのフックでは <code>0</code>=成功、<code>2</code>=ブロック、その他=非ブロッキングエラー
関連項目
公式ドキュメント
https://docs.claude.com/en/docs/claude-code/hooks#pretooluse