Claude Code の Hooks(フック)は、ツールの実行前後やプロンプトの送信時など、決まったタイミングで自分のコマンドを必ず実行させる仕組みです。settings.json の hooks に「イベント名 → matcher → 実行するコマンド」を書き、コマンドが終了コード 2 で終わると、その操作(ツールの実行など)を止められます。

CLAUDE.md に書いた指示は「守ってほしいお願い」ですが、Hooks は毎回必ず動く処理です。危険なコマンドの停止や、編集後の自動フォーマットのように、毎回確実に行いたいことに向いています。いちばん小さな例は次のとおりです。

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-dangerous.sh" }
        ]
      }
    ]
  }
}
知りたいこと 答え 詳しくは
どこに書く? ~/.claude/settings.json(全プロジェクト)か .claude/settings.json(そのプロジェクト) 設定ファイルの場所
何を受け取る? 標準入力に JSON が1つ(tool_name・tool_input など) 入力 JSON
どう止める? exit 2 と stderr の理由。または exit 0 + JSON の deny 終了コード・JSON 出力
動いたか見るには? /hooks、claude --debug-file、Ctrl+O デバッグ方法
一時的に止めるには? "disableAllHooks": true、1回だけなら --settings '{"disableAllHooks": true}' 止め方・無効化

確認環境:Claude Code 2.1.301(npm の latest。stable タグは 2.1.292)・jq 1.7・Prettier 3.9.10・Node.js 22.22.2(Linux)。イベント・入力・出力の仕様は公式ドキュメント(Hooks reference と hooks ガイド)と、同じ版番号の Agent SDK(@anthropic-ai/claude-agent-sdk 0.3.301)の型定義で確かめました。この記事のフックのスクリプトは、ドキュメントの入力 JSON を自分で作って標準入力に流し、終了コードと出力を確かめました。さらに、決まった順にツール呼び出しを返す検証用の API サーバーに Claude Code 2.1.301 をつなぎ、claude -p で実際にフックを発火させて、フックの実行記録(--include-hook-events)と Claude 側に返った文言を確かめています。出力中のパスは /home/you/... に置き換えています(確認日 2026年10月16日)。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Claude Code Hooksの使い方|全33イベント表と設定例・止め方」

Hooksとは:CLAUDE.md・Skillsとの違い

仕組み 性質 向いていること
Hooks 決まったタイミングで必ず実行される 危険な操作の停止、フォーマッター、通知、ログ、文脈の自動追加
CLAUDE.md Claude が読んで従う方針 コーディング規約、テストの書き方、プロジェクトの説明
Skills 必要なときに読み込む手順書 決まった作業の手順、テンプレート
権限ルール(permissions) ツールの許可・拒否・確認 使ってよいコマンドの範囲

「毎回必ず」なら Hooks、「Claude に判断してほしい方針や手順」なら CLAUDE.md や Skills です。CLAUDE.md と Skills の使い分けは、関連記事「CLAUDE.mdとSkillsの違い|使い分け判断表と移し替え手順」で扱っています。権限ルールの書き方や設定の優先順位は、関連記事「Claude Code settings.json設定一覧|権限の雛形と優先順位」で扱っています。なお、Git の pre-commit などのフックは別の仕組みで、関連記事「Git hooks・husky v9の設定方法|実行結果とlefthook比較」で扱っています。

Hooksの全イベント一覧(2026年10月・33イベント)

公式ドキュメントのイベント表と、Agent SDK の型定義(HOOK_EVENTS)はどちらも 33 イベントでした。「止められるか」は、ドキュメントの「exit 2 のときの動き」の表に従っています。主な入力項目は、全イベント共通の項目(後述)を除いたものです。

セッション・プロンプトのイベント

