Advanced (更新: 2026/6/7)

Claude Code Hooks 実装ガイド:PreToolUseで危険コマンドを止める設定例

Claude Code Hooks の使い方を実装目線で。PreToolUse/PostToolUse/Stop の役割、settings.json 登録、exit code 2 とフックの権限上書きまで、コピペで動く設定例で解説。

Claude Code Hooks 実装ガイド:PreToolUseで危険コマンドを止める設定例

「format は毎回かけてって言ったよね?」

Claude Code に作業を任せはじめた頃、僕はこのセリフを1日に何度もタイプしていました。整形して、テスト回して、.env は触らないで、本番DBに DROP は投げないで——。賢いのに、毎回こちらが横で見張っていないと不安。これじゃ自分でやったほうが早い、と何度も思いました。

転機は Hooks を入れたときです。Hooks は、Claude が道具(ツール)を使う前後に、僕が決めたスクリプトを勝手に走らせてくれる仕組み。要するに自動で立つ見張り番です。「危ないコマンドは止める」「編集したら整形する」を、口で言う代わりに設定ファイルへ書いておく。それだけで横についている必要がなくなりました。

この記事は、その Hooks をピラー(土台)として一気にまとめたものです。イベントの種類、settings.json への登録、exit code 2 でのブロック、そしてハマりやすい「フックで権限を上書きできるのか問題」まで、コピペで試せる形で置いていきます。

この記事の要点

  • Hooks は Claude Code がツールを使う前後で走る自前スクリプト。PreToolUse はブレーキ、PostToolUse は整備、UserPromptSubmit は受付記録、Stop は退出前チェック。
  • 登録先は .claude/settings.jsonhooks キー。matcher で対象ツールを絞り、type: "command" でコマンドを指定する。
  • フックの止め方は2通り。exit code 2(stderr が Claude へ返る)と、PreToolUsepermissionDecision: "deny" を JSON で返す方法。
  • PreToolUse の JSON 判定は settings.jsondeny/ask上書きできるallow を返せばルールで拒否される操作も通る——だからフック自体が攻撃面になる。
  • まずは「ログ保存」と「保存時 format」から。慣れたら危険コマンドのブロックを足す。最初から全部を止めにいかない。

Hooks とは何か:3秒で言うと「自動で立つ見張り番」

Hooks を一言でいうと、Claude Code の作業の節目に差し込む自分のプログラムです。Claude が「このファイルを書く」「このコマンドを叩く」と動こうとした瞬間に割り込んで、止めたり、記録したり、後始末をしたりできます。

勘違いしやすいのですが、Hooks は「賢い自動化AI」ではありません。むしろ逆で、決め打ちの、機械的なルールです。AIの気分で動く部分をあえて減らし、「この条件なら必ず止まる」を保証するためにあります。工事現場の安全帯みたいなものですね。本人の能力は変えず、落ちないようにするだけ。

なぜわざわざ外側に置くのか。プロンプトで「危ないことはしないでね」と頼む方法もありますが、それはお願いベースで、守られる保証がありません。Hooks はコードなので、Claude の気分に左右されず、必ず実行されます。「お願い」と「ガードレール」の違いです。

まず覚える4つのイベント

Hooks のイベントは実はかなり多くて、SessionStartPreCompactSubagentStop など十数種類あります。でも最初から全部を覚える必要はありません。事故防止と品質改善に効くのは、まず次の4つだけです。

イベント走るタイミング向いている用途
UserPromptSubmit依頼が Claude に渡る前依頼文をログに残す、秘密っぽい文字列を警告
PreToolUseツール実行の直前rm -rf や本番DB操作を止める
PostToolUseツール実行の成功直後編集後に format・lint・関連テスト
StopClaude が応答を終える時未解決の競合や未実行テストの確認

PreToolUse がブレーキ、PostToolUse が整備、UserPromptSubmit が受付記録、Stop が退出前チェック——この4語で覚えると迷いません。自動化に慣れていないチームほど、いきなり全部をブロックしにいかず、ログ保存と format だけから始めるのが安全です。