イベント いつ動くか matcher の対象 主な入力項目 exit 2 で止められるか
SessionStart セッションの開始・再開 startup・resume・clear・compact・fork source・model(あれば) × stderr は利用者に表示のみ
Setup --init-only、-p での --init・--maintenance init・maintenance trigger × 無視
UserPromptSubmit プロンプト送信後、Claude が処理する前 なし(毎回) prompt ○ プロンプトを止める
UserPromptExpansion 入力したコマンドがプロンプトに展開される前 コマンド名 command_name・command_args・prompt ○ 展開を止める
InstructionsLoaded CLAUDE.md・.claude/rules/*.md の読み込み時 読み込み理由(session_start など) file_path・memory_type・load_reason × 無視
SessionEnd セッションの終了 clear・resume・logout・prompt_input_exit・other reason × 利用者に表示のみ

ツール実行のイベント

イベント いつ動くか matcher の対象 主な入力項目 exit 2 で止められるか
PreToolUse ツールを実行する直前 ツール名 tool_name・tool_input・tool_use_id ○ ツールの実行を止める
PermissionRequest 許可の判断が要るとき ツール名 tool_name・tool_input・permission_suggestions × JSON の decision で許可・拒否
PermissionDenied auto モードで拒否されたとき ツール名 tool_name・tool_input・reason × JSON の retry のみ
PostToolUse ツールが成功した直後 ツール名 tool_input・tool_response・duration_ms × 実行済み。stderr を Claude に渡す
PostToolUseFailure ツールが失敗した直後 ツール名 tool_input・error・is_interrupt × 失敗済み。stderr を Claude に渡す
PostToolBatch 並列のツール呼び出しがすべて終わり、次の推論の前 なし(毎回) tool_calls ○ 次の推論の前で止める

応答・エージェント・タスクのイベント

イベント いつ動くか matcher の対象 主な入力項目 exit 2 で止められるか
Notification 通知を出すとき(許可待ち・入力待ちなど) 通知の種類(permission_prompt・idle_prompt など) message・notification_type × 無視
MessageDisplay 応答の文が表示されている間 なし(毎回) delta・final × 元の文を表示
Stop Claude が応答を終えたとき なし(毎回) stop_hook_active・last_assistant_message ○ 終わらせずに続けさせる
StopFailure API エラーでターンが終わったとき エラーの種類(rate_limit など) error・error_details × 無視
SubagentStart サブエージェントの起動時 エージェント名 agent_id・agent_type × 利用者に表示のみ
SubagentStop サブエージェントの終了時 エージェント名 stop_hook_active・agent_transcript_path ○ 終わらせずに続けさせる
TaskCreated TaskCreate でタスクを作るとき なし(毎回) task_id・task_subject ○ 作成を取り消す
TaskCompleted タスクを完了にするとき なし(毎回) task_id・task_subject ○ 完了にさせない
TeammateIdle エージェントチームのメンバーが待機に入る前 なし(毎回) teammate_name・team_name ○ 作業を続けさせる

設定・ファイル・その他のイベント

イベント いつ動くか matcher の対象 主な入力項目 exit 2 で止められるか
ConfigChange セッション中に設定ファイルが変わったとき user_settings・project_settings など source・file_path ○ 変更を反映させない(policy_settings 以外)
CwdChanged 作業ディレクトリが変わったとき なし(毎回) old_cwd・new_cwd × 利用者に表示のみ
DirectoryAdded /add-dir などで作業ディレクトリを追加したとき slash_command・register_repo_root directory・source × デバッグログのみ
FileChanged 監視しているファイルが変わったとき ファイル名(.envrc|.env など) file_path・event × 利用者に表示のみ
WorktreeCreate worktree を作るとき なし(毎回) name ○ 0 以外の終了で作成失敗
WorktreeRemove フックで作った worktree を消すとき なし(毎回) worktree_path ○ 0 以外の終了で削除失敗
PreCompact コンテキストの圧縮(compact)の前 manual・auto trigger・custom_instructions ○ 圧縮を止める
PostCompact 圧縮が終わった後 manual・auto trigger・compact_summary × 利用者に表示のみ
PreModelSwitch モデルを切り替える前 切り替え先のモデル名 from_model・to_model ○ 切り替えを止める
PostModelSwitch モデルが切り替わった後 切り替え先のモデル名 from_model・to_model × 利用者に表示のみ
Elicitation MCP サーバーが入力を求めたとき MCP サーバー名 mcp_server_name・message・requested_schema ○ 要求を断る
ElicitationResult 入力への応答をサーバーに返す前 MCP サーバー名 mcp_server_name・action・content ○ 応答を断る

覚えておきたいのは「止めたいなら Pre 系、後処理なら Post 系」です。 PostToolUse はツールがすでに実行された後なので、取り消せません。

設定の書き方:イベント・matcher・ハンドラーの3段

{
  "hooks": {
    "イベント名": [
      {
        "matcher": "対象の絞り込み(ツール名など)",
        "hooks": [
          { "type": "command", "command": "実行するコマンド", "timeout": 60 }
        ]
      }
    ]
  }
}

同じイベントに当たったハンドラーは、すべて並列に実行されます(ドキュメント)。PreToolUse の判断が分かれたときは、より厳しいもの(deny → defer → ask → allow の順)が優先されます。

設定ファイルの場所

置き場所 効く範囲 共有
~/.claude/settings.json 自分のすべてのプロジェクト しない
.claude/settings.json そのプロジェクト する(リポジトリにコミット)
.claude/settings.local.json そのプロジェクト しない(gitignore される)
管理ポリシーの設定(managed settings) 組織全体 管理者が配布
プラグインの hooks/hooks.json プラグインが有効なとき プラグインと一緒に配る
スキルのフロントマター スキルを呼んだ後、セッションの終わりまで スキルのファイルと一緒
サブエージェントのフロントマター そのサブエージェントの実行中だけ エージェントのファイルと一緒

ドキュメントでは、フックは上書きではなく、すべての置き場所のものが合わさって(マージされて)動きます。 設定ファイルを直接書き換えると、通常はファイルの監視で自動的に読み直されます。

ハンドラーの種類(type)

type 動き タイムアウトの既定値
command シェルのコマンドを実行。標準入力に JSON 600 秒
http JSON を本文にして URL へ POST 600 秒
mcp_tool 設定済みの MCP サーバーのツールを呼ぶ 600 秒
prompt モデルに1回だけ判断させる 30 秒
agent ツールを使える検証役を動かす(実験的) 60 秒

UserPromptSubmit・PreModelSwitch・PostModelSwitch では command・http・mcp_tool の既定が 30 秒、MessageDisplay では 10 秒に下がります。SessionEnd のフックは全体で 1.5 秒の持ち時間を共有します(ドキュメント)。

ハンドラーに書ける項目

項目 対象 意味
type すべて 上の5種類のどれか(必須)
if すべて(ツールのイベントのみ評価) 権限ルールの書式で引数を絞る。例 "Bash(git *)"
timeout すべて 打ち切るまでの秒数
statusMessage すべて 実行中に表示する文言
once すべて(スキルのフロントマターのみ有効) true で初回の成功後に外す
command / args command args を付けるとシェルを通さず直接起動
async / asyncRewake command バックグラウンドで実行(asyncRewake は exit 2 で Claude を起こす)
shell command bash か powershell
onFailure command・http "block" でフックの失敗時に操作を止める(v2.1.295 以降)
url / headers / allowedEnvVars http 送り先、ヘッダー、ヘッダーに展開してよい環境変数
server / tool / input mcp_tool 呼ぶ MCP サーバーとツール、引数
prompt / model prompt・agent 判断させる文($ARGUMENTS に入力 JSON)とモデル

matcher の書き方

matcher は大文字・小文字を区別します。書き方で「完全一致」か「正規表現」かが自動で切り替わります(ドキュメント)。

書き方 解釈 例
"*"、""、省略 すべてに一致 すべてのツール
英数字・_・-・空白・,・| だけ 完全一致(| か , で複数) Bash、Edit|Write、Edit, Write
それ以外の記号を含む JavaScript の正規表現(部分一致) ^Notebook、mcp__memory__.*
MCP のツール mcp__<サーバー名>__<ツール名> mcp__memory__.*(mcp__memory だけでは一致しない)

正規表現は部分一致なので、Edit.* は NotebookEdit にも当たります。全体で一致させるなら ^Edit$ と書きます。matcher に対応しないイベント(UserPromptSubmit・Stop など)に書いた matcher は無視されます。

さらに引数で絞るなら if を使います。matcher でツールを選び、if で引数を絞ると整理できます。ドキュメントの Bash の判定例は次のとおりです。

if Bash のコマンド フックは動くか
Bash(git *) FOO=bar git push 動く(先頭の変数代入は外して判定)
Bash(git *) npm test && git push 動く(&& の後ろも判定)
Bash(rm *) echo $(rm -rf /) 動く($() の中も判定)
Bash(rm *) echo $(date) 動かない

実際に、後述の設定(deny-git-push.sh に "if": "Bash(git *)")で動かすと、rm -rf ./tmpdir のときは Bash 用のフックが2つ、git push origin main のときは3つ起動しました。if は最善努力の絞り込みなので、確実に禁止したいものは権限ルールと組み合わせます。

入力:標準入力に届くJSON

command のフックには、標準入力(stdin)に JSON が1つ届きます(http では POST の本文)。全イベントに共通の項目は次のとおりです。

項目 意味
session_id セッションの ID
transcript_path 会話の記録ファイル(書き込みが遅れることがある)
cwd フック実行時の作業ディレクトリ
hook_event_name 発火したイベント名
permission_mode default・plan・acceptEdits・auto・dontAsk・bypassPermissions(届かないイベントもある)
prompt_id 処理中のプロンプトの ID(最初の入力の後から)
effort {"level": ...}。ツール実行まわりのイベントで、モデルが対応しているとき
agent_id / agent_type サブエージェントの中で動いたとき
scratchpad_dir セッションの作業用ディレクトリ(v2.1.257 以降、ある場合)

claude -p で実際に届いた JSON を、フックの中でそのまま保存したものです(ID は伏せています)。まず PreToolUse(Bash)です。

{
  "session_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "transcript_path": "/home/you/.claude/projects/-home-you-hooks-lab/xxxxxxxx.jsonl",
  "cwd": "/home/you/hooks-lab",
  "prompt_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "permission_mode": "auto",
  "effort": { "level": "medium" },
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "tool_input": { "command": "ls src", "description": "t6" },
  "tool_use_id": "toolu_msg_6"
}

PostToolUse には tool_response(実行結果)と duration_ms が加わります。Bash では標準出力・標準エラー出力が入っていました(共通項目は省略)。

{
  "hook_event_name": "PostToolUse",
  "tool_name": "Bash",
  "tool_input": { "command": "ls -1 src && echo rewritten-by-hook", "description": "rewritten" },
  "tool_response": { "stdout": "a.txt\nrewritten-by-hook", "stderr": "", "interrupted": false, "isImage": false, "noOutputExpected": false },
  "tool_use_id": "toolu_msg_6",
  "duration_ms": 12
}
イベント 実際に届いた固有の項目
SessionStart "source": "startup"
UserPromptSubmit "prompt": "テスト"
Stop "stop_hook_active": false、"last_assistant_message": "作業が完了しました。"、"background_tasks": []、"session_crons": []
SessionEnd "reason": "other"(-p の終了時)

Bash なら .tool_input.command、Edit・Write・Read なら .tool_input.file_path を取り出すのが基本です。

終了コードの意味(exit 0・exit 2・それ以外)

終了のしかた 何が起きるか 向いている用途
exit 0(出力なし) そのまま続行。PreToolUse でも「許可」ではなく、通常の許可判定に進む 何もしない・ログだけ
exit 0 + テキスト UserPromptSubmit・UserPromptExpansion・SessionStart・PostModelSwitch では Claude の文脈に追加。ほかはデバッグログのみ 文脈の追加
exit 0 + JSON JSON の内容で制御(deny・ask・additionalContext など) 理由つきの拒否、確認、書き換え
exit 2 止められるイベントでは操作を止め、stderr を理由として渡す 単純な禁止
それ以外(1・127 など) 止まらない。<フック名> hook error の通知が出るだけ 想定外の失敗

exit 1 では止まりません。 ドキュメントも「方針を強制するフックは exit 2 を使う」と注意しています。実際に PreToolUse のフックが stderr に something broke を出して exit 1 で終わると、フックの記録は "exit_code": 1, "outcome": "error" でしたが、Bash は実行されました。デバッグログには次の行が残りました。

[ERROR] "Hook PreToolUse:Bash (PreToolUse) error: status code 1\nstderr:\nsomething broke"

標準出力と終了コードの組み合わせで、結果は次のように決まります(ドキュメント)。

標準出力 exit 0 exit 2 それ以外
正しい JSON 成功。JSON の項目が効く 止まる。JSON で止めるのを覆せない 成功扱い。JSON だけで決まる
壊れた JSON・形式違反 止まらないエラー 止まる(stderr が理由) 止まらないエラー
テキスト、または出力なし 成功 止まる(stderr が理由) 止まらないエラー(stderr の1行目を表示)

JSON出力の早見表(decision・additionalContext など)

exit 0 で、標準出力に JSON を1つだけ出すと、細かい制御ができます。全イベント共通の項目は次のとおりです。

項目 既定 意味
continue true false で Claude の処理をそこで終える。ほかの判断より優先
stopReason なし continue: false のときに利用者に見せる文
systemMessage なし 利用者に見せる警告
suppressOutput false 受け付けるが効果なし(成功時の stdout はもともと会話に出ない)
terminalSequence なし 通知やタイトル変更の端末シーケンス(OSC 0・1・2・9・99・777 と BEL のみ)

イベントごとの「判断」の書き方は3通りに分かれます。

イベント 書き方 主な項目
UserPromptSubmit・UserPromptExpansion・PostToolUse・PostToolUseFailure・PostToolBatch・Stop・SubagentStop・ConfigChange・PreCompact トップレベルの decision "decision": "block"、"reason"
PreToolUse hookSpecificOutput permissionDecision(allow・deny・ask・defer)、permissionDecisionReason、updatedInput、additionalContext
PermissionRequest hookSpecificOutput decision.behavior(allow・deny)、updatedInput、updatedPermissions
PreModelSwitch hookSpecificOutput かトップレベルの decision permissionDecision(allow・deny・ask)
PermissionDenied hookSpecificOutput retry: true(再試行してよいとモデルに伝える)
TaskCreated 終了コードかトップレベルの decision "decision": "block" で作成を取り消す
TeammateIdle・TaskCompleted 終了コードか continue: false exit 2 で続行させる
Elicitation・ElicitationResult hookSpecificOutput かトップレベルの decision action(accept・decline・cancel)、content
MessageDisplay hookSpecificOutput displayContent(画面の表示だけ置き換え)
WorktreeCreate パスを返す stdout に作ったパス(http は worktreePath)
SessionStart・SubagentStart・PostModelSwitch 文脈の追加だけ additionalContext(SessionStart は initialUserMessage・sessionTitle なども)
Setup・Notification・SessionEnd・PostCompact・InstructionsLoaded・StopFailure・CwdChanged・DirectoryAdded・FileChanged 判断なし ログや後片付けに使う

permissionDecision や additionalContext は hookSpecificOutput の中に書きます。 トップレベルに書くと、エラーにならずに無視されます。実際に {"permissionDecision":"deny",...} をトップレベルで返すとコマンドは実行され、claude --debug-file のログに次の行が残りました。

[DEBUG] Hook JSON output had unrecognized keys (ignored): permissionDecision, permissionDecisionReason.

additionalContext・systemMessage・テキストの stdout は、それぞれ 10,000 文字までです。超えた分はファイルに保存され、先頭 2,000 文字とファイルのパスに置き換わります(ドキュメント)。hookSpecificOutput を使うときは hookEventName にイベント名を入れます。

PreToolUse の allow を返しても、settings の deny ルールは越えられません。ドキュメントでは、フックは制限を強めることはできても、権限ルールより緩めることはできないと説明されています。逆に、PreToolUse の deny は bypassPermissions モードでも効きます。

すぐ使える設定例(settings.json 全文)

次の settings.json と .claude/hooks/ のスクリプトで、危険なコマンドの停止、git push の拒否、秘密ファイルの保護、編集後のフォーマット、コマンドの記録、日付の追加、通知、テストが通るまで終わらせない、圧縮後の再注入、をまとめて設定できます。プロジェクト直下の .claude/settings.json に置きます。

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/log-bash.sh" },
          { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-dangerous.sh" },
          { "type": "command", "if": "Bash(git *)", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/deny-git-push.sh" }
        ]
      },
      {
        "matcher": "Read|Edit|Write",
        "hooks": [
          { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh" }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/format.sh", "statusMessage": "Prettier で整形中" }
        ]
      }
    ],
    "UserPromptSubmit": [
      {
        "hooks": [
          { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/add-date.sh" }
        ]
      }
    ],
    "Notification": [
      {
        "matcher": "permission_prompt|idle_prompt",
        "hooks": [
          { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/notify.sh" }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/stop-check.sh", "timeout": 120 }
        ]
      }
    ],
    "SessionStart": [
      {
        "matcher": "compact",
        "hooks": [
          { "type": "command", "command": "echo '再確認:パッケージ管理は npm。コミット前に npm test を実行する。'" }
        ]
      }
    ]
  }
}

スクリプトには実行権限を付けます(chmod +x .claude/hooks/*.sh)。$CLAUDE_PROJECT_DIR はプロジェクトのルートで、作業ディレクトリが変わっても同じ場所を指します。パスに空白が入ることがあるため、ダブルクォートで囲んでいます。

例1:危険なコマンドを exit 2 で止める(PreToolUse)

#!/bin/bash
# .claude/hooks/block-dangerous.sh
# PreToolUse(Bash):危険なコマンドを exit 2 で止める
INPUT=$(cat)
CMD=$(jq -r '.tool_input.command // empty' <<<"$INPUT")
if grep -Eq '(^|[;&|[:space:]])rm[[:space:]]+-[a-zA-Z]*(rf|fr)[a-zA-Z]*([[:space:]]|$)|git[[:space:]]+push[[:space:]].*(--force|-f)([[:space:]]|$)' <<<"$CMD"; then
  echo "Blocked by hook: '$CMD' は禁止しています。削除や強制 push は人が実行します。" >&2
  exit 2
fi
exit 0

ドキュメントの入力 JSON(tool_name: "Bash"・tool_input.command)を作って標準入力に流した結果です。

tool_input.command 終了コード stderr
rm -rf ./tmpdir 2 Blocked by hook: 'rm -rf ./tmpdir' は禁止しています。…
rm -fr build 2 同上
npm test && rm -rf / 2 同上
git push --force origin main 2 同上
ls -la・git status 0 なし
$ jq -nc '{hook_event_name:"PreToolUse",tool_name:"Bash",tool_input:{command:"rm -rf ./tmpdir"}}' \
    | .claude/hooks/block-dangerous.sh; echo "exit=$?"
Blocked by hook: 'rm -rf ./tmpdir' は禁止しています。削除や強制 push は人が実行します。
exit=2

claude -p で Claude が rm -rf ./tmpdir を実行しようとしたとき、Claude に返ったツールの結果は次のとおりで、tmpdir は消えずに残りました。--allowedTools で Bash を許可していても止まります。

PreToolUse:Bash hook error: ["$CLAUDE_PROJECT_DIR"/.claude/hooks/block-dangerous.sh]: Blocked by hook: 'rm -rf ./tmpdir' は禁止しています。削除や強制 push は人が実行します。

正規表現での禁止は、書き方を変えるとすり抜けます。 同じスクリプトに次のコマンドを流すと、どれも exit 0 でした。

すり抜けたコマンド 理由
rm -r -f ./tmpdir オプションが分かれている
rm --recursive --force ./tmpdir 長い形のオプション
/bin/rm -rf ./tmpdir 前が / で、パターンの区切りに当たらない
bash -c "rm -rf ./tmpdir" 前が " で、パターンの区切りに当たらない
find ./tmpdir -delete 別のコマンド

フックは多層防御の1枚と考え、権限ルールの deny やサンドボックスと組み合わせます。

例2:git push を JSON で拒否する(理由つき)

#!/bin/bash
# .claude/hooks/deny-git-push.sh
# PreToolUse(Bash):git push を JSON で deny(理由つき)
INPUT=$(cat)
CMD=$(jq -r '.tool_input.command // empty' <<<"$INPUT")
if grep -Eq '(^|[;&|[:space:]])git[[:space:]]+push([[:space:]]|$)' <<<"$CMD"; then
  jq -n --arg cmd "$CMD" '{
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "deny",
      permissionDecisionReason: ("git push はレビュー後に人が実行します(hook で拒否): " + $cmd)
    }
  }'
fi
exit 0
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "git push はレビュー後に人が実行します(hook で拒否): git push origin main"
  }
}

claude -p では、Claude に次の結果が返りました。JSON の permissionDecisionReason がそのまま理由になっています。

PreToolUse:Bash hook error: git push はレビュー後に人が実行します(hook で拒否): git push origin main

permissionDecision の値は4つです。allow は許可の確認を省く(ただし deny ルールは越えない)、deny は拒否して理由を Claude に渡す、ask は利用者に確認する、defer は -p の実行でツール呼び出しを保留して後で再開できるようにする、です。JSON は文字列の連結ではなく jq -n --arg で作ると、引用符や改行が正しくエスケープされます。

claude -p の最終結果("type": "result")の permission_denials には、exit 2 で止めた rm -rf、JSON で止めた git push、例3で止めた .env の読み取りの3件が並んで記録されていました。CI で「何を止めたか」を集計するときに使えます。

例3:.env や鍵ファイルを守る

#!/bin/bash
# .claude/hooks/protect-files.sh
# PreToolUse(Read|Edit|Write):.env や鍵ファイルへのアクセスを止める
INPUT=$(cat)
FILE=$(jq -r '.tool_input.file_path // empty' <<<"$INPUT")
case "$FILE" in
  *.env|*.env.*|*/.git/*|*.pem|*id_rsa*)
    echo "Blocked by hook: $FILE は保護対象のファイルです" >&2
    exit 2 ;;
esac
exit 0
tool_input.file_path 終了コード
/p/.env・/p/.env.local・/p/.git/config・/p/key.pem 2
/p/src/app.js 0

matcher に Read も入れているのは、書き込みだけでなく読み取りも止めるためです。claude -p で Claude が .env を Read しようとすると、PreToolUse:Read hook error: [...protect-files.sh]: Blocked by hook: /home/you/hooks-lab/.env は保護対象のファイルです が返りました。Bash の cat .env は別の経路なので、権限ルールの Read(./.env) の deny などと組み合わせます。

例4:編集後に Prettier で整形する(PostToolUse)

#!/bin/bash
# .claude/hooks/format.sh
# PostToolUse(Edit|Write):編集したファイルを Prettier で整形
INPUT=$(cat)
FILE=$(jq -r '.tool_input.file_path // empty' <<<"$INPUT")
case "$FILE" in
  *.js|*.jsx|*.ts|*.tsx|*.json|*.css|*.md)
    cd "$CLAUDE_PROJECT_DIR" && npx --no-install prettier --write "$FILE" >&2 ;;
esac
exit 0
src/app2.js の中身
Write ツールが書いた内容 const a={b:1,c:'x'};console.log( a )
PostToolUse の実行後 const a = { b: 1, c: "x" }; と console.log(a); の2行

Prettier の出力(src/app2.js 44ms)は stderr に出したので、フックの記録に残るだけで会話には入りません。一方、フックがファイルを書き換えたことは、Claude Code が Claude に伝えていました。 次の要求に、この文が入っていました。

PostToolUse:Write hook additional context: PostToolUse hook modified /home/you/hooks-lab/src/app2.js after your edit (likely a formatter). Your next Edit will not fail with a stale-file error, but if its old_string targets a region the hook reformatted, Read the file first.

ドキュメントの例どおり1行で書くなら "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write" です。Bash のコマンドがファイルを書き換えた場合も整形したいときは、FileChanged イベントを使う方法がドキュメントで案内されています。

例5:許可待ち・入力待ちを通知する(Notification)

#!/bin/bash
# .claude/hooks/notify.sh
# Notification:デスクトップ通知(OS ごとのコマンド)と端末への通知シーケンス
INPUT=$(cat)
MSG=$(jq -r '.message // "Claude Code が入力を待っています"' <<<"$INPUT")
if command -v osascript >/dev/null; then
  osascript -e "display notification \"$MSG\" with title \"Claude Code\"" >/dev/null 2>&1
elif command -v notify-send >/dev/null; then
  notify-send "Claude Code" "$MSG" >/dev/null 2>&1
fi
SEQ=$(printf '\033]777;notify;%s;%s\007' "Claude Code" "$MSG")
jq -nc --arg seq "$SEQ" '{terminalSequence: $seq}'

{"hook_event_name":"Notification","message":"Claude needs your permission to use Bash","notification_type":"permission_prompt"} を流すと、{"terminalSequence":"\u001b]777;notify;Claude Code;Claude needs your permission to use Bash\u0007"} が出力されました。ドキュメントでは、フックは端末を持たずに動くため /dev/tty に直接書けず、通知の端末シーケンスは terminalSequence で Claude Code に出してもらいます(対話モードのときだけ)。

matcher 通知されるとき(ドキュメント)
permission_prompt 許可が必要で、約6秒待っても応答がない
idle_prompt 応答の終了から約60秒、入力がない
auth_success 認証が完了した
elicitation_dialog MCP サーバーが入力フォームを出した
""(空) すべての通知

macOS の osascript は、スクリプトエディタに通知の許可がないと何も出ません。Linux の notify-send は通知デーモンがない環境(SSH・コンテナ)では使えません(ドキュメント)。

例6:実行されたコマンドを記録する

#!/bin/bash
# .claude/hooks/log-bash.sh
# PreToolUse(Bash):実行されようとしたコマンドを JSON Lines で記録
jq -c '{time: (now | todate), session: .session_id, cwd: .cwd, command: .tool_input.command}' \
  >> "$CLAUDE_PROJECT_DIR/.claude/bash-commands.log"
exit 0
{"time":"2026-10-16T07:33:08Z","session":"xxxxxxxx-...","cwd":"/home/you/hooks-lab","command":"rm -rf ./tmpdir"}
{"time":"2026-10-16T07:33:08Z","session":"xxxxxxxx-...","cwd":"/home/you/hooks-lab","command":"git push origin main"}
{"time":"2026-10-16T07:33:09Z","session":"xxxxxxxx-...","cwd":"/home/you/hooks-lab","command":"ls src"}

例1・例2で止めた rm -rf と git push も記録されています。同じイベントのフックは並列にすべて走るため、ほかのフックが拒否しても記録のフックは動きます。 ログのファイルは .gitignore に入れておきます。

例7:プロンプトに日付とブランチを足す(UserPromptSubmit)

#!/bin/bash
# .claude/hooks/add-date.sh
# UserPromptSubmit:プロンプトに今日の日付とブランチを足す
BRANCH=$(git -C "$CLAUDE_PROJECT_DIR" branch --show-current 2>/dev/null)
jq -n --arg d "$(date '+%Y-%m-%d (%a) %H:%M %Z')" --arg b "${BRANCH:-unknown}" '{
  hookSpecificOutput: {
    hookEventName: "UserPromptSubmit",
    additionalContext: ("現在の日時は " + $d + "。作業中のブランチは " + $b + "。")
  }
}'

claude -p では、Claude への要求に次の1行が入っていました。

UserPromptSubmit hook additional context: 現在の日時は 2026-10-16 (Fri) 16:33 JST。作業中のブランチは master。

UserPromptSubmit は、JSON を使わずに echo したテキストも文脈に入ります。ドキュメントは、additionalContext は命令ではなく事実として書くよう勧めています(命令調だとプロンプトインジェクションの防御に引っかかることがあるため)。--resume で再開すると、過去のターンの値は再実行されずに保存済みのものが使われます。

例8:テストが通るまで終わらせない(Stop)

#!/bin/bash
# .claude/hooks/stop-check.sh
# Stop:テストが通るまで終わらせない(無限ループ防止つき)
INPUT=$(cat)
if [ "$(jq -r '.stop_hook_active' <<<"$INPUT")" = "true" ]; then
  exit 0   # すでに一度続行させている
fi
cd "$CLAUDE_PROJECT_DIR" || exit 0
if ! OUT=$(npm test --silent 2>&1); then
  jq -n --arg out "$(tail -n 20 <<<"$OUT")" '{decision: "block", reason: ("npm test が失敗しています。失敗を直してから応答を終えること。\n" + $out)}'
fi
exit 0
入力 結果
stop_hook_active: false、テストが失敗 {"decision": "block", "reason": "npm test が失敗しています。…FAIL: ok ファイルがありません"}
stop_hook_active: true 何も出さず exit 0
stop_hook_active: false、テストが成功 何も出さず exit 0

claude -p では、1回目の Stop で会話が続き、Claude には Stop hook feedback: に続けて理由の文が渡されました。2回目の Stop は stop_hook_active: true だったため、そのまま終わりました。stop_hook_active を見ないと、Stop フックが止め続けます。 ドキュメントでは、ツール呼び出しを挟まずに8回続けて止めると Claude Code が上書きして終わらせ、上限は CLAUDE_CODE_STOP_HOOK_BLOCK_CAP で変えられます。

例9:圧縮(compact)の後に大事な文脈を入れ直す

settings.json の SessionStart の "matcher": "compact" がこの例です。SessionStart はテキストの stdout が文脈に入るため、echo だけで足せます。git log --oneline -5 のような動的な出力にも置き換えられます(ドキュメントの例)。毎回のセッション開始で入れたい固定の内容は、CLAUDE.md に書くほうが簡単です。

例10:コマンドを書き換える(updatedInput)

PreToolUse の hookSpecificOutput.updatedInput で、ツールの引数を実行前に置き換えられます。ls src を書き換えるフックで試しました。

jq -n '{hookSpecificOutput: {hookEventName: "PreToolUse", permissionDecision: "allow",
  updatedInput: {command: "ls -1 src && echo rewritten-by-hook", description: "rewritten"}}}'

Claude に返った結果は a.txt と rewritten-by-hook で、書き換えたコマンドが実行されていました。デバッグログには modified tool input keys: [command, description] と記録されました。ドキュメントでは、複数のフックが同じツールの updatedInput を返すと、最後に終わったものが使われ、順番は決まりません。

デバッグ方法:動いたか・何を返したかを確かめる

方法 わかること
サンプル JSON をパイプで流す スクリプト単体の出力と終了コード(いちばん早い)
/hooks(対話モード) 設定されたフックの一覧と、どのファイル由来か(読み取り専用)
Ctrl+O(記録の表示) ブロックの理由、hook error の通知
claude --debug-file /tmp/claude.log 一致したフック、終了コード、stdout・stderr、無視された項目
/debug(対話中) 途中からデバッグログを有効にし、ログの場所を表示
claude -p ... --output-format stream-json --verbose --include-hook-events フックの開始・終了を JSON で出す(CI 向け)

まずはスクリプト単体で確かめます。

echo '{"tool_name":"Bash","tool_input":{"command":"ls"}}' | ./.claude/hooks/block-dangerous.sh
echo $?   # 0 なら止めない、2 なら止める

--include-hook-events では、フックごとに次のような記録が出ました(ID は省略)。

{"type":"system","subtype":"hook_response","hook_name":"PreToolUse:Bash","hook_event":"PreToolUse","stdout":"","stderr":"Blocked by hook: 'rm -rf ./tmpdir' は禁止しています。削除や強制 push は人が実行します。\n","exit_code":2,"outcome":"error"}

デバッグログ(--debug-file)で役に立った行です。

ログの行 意味
Hooks: PreToolUse:Bash [...] finished with status 0 (21ms) フックが終了コード 0 で終わった
Hook PreToolUse (...) returned permissionDecision: allow JSON の判断を読み取った
Hook JSON output had unrecognized keys (ignored): ... JSON の項目の位置が違う
Hook output does not start with {, treating as plain text JSON として読まれていない
Hooks: PreToolUse:Bash [...] timed out after 2000ms タイムアウトで打ち切られた

よくあるつまずきと直し方

症状 原因 直し方
フックがまったく動かない イベント名・matcher の大文字小文字、設定ファイルの場所 /hooks に出るか確かめる。JSON に末尾カンマやコメントがないか
exit 1 で終わったのに止まらない exit 1 は止まらないエラー 止めるなら exit 2
/bin/sh: 1: .../no-such-hook.sh: not found(127) スクリプトのパスの誤り。止まらずにツールは実行される 絶対パスか "$CLAUDE_PROJECT_DIR"/...。止めたいなら "onFailure": "block"
スクリプトが実行されない 実行権限がない chmod +x
jq: command not found jq が入っていない jq を入れるか、Python・Node.js で JSON を読む
JSON の判断が効かない stdout の先頭に別の出力がある シェルのプロファイルの echo を対話シェルのときだけにする
JSON の項目が無視される hookSpecificOutput の外に書いた 入れ子の位置を直す。デバッグログの unrecognized keys を見る
タイムアウトしたのに止まらない タイムアウトした PreToolUse はブロックしない 処理を軽くするか "onFailure": "block"
Stop フックが止め続ける stop_hook_active を見ていない true のときは exit 0
正規表現をすり抜ける 書き方を変えれば回避できる 権限ルールの deny・サンドボックスと組み合わせる

いくつかは claude -p で実際に起こしました。

  • パスの誤り:存在しないスクリプトを登録すると、記録は "exit_code": 127, "outcome": "error" で、Bash はそのまま実行されました
  • onFailure: "block":同じく存在しないスクリプトに "onFailure": "block" を付けると、ツールは止まり、Claude に PreToolUse:Bash hook error: [node ${CLAUDE_PROJECT_DIR}/.claude/hooks/check-command.js]: failed; blocking because onFailure is "block" と Error: Cannot find module ... が返りました
  • プロファイルの出力:JSON の前に Shell ready on arm64 を出すと、deny の JSON は読まれず、コマンドは実行されました
  • タイムアウト:"timeout": 2 のフックで 5 秒待つと、記録は "outcome": "cancelled" で、exit 2 を返す前に打ち切られ、Bash は実行されました

シェルのプロファイルの echo は、ドキュメントの方法で対話シェルのときだけ出すようにします。

# ~/.zshrc や ~/.bashrc
if [[ $- == *i* ]]; then
  echo "Shell ready"
fi

止め方・無効化

やりたいこと 方法
1つのフックをやめる 設定ファイルからその項目を消す(1つだけ無効にする設定はない)
すべてのフックを一時的に止める 設定に "disableAllHooks": true
1回の実行だけ止める claude --settings '{"disableAllHooks": true}'
フックもプラグインも読まずに最小構成で動かす claude --bare(ヘルプでは「settings とプラグインのフックを読まない」)
読み込む設定の範囲を決める --setting-sources project など(user・project・local から選ぶ)

--settings '{"disableAllHooks": true}' と --bare の両方で claude -p を動かすと、プロジェクトの .claude/settings.json のフックは1つも起動しませんでした。ドキュメントでは、disableAllHooks は設定の優先順位を適用した後の値で決まるため、ユーザー設定の true はプロジェクトの false で上書きされます。--settings で渡したものはプロジェクトやローカルの設定より優先されます。

管理ポリシー(managed settings)で配られたフックは、ユーザー・プロジェクト・ローカルの disableAllHooks では止まりません。 管理側の設定でだけ止められます。管理側で allowManagedHooksOnly が設定されていると、ユーザー・プロジェクト・ローカル・プラグインのフックは動かず、/hooks には Only hooks from managed settings run here と表示されます(ドキュメント)。

セキュリティ上の注意

フックは、あなたのユーザー権限で、確認なしにコマンドを実行します。

  • 他人のリポジトリの .claude/settings.json を確かめてから使う:今回、まだ信頼(trust)の確認をしていないフォルダーで claude -p を実行しても、プロジェクトの .claude/settings.json のフックは動きました。CI などで読み込む範囲を絞るなら、--bare や --setting-sources を使います
  • 変数はダブルクォートで囲む:"$FILE" のようにしないと、空白や記号を含むパスで想定外の動きになります。パスの置き換えを使うなら、args を付けたシェルを通さない形(exec form)がドキュメントで勧められています
  • 入力を eval しない:標準入力の JSON は Claude が組み立てた値です。判定に使い、コマンドとして実行しないようにします
  • 正規表現での禁止はすり抜けを前提にする:例1の表のとおりです。権限ルール(deny)やサンドボックスと組み合わせます
  • .env や鍵は読み取りも守る:Edit・Write だけでなく Read、Bash の cat にも目を配ります
  • HTTP フックのヘッダーに入れる環境変数は allowedEnvVars に書いたものだけ展開されます。秘密情報は設定ファイルに直接書きません
  • additionalContext に外部の内容をそのまま入れない:Claude の文脈に入るため、外部の文に仕込まれた指示が混ざる危険があります

よくある質問

Q. claude hooks(Claude Code の Hooks)とは何ですか? A. Claude Code の決まったタイミング(ツールの実行前後、プロンプトの送信、応答の終了など33のイベント)で、自分のコマンド・HTTP・MCP ツールなどを必ず実行させる仕組みです。settings.json の hooks に書きます。

Q. hooks は Claude Code のどこに設定しますか? A. 全プロジェクト共通なら ~/.claude/settings.json、チームで共有するなら .claude/settings.json、自分だけの実験なら .claude/settings.local.json です。すべて合わさって動きます。設定後は /hooks で確かめます。

Q. Claude Code の hook が動きません。 A. /hooks に出るか、イベント名と matcher の大文字小文字、スクリプトの実行権限(chmod +x)、パス("$CLAUDE_PROJECT_DIR"/...)を確かめます。サンプル JSON をパイプで流して単体で動くかを見て、それでも分からなければ claude --debug-file のログを見ます。

Q. PreToolUse で exit 0 を返せば、そのツールは許可されますか? A. いいえ。exit 0 は「フックとしては反対しない」という意味で、通常の許可判定に進みます。確認を省きたいときは JSON で permissionDecision: "allow" を返します。ただし settings の deny ルールは越えられません。

Q. PostToolUse でテストが失敗したら、編集を取り消せますか? A. 取り消せません。ツールは実行済みです。トップレベルの "decision": "block" と reason を返すか exit 2 で stderr を渡して Claude に直させるか、事前に止めたいなら PreToolUse で判断します。

Q. Hooks は有料プランでないと使えませんか? A. Hooks は Claude Code の機能で、設定ファイルに書けば使えます。type: "prompt" と type: "agent" はモデルを呼ぶため、その分の利用量がかかります。

Q. Windows でも使えますか? A. 使えます。ドキュメントでは、シェルの形のコマンドは Git Bash(なければ PowerShell)で動き、"shell": "powershell" で PowerShell を選べます。npx などの .cmd は、args を付けた形では直接起動できないため、シェルの形で書くか node からスクリプトを起動します。

まとめ

  • Hooks は33のイベントで必ず動く処理。止めたいなら Pre 系、後処理なら Post 系
  • 設定は イベント → matcher → ハンドラー の3段。matcher は大文字小文字を区別し、記号を含むと正規表現。引数は if で絞る
  • 止めるなら exit 2 + stderr。exit 1 では止まらない
  • 理由つきの拒否・確認・書き換えは exit 0 + JSON。項目は hookSpecificOutput の中
  • フックは並列にすべて走る。拒否されても記録のフックは動く
  • PostToolUse のフォーマッターの変更は Claude に伝えられる
  • 動かないときは サンプル JSON のパイプ実行 → /hooks → --debug-file
  • 止めるなら disableAllHooks(1回なら --settings)か --bare。管理ポリシーのフックは管理側でしか止まらない
  • フックは自分の権限で動く。他人の設定を確かめ、正規表現のすり抜けを前提に多層で守る

参考・出典

確認日はいずれも 2026年10月16日です。

  • Claude Code ドキュメント「Hooks reference」:https://code.claude.com/docs/en/hooks
  • Claude Code ドキュメント「Automate actions with hooks」(hooks ガイド):https://code.claude.com/docs/en/hooks-guide
  • Claude Code ドキュメントの一覧:https://code.claude.com/docs/en/claude_code_docs_map.md
  • @anthropic-ai/claude-code(npm):https://www.npmjs.com/package/@anthropic-ai/claude-code
  • @anthropic-ai/claude-agent-sdk(npm、フックの入力・出力の型定義):https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk
  • Bash command validator のサンプル(GitHub):https://github.com/anthropics/claude-code/blob/main/examples/hooks/bash_command_validator_example.py

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Claude Code Hooksの使い方|全33イベント表と設定例・止め方」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?