設定は基本 .claude/settings.json に置きます。個人の実験なら .claude/settings.local.json に逃がす手もありますが、チームで共有したい品質ルールはプロジェクト側(settings.json)に置くほうが再現します。設定はユーザー・プロジェクト・ローカルなど複数のスコープで読まれるので、どこに置いたフックなのかは README かコメントに一言残しておくと、後で自分が混乱しません。

settings.json への登録:最小構成をまるごと置く

最初の設定は、依頼ログ・危険コマンド防止・編集後チェックの3つで十分です。以下を .claude/settings.json に置きます。スクリプト本体は後のセクションで作ります。

{
  "hooks": {
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "node .claude/hooks/log-prompt.mjs"
          }
        ]
      }
    ],
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "node .claude/hooks/block-dangerous-command.mjs"
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "node .claude/hooks/run-quality-checks.mjs",
            "timeout": 120
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "node .claude/hooks/stop-summary.mjs"
          }
        ]
      }
    ]
  }
}

構造はシンプルです。イベント名の下に配列、その中の matcher で対象ツールを絞り、hooks 配列に実行するコマンドを並べます。matcher を省くか "*" にすると全ツールが対象。"Edit|Write|MultiEdit" のように | で複数指定もできます(英数字・アンダースコア・| だけなら文字列一致、それ以外の記号が入ると正規表現として解釈される点だけ注意)。

timeout は秒数で、超えるとそのフックは打ち切られます。command 型のデフォルトは長め(公式では600秒)ですが、UserPromptSubmit だけは入力をブロックしないよう既定が30秒に下がります。重い処理を載せるイベントでは明示しておくと安心です。

ここでの設計のコツは、役割ごとにイベントを分けること。PreToolUse に format やテストまで詰め込むと、ツールを叩くたびに待たされます。逆に PostToolUse で危険コマンドを止めようとしても、もう実行された後なので手遅れ。「どこで止めたいか/どこは記録だけでいいか」を分けると運用が安定します。

フックが受け取る入力と、止め方の2系統

スクリプトを書く前に、フックの入出力を押さえます。コマンド型のフックは、標準入力(stdin)に JSON が1つ流れてくる。中身はこんな形です。

{
  "session_id": "abc123",
  "transcript_path": "/home/user/.claude/projects/xxx/transcript.jsonl",
  "cwd": "/home/user/my-project",
  "permission_mode": "default",
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "tool_input": {
    "command": "rm -rf /tmp/build"
  }
}

tool_name でどのツールか、tool_input でその引数が取れます。Bash なら tool_input.command、ファイル編集なら tool_input.file_path といった具合です。cwdsession_id も入っているので、ログに添えると後で追いやすくなります。

止め方は大きく2系統あります。ここがフックの心臓部です。

  1. exit code 2 で止める。 終了コード2は「ブロッキングエラー」。PreToolUse ならツールの実行そのものを止めます。このとき Claude は stdout を見ず、stderr に書いた文字列をエラー理由として読みます。だから停止理由は console.error で書く。PostToolUseexit 2 を返しても、ツールはもう動いた後なので「止める」のではなく「stderr を Claude に見せる」だけになります。Stop での exit 2 は「終わらせない=会話を続行させる」という別の意味になる点も覚えておくと事故りません。

  2. JSON で判定を返す(PreToolUse 限定)。 終了コードは0のまま、stdout に判定 JSON を出すと、より細かく制御できます。permissionDecision"allow"(許可)/"deny"(拒否)/"ask"(ユーザーに確認)/"defer"(通常の権限フローに任せる)を入れます。exit 2 が「とにかく止める」なら、JSON は「通す・止める・聞く」を選べるダイヤルです。

exit 2 と JSON、どっちを使う?」とよく聞かれます。ただ止めたいだけなら exit 2 が手軽。許可も含めて条件分岐したいなら JSON。迷ったら JSON 寄りにしておくと、後から ask を足すときに楽です。

実装1:PreToolUse で危険コマンドを止める(コピペで動く)

では本丸。Bash 実行前の事故防止です。Claude に大きめの変更を任せると、削除・移動・環境変数の表示・デプロイ系が紛れ込むことがあります。全部を疑う必要はありませんが、rm -rf /.env の中身表示、本番DBの破壊的操作あたりは実行前に止めたい。

.claude/hooks/block-dangerous-command.mjs を作ります。これは僕が実際に使っているものをそのまま貼れる形にしたものです。

import fs from "node:fs";

// stdin の JSON を読む。空なら {} 扱いにして落ちないようにする
const input = JSON.parse(fs.readFileSync(0, "utf8") || "{}");
const command = String(input.tool_input?.command || "");

// 「明らかに危ない」だけを並べる。完璧を狙わず、事故率を下げるのが目的
const denyPatterns = [
  /rm\s+-rf\s+(\/|\*|\.|\$HOME|~)/i, // ルートやホーム配下の一括削除
  /:\s*\(\)\s*\{.*\};\s*:/, // フォークボム
  /cat\s+\.env(\.|$|\s)/i, // .env の中身表示
  /printenv/i, // 環境変数の全表示
  /aws\s+.*\sdelete-/i, // AWS の削除系
  /gcloud\s+.*\sdelete/i, // GCP の削除系
  /kubectl\s+delete\s+(namespace|deployment|secret)/i,
  /DROP\s+(DATABASE|TABLE)/i, // SQL の破壊的操作
  /git\s+push\s+.*--force/i, // force push
];

const matched = denyPatterns.find((pattern) => pattern.test(command));

if (matched) {
  // JSON で deny を返す。理由は人が読んで分かる文にする
  console.log(
    JSON.stringify({
      hookSpecificOutput: {
        hookEventName: "PreToolUse",
        permissionDecision: "deny",
        permissionDecisionReason: `プロジェクトのフックがブロックしました: ${matched}`,
      },
    }),
  );
  process.exit(0);
}

// 当てはまらなければ何もしない。通常の権限フローに任せる
process.exit(0);

やっていることは単純です。stdin の JSON から tool_input.command を取り出し、危険パターンに当たったら permissionDecision: "deny" を返す。当たらなければ素通りさせて、普段どおり settings.json の権限ルールに任せます。

大事なのは、完璧なセキュリティ製品を作ろうとしないこと。正規表現だけで全ての危険操作を網羅するのは無理ですし、頑張るほど誤検知で日常の作業まで止まります。それでも「明らかにヤバい一手」を1段止められるだけで、初心者チームの事故率はガクッと下がります。厳密さが要るなら、後述のとおり権限設定の deny と二重にしてください。

実装2・3:依頼ログと、編集後の自動 format/test

残り2つも要点だけ置きます。まず UserPromptSubmit で依頼文を保存する .claude/hooks/log-prompt.mjs

import fs from "node:fs";
import path from "node:path";

const input = JSON.parse(fs.readFileSync(0, "utf8") || "{}");
const dir = path.join(process.cwd(), ".claude", "hook-logs");
fs.mkdirSync(dir, { recursive: true });

const record = {
  time: new Date().toISOString(),
  event: input.hook_event_name,
  cwd: input.cwd,
  prompt: input.prompt || "",
};

// JSON Lines で追記。外部送信はしない
fs.appendFileSync(path.join(dir, "prompts.jsonl"), JSON.stringify(record) + "\n", "utf8");

地味ですが効きます。Claude の作業がブレるとき、原因はコードより最初の依頼文にあることが多い。「ざっくり直して」だけだと成功条件が伝わらないんですね。ログを残すと「どの言い方で壊れて、どの言い方で通ったか」を後で見返せます。ただし依頼文には顧客名やURLが混ざるので、.claude/hook-logs/.gitignore に入れ、保存期間も決めておくこと。

次に PostToolUse.claude/hooks/run-quality-checks.mjs。編集の直後に整形とテストを走らせ、結果を Claude に返します。

import fs from "node:fs";
import { execFileSync } from "node:child_process";

const input = JSON.parse(fs.readFileSync(0, "utf8") || "{}");
const filePath = String(input.tool_input?.file_path || "");

// 対象拡張子だけ処理。それ以外は何もせず終了
const isSourceFile = /\.(js|jsx|ts|tsx|css|md|mdx|json)$/.test(filePath);
if (!isSourceFile) process.exit(0);

const run = (cmd, args) => {
  try {
    return execFileSync(cmd, args, { cwd: process.cwd(), encoding: "utf8" });
  } catch (error) {
    // 失敗しても落とさず、出力を文字列で拾う
    return String(error.stdout || "") + String(error.stderr || "");
  }
};

const messages = [];
messages.push(run("npx", ["prettier", "--write", filePath]));

if (/\.(js|jsx|ts|tsx)$/.test(filePath)) {
  messages.push(run("npm", ["test", "--", "--runInBand"]));
}

// 結果を additionalContext で Claude に渡す。長すぎないよう末尾4000文字に絞る
console.log(
  JSON.stringify({
    hookSpecificOutput: {
      hookEventName: "PostToolUse",
      additionalContext: messages.join("\n").slice(-4000),
    },
  }),
);

このサンプルは小さなプロジェクト向けです。大きなリポジトリで毎回 npm test を回すと重いので、変更ファイルに関連するテストだけに絞るか、CI に寄せる範囲を決めてください。肝は additionalContext に結果を返すこと。テストの失敗を Claude が読めると、次の応答で自分から直しにいきます。こちらがログを貼り直す回数が、目に見えて減りました。

落とし穴:フックは権限を「上書きできてしまう」

ここが、この記事でいちばん伝えたい注意点です。

公式リファレンスを読むと、PreToolUse のフックが返す JSON は、settings.json の静的な権限ルールより優先されます。つまり——

  • deny に設定した操作でも、フックが "allow" を返せば通ってしまう
  • ask(ユーザーに確認)の操作でも、フックが "allow" を返せば確認なしで実行される。

便利な反面、これはフック自体が新しい攻撃面になることを意味します。「フックを入れたから安全」ではなく、「フックの中身が安全なら安全」。ここを取り違えると、せっかくの deny ルールがフック経由で骨抜きになります。

そもそもコマンド型のフックは、あなたのユーザー権限でそのまま動きます。間違ったスクリプトはファイル削除も秘密情報の読み取りもできる。だから次の基本は外さないでください。

  1. 入力を信用しない。 tool_input の値はそのまま eval やシェルに渡さない。execFileSync で配列引数にして、シェル経由を避ける。
  2. 危ない場所に触れない。 パスに .. が含まれたら止める。.env.git は読み書きしない。
  3. 基本は denyask、安易に allow を返さない。 フックで自動許可するのは、検証済みの安全な操作だけにする。緩める順番は Claude Code 権限の“はしご”:厳しめから安全に allow を広げる5段 の考え方が役立ちます。
  4. 多層にする。 フックは権限設定やレビューの代わりではありません。危険操作は permissions.deny、人間の承認、CI の保護ルールと組み合わせて初めて効きます。権限側の設計は Claude Code 権限設定リファレンス に分けてまとめています。

もう1つよくあるのが、遅いフックを同期で回してしまう失敗。PostToolUse で毎回フルテスト+型チェック+ビルドまで走らせると、Claude の作業が固まります。ブロックが要る処理だけ同期に残し、重い処理は別途バックグラウンドや CI に逃がす。これで体感速度が戻ります。

そして、入口の依頼文そのものを安全にする話は別軸で大事です。曖昧な「全部やって」をフックで全部受け止めるのは無理があるので、依頼の書き方は Claude Code に「全部やって」は禁句:暴走を防ぐ安全な指示の書き方 に切り出しました。フック(出口のガード)と依頼文(入口の設計)は、両輪で考えるとうまくいきます。

僕がやらかした失敗3つ

正直に書きます。最初のフック運用は事故だらけでした。

ひとつ目は、Stop フックを厳しくしすぎたこと。「テストが緑じゃなきゃ終わらせない」を入れたら、Claude が延々と終われないループに突入しました。Stopexit 2 は「終わらせない」なので、条件が厳しいと無限に粘ります。今は「Git の競合が残っているときだけ止める、あとはログだけ」に緩めました。再入を避けるため、入力の stop_hook_active が真なら即終了する一行も入れています。

ふたつ目は、停止理由を stdout に書いていたこと。exit 2 のときは stdout は無視され、Claude が読むのは stderr です。理由を console.log で出していたら、Claude には「なぜ止まったか」が一切伝わらず、見当違いの修正を繰り返しました。console.error に直したら、急に自己解決しはじめました。

みっつ目は、正規表現を欲張ったこと。危険コマンドの判定を盛りすぎて、普通の rm node_modules まで止まるようになり、結局みんながフックを切ってしまった。安全装置は、切られたら意味がありません。「明らかにヤバい一手」だけに絞るのが、長く使われるコツでした。

よくある質問

Q. Hooks と権限設定(permissions)の違いは? 権限設定は「このツールのこの操作を許す/拒む/聞く」を静的なルールで決めるもの。Hooks は作業の節目に自分のコードを走らせる仕組みです。フックは権限を上書きできるほど強いので、置き換えではなく重ねて使います。設計の全体像は Claude Code 権限設定リファレンス を参照してください。

Q. exit code 2 と JSON の permissionDecision、どっちを使えばいい? ただ止めたいだけなら exit 2(停止理由は stderr へ)。許可・拒否・確認を条件で出し分けたいなら PreToolUse で JSON を返す方法。後から ask を足す予定があるなら、最初から JSON 寄りにしておくと楽です。

Q. フックで deny 設定を allow に上書きできるって本当? 本当です。PreToolUse のフックが返す permissionDecision: "allow"settings.jsondeny/ask より優先されます。だからフックの中身が雑だと、権限ルールが骨抜きになります。自動許可は検証済みの操作だけに限ってください。

Q. フックが遅くて作業が固まる。 同期実行のフックに重い処理を載せている可能性が高いです。ブロックが必要な処理だけ同期に残し、フルテストやビルドは CI へ。PostToolUse の毎回フルテストは特に重くなりがちです。

Q. どのイベントから始めるのが安全? UserPromptSubmit のログ保存と、PostToolUse の保存時 format の2つから。1週間運用してログを眺め、よく起きる事故だけを PreToolUse のブロックに移すと、速度を落とさず安心感だけ上げられます。

実際に試した結果

冒頭の「format してって言ったよね」地獄から抜けて半年、いちばん効いたのは意外にも危険コマンドのブロックではなく、PostToolUse で format/test の結果を Claude に返す部分でした。Claude が自分の失敗を読んで自分で直すので、僕がログを貼り直す往復が消えたんです。

一方で、PreToolUse のブロックは「保険」として静かに効いています。半年で permissionDecision: "deny" が発火したのは数回。でもそのうち1回は本番DBへの破壊的クエリで、あれが通っていたら今ここで呑気に記事を書けていません。

教訓はシンプルです。フックは賢くしすぎない。「明らかにヤバい一手だけ止める」「結果は必ず Claude に返す」——この2つに絞るほど、長く使えて事故が減りました。Hooks を含めた権限・CLAUDE.md・レビュー手順をチームでまとめて整えたい場合は 研修・相談 で、自分の手元でテンプレ化して進めたい人は 教材一覧 から始めてみてください。

仕様の細部は更新されるので、イベント名や permissionDecision の挙動は Claude Code Hooks 公式リファレンス で最新を確認するのをおすすめします。

#Claude Code #Hooks #PreToolUse #自動化 #settings.json
無料

無料PDF: Claude Code はじめてのチートシート

まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。

スパムは送りません。登録情報は厳重に管理します。

Claude Codeを仕事で使える形にしませんか?

まず無料PDFで基本を固め、繰り返し使う作業はGumroad教材へ、チーム導入や権限設計は導入相談へ進めます。

Masa

この記事を書いた人

Masa

Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。

PR

関連書籍・参考図書

この記事のテーマに関連する書籍を楽天ブックスで探せます。

※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。