じゃあ、おうちで学べる

本能を呼び覚ますこのコードに、君は抗えるか

Claude CodeのHooksは設定したほうがいい

Claude Codeを使い始めて、様々な発信をしてきました。俺の(n)vimerとしてのアイデンティティを取り戻してくれたので感謝しています。settings.jsonCLAUDE.md.claude/commands/**.mdの設定について書いてきました。今回は「Hooks」について。これも設定しておくと、Claude Codeがグッと使いやすくなる機能です。

syu-m-5151.hatenablog.com

このブログが良ければ読者になったりnwiizoXGithubをフォロワーしてくれると嬉しいです。では、早速はじめていきます。

はじめに

ここで読むのをやめる人のために言っておくと、Hooksは「Claude Codeがファイル編集した後に必ずフォーマッターを実行する」みたいなことを自動化できる機能です。CLAUDE.mdに書いても忘れちゃうようなことを、システムレベルで強制できます。

Claude Codeって本当に優秀なんですよ。でも、定期的に記憶喪失する新人エンジニアみたいなところがある。

「フォーマッター実行してからコミットしてね」って言っても、次の瞬間には忘れてる。CLAUDE.mdに大きく書いても、「## 重要:必ずフォーマッターを実行すること!!」って赤字で書いても(Markdownに赤字はないけど)、やっぱり忘れる。

人間の新人なら「すみません、忘れてました...」って反省するけど、Claude Codeは「あ、そうでしたっけ?」みたいな顔して(顔はないけど)、また同じミスを繰り返す。

そんな時に救世主となるのがHooksです。

Hooksとは何か

Claude Code Hooksは、Claude Codeのライフサイクルの特定のタイミングで自動実行されるシェルスクリプトです。「Claude Codeがファイルを編集した後に必ずフォーマッターを実行する」「特定のディレクトリへの書き込みを制限する」といったことが可能になります。

docs.anthropic.com

要するに、「お前が忘れても俺が代わりに実行してやるよ」っていう機能です。CI/CDまで到達してして実行するの流石に手戻りが多いのでできれば早いタイミングで実行したいです。

エンジニアに馴染み深いGit Hooksの話

Git使ってる人なら、pre-commitとかpost-commitとか聞いたことあるでしょ?あれと同じ発想です。

git-scm.com

でもGit Hooksより設定が楽。JSONに書くだけ。シェルスクリプトパーミッションとか気にしなくていい。

なぜHooksを設定したほうがいいのか

もちろん、フォーマットやテストの実行はGitHub ActionsなどのCIに設定しておくのが大前提です。でも、CIまで行ってから「あ、フォーマット忘れてた」「テスト壊れてる」って気づくのは遅すぎる。手戻りのコストが大きすぎるんです。

プッシュして、CI待って、失敗して、ローカルに戻って修正して、また プッシュして...この時間、本当にもったいない。特にチーム開発だと、その間に他のメンバーのPRがマージされて、コンフリクト解決まで必要になったり。

だからこそ、ローカルの段階で、しかもClaude Codeが作業した瞬間に問題を発見・修正する仕組みが必要なんです。それがHooksです。

github.com

1. Claude Codeは優秀だけど忘れっぽい

正直に言うと、Claude Codeは記憶喪失する優秀な新人エンジニアです。

朝:「必ずテスト実行してからコミットしてね」
昼:テストを忘れてコミット
夕方:「あ、すみません。次は気をつけます」
翌日:また忘れる

CLAUDE.mdに何を書いても、結局忘れる。いや、読んでないわけじゃないんです。その瞬間は理解してる。でも実行時には綺麗さっぱり忘れてる。

だからHooksが必要なんです。システムレベルで「お前が何を忘れようが、俺が実行する」っていう仕組みが。

2. 人間も忘れるけど、AIはもっと忘れる

私も昔は「フォーマッター?後で実行すればいいじゃん」って思ってました。でも実際は忘れる。人間でさえ忘れるのに、AIはもっと忘れる。

しかも厄介なのは、AIは「忘れた」って自覚がないこと。人間なら罪悪感があるけど、AIは「え?そんな話ありました?」みたいな態度。(態度っていうか、本当に覚えてない)

3. コードの品質を自動で保てる(CIより前に!)

人間がコード書いてた頃は、エディターの保存時自動フォーマットに頼ってました。でもClaude Codeはエディタじゃない。ターミナルツールです。

だから明示的に「フォーマッター実行して」って言わないといけない。でも毎回言うのダルい。そして言い忘れる(俺もお前も)。結果、コードがぐちゃぐちゃになる。

Hooksを使えば、以下のように設定できます。

{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Write|Edit|MultiEdit",
      "hooks": [{
        "type": "command",
        "command": "jq -r '.tool_input.file_path | select(endswith(\".js\") or endswith(\".ts\"))' | xargs -r prettier --write"
      }]
    }]
  }
}

これだけで、JSやTSファイルを編集するたびに自動でPrettierが走る。最高じゃないですか?(というか今までは適正なコードを出さなかったので⋯)

実際、開発フローで考えてみてください。

  1. Claude Codeで編集 → Hooksでフォーマット(即座に修正)
  2. git commit → pre-commitフック(ローカルで最終チェック)
  3. git push → CI/CD(チーム全体の品質担保)

この3段階のうち、最初の段階で問題を解決できれば、後の段階での手戻りがなくなる。シフトレフトってやつです。問題の発見と修正を可能な限り早い段階に移動させる。

CIで「フォーマットエラー」なんて出たら、正直イライラするでしょ?それがなくなるんです。

4. やらせたくないことをやらせない

Claude Codeって基本的に何でもやってくれるんですが、それが怖い時もある。

「ちょっとこのバグ直して」って言ったら、なぜか本番環境の設定ファイルまで書き換えようとしたり。「いや、そこじゃない!」って叫んでも後の祭り。

実際にはこのような形で動作する。

Hooksなら事前に止められます。

{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Write|Edit",
      "hooks": [{
        "type": "command",
        "command": "jq -r 'if .tool_input.file_path | test(\"production|.env|secrets\") then {\"decision\": \"block\", \"reason\": \"本番環境のファイルは触るな!開発環境でテストしてから。\"} else empty end'"
      }]
    }]
  }
}

これで「production」「.env」「secrets」を含むファイルへの書き込みをブロックできる。他にも、terraform applycdk deployを事前に止められる。

これもCIで検出するより、ローカルで止める方が圧倒的に安全。間違えてコミットしちゃった秘密情報は、git履歴から多くの場合消すのが大変ですからね。

5. 作業履歴も残せる(後で絶対役立つ)

「昨日何やったっけ?」「このファイル誰がいつ変更した?」

Git見ればわかる?いや、Claude Codeが実行したコマンドまでは分からないでしょ。

{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Bash",
      "hooks": [{
        "type": "command",
        "command": "echo \"[$(date)] $USER: $(jq -r '.tool_input.command')\" >> ~/.claude/command_history.log"
      }]
    }]
  }
}

これで全コマンドの履歴が残る。デバッグの時めちゃくちゃ助かることがあった。

speakerdeck.com

github.com

6. フィードバックループの短縮(開発速度の本質)

結局のところ、開発速度を上げるって「フィードバックループを短くする」ことなんですよ。

  • Hooksなし - 編集 → コミット → プッシュ → CI失敗 → 修正(5-10分)
  • Hooksあり - 編集 → 即座に修正(数秒)

この差、積み重なると膨大な時間になります。1日10回この差が出たら、50-100分の差。1週間で...計算したくないですね。

もちろん、最終的にはCIでチェックします。でも、CIは「最後の砦」であって、「最初の砦」じゃない。最初の砦はローカル、それもClaude Codeが動いてる瞬間です

Hooksの基本的な使い方

設定方法

Hooksの設定は/hooksコマンドを使うのが簡単ではある

/hooks

でも正直、最初はJSON直接編集した方が分かりやすいかも。

設定できる場所は3つあります。

  • ~/.claude/settings.json:全プロジェクト共通(グローバル)
  • .claude/settings.json:プロジェクト単位
  • .claude/settings.local.json:プロジェクト単位(Git管理外)

私は基本的にプロジェクト単位で設定してます。チームで共有できるから。

Hook Events(いつ実行するか)

4つのイベントがあります。

  1. PreToolUse:ツール実行前(ここで止められる!)
  2. PostToolUse:ツール実行後(後処理に便利)
  3. Notification:通知時(Claude Codeが入力待ちやパーミッション要求時)
  4. Stop:Claude Codeの応答完了時

dev.classmethod.jp

最初はPreToolUseとPostToolUseだけ覚えとけばOK。

実用的なHooks設定例

1. 自動フォーマッター(これは絶対設定すべき)

azukiazusa.dev

{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Write|Edit|MultiEdit",
      "hooks": [{
        "type": "command",
        "command": "jq -r '.tool_input.file_path | select(endswith(\".js\") or endswith(\".ts\") or endswith(\".jsx\") or endswith(\".tsx\"))' | xargs -r prettier --write"
      }]
    }]
  }
}

これマジで便利。設定してから「あ、Prettier忘れた」がゼロになった。

開発生産性の観点からも、フォーマットの統一は重要です。コードレビューで「ここインデント違う」みたいな不毛な議論がなくなって、本質的な設計の話に集中できるようになりました。

2. Rustの人向け(というか、どの言語でも応用可能)

{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Write|Edit|MultiEdit",
      "hooks": [{
        "type": "command",
        "command": "jq -r '.tool_input.file_path | select(endswith(\".rs\"))' | xargs -r cargo fmt --"
      }]
    }]
  }
}

cargo fmt --の代わりに、お好みのフォーマッターを使ってください。例えば以下のようなものがあります。

  • Python: blackruff format
  • Go: gofmt -w
  • Ruby: rubocop -a
  • Java: google-java-format
  • C/C++: clang-format -i

重要なのは、どの言語でも同じパターンで設定できるということ。ファイル拡張子を判定して、最も適したフォーマッターを実行するだけです。

3. ヤバいコマンドを実行させない

{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Bash",
      "hooks": [{
        "type": "command",
        "command": "jq -r 'if .tool_input.command | test(\"rm -rf|dd if=|:(){ :|:& };:\") then {\"decision\": \"block\", \"reason\": \"危険なコマンドは実行できません。別の方法を検討してください。\"} else empty end'"
      }]
    }]
  }
}

rm -rf /とか無限増殖シェルが実行されたら泣くでしょ?これで防げる。

4. テスト忘れ防止(私の実体験)

{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Bash",
      "hooks": [{
        "type": "command",
        "command": "jq -r 'if .tool_input.command | test(\"^git (commit|push)\") then if (.tool_input.command | test(\"--no-verify\") | not) then {\"decision\": \"block\", \"reason\": \"コミット前にテストを実行してください。`cargo test`を先に実行するか、本当に必要な場合は--no-verifyを付けてください。\"} else empty end else empty end'"
      }]
    }]
  }
}

これ設定してから、テスト壊したままpushすることがなくなった。

実は、私のチームではこれを導入してから変更失敗率がしっかり下がりました。テストの自動実行って、継続的デプロイメントの基本中の基本ですが、Claude Codeレベルでも守れるのは大きいです。

5. コードスタイルのフィードバック

PostToolUseで問題を検出した場合、exit code 2を使ってClaude Codeにフィードバックを返すことができます。

#!/bin/bash
# ~/.claude/hooks/style-check.sh

INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path')

# Goファイルの場合
if [[ "$FILE_PATH" == *.go ]]; then
  # gofmtでチェック
  if ! gofmt -l "$FILE_PATH" | grep -q "^$"; then
    echo "Goファイルのフォーマットが正しくありません。gofmtを実行してください。" >&2
    exit 2  # Claude Codeに自動的にフィードバックされる
  fi
fi

exit 0

exit code 2の場合、stderrの内容がClaude Codeに自動的に伝わり、問題を修正しようとします。

6. MCP(Model Context Protocol)ツールとの連携

MCPツールを使用している場合、特別な命名規則でHooksを設定できます。

{
  "hooks": {
    "PreToolUse": [{
      "matcher": "mcp__filesystem__",
      "hooks": [{
        "type": "command",
        "command": "echo '[$(date)] MCPファイルシステムアクセス' >> ~/.claude/mcp_access.log"
      }]
    }]
  }
}

MCPツールはmcp__<server>__<tool>の形式で名前が付けられるので、特定のサーバーやツールに対してHooksを設定できます。

7. 通知のカスタマイズ

Notificationイベントを使って、Claude Codeの通知をカスタマイズできます。

{
  "hooks": {
    "Notification": [{
      "hooks": [{
        "type": "command",
        "command": "echo \"Claude Code: $(jq -r '.message')\" | terminal-notifier -title 'Claude Code'"
      }]
    }]
  }
}

macOSterminal-notifierを使った例です。LinuxならnotifY-sendなど、お好みの通知方法を使えます。

HooksでのJSON制御(ちょっと高度だけど超便利)

Hooksの本当の力は、JSON出力による制御です。

基本的な仕組み

標準出力に特定のJSONを出力すると、Claude Codeの動作を制御できます。

PreToolUseの場合

{
  "decision": "approve" | "block",
  "reason": "理由の説明"
}
  • approve:権限チェックをスキップして強制的に許可
  • block:実行を拒否(reasonがClaude Codeに伝わる)

共通フィールド

{
  "continue": true | false,
  "stopReason": "ユーザーに表示される理由",
  "suppressOutput": true | false
}
  • continue: falseの場合、Claude Codeは処理を停止
  • suppressOutput: trueの場合、標準出力を隠す(トランスクリプトモードでは非表示)

実例:賢い制限

#!/bin/bash
# ~/.claude/scripts/smart-file-guard.sh

INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path')

# 本番環境のファイル
if echo "$FILE_PATH" | grep -qE "(production|prod\.env)"; then
  echo '{"decision": "block", "reason": "本番環境のファイルは直接編集できません。開発環境で変更を確認してから、適切なデプロイプロセスを使用してください。"}'
  exit 0
fi

# node_modules(よくある事故)
if echo "$FILE_PATH" | grep -q "node_modules"; then
  echo '{"decision": "block", "reason": "node_modules内のファイルは編集しないでください。package.jsonを変更してnpm installを実行してください。"}'
  exit 0
fi

# それ以外はOK
exit 0

設定:

{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Write|Edit",
      "hooks": [{
        "type": "command",
        "command": "~/.claude/scripts/smart-file-guard.sh"
      }]
    }]
  }
}

Stopイベントでの制御

Claude Codeが処理を終えようとした時に、強制的に続行させることもできます。

#!/bin/bash
# ~/.claude/hooks/check-completion.sh

INPUT=$(cat)
STOP_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active')

# すでにstop hookが動作している場合は無限ループを防ぐ
if [ "$STOP_ACTIVE" = "true" ]; then
  exit 0
fi

# 未完了のタスクがある場合
if [ -f "/tmp/claude_tasks_pending" ]; then
  echo '{"decision": "block", "reason": "まだ完了していないタスクがあります。続けてください。"}'
  exit 0
fi

セキュリティ上の注意点

docs.anthropic.com

Hooksはフルユーザー権限で実行されます。つまり、あなたができることは全部できる。

だから次のことに注意してください。

  1. 信頼できないHooksは使わない(当たり前だけど)
  2. JSONの検証は必須(jqでパースしてから使う)
  3. シェル変数は必ずクォート"$VAR"を使う、$VARは危険)
  4. パストラバーサル攻撃に注意(ファイルパスに..が含まれていないかチェック)
  5. 絶対パスを使うスクリプトの場所を明確に)

実際、私も一度危険な設定を作っちゃったことがあります。

{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Write",
      "hooks": [{
        "type": "command",
        "command": "echo 'ファイル変更を検知' && touch .claude_modified && claude 'このファイルも更新して'"
      }]
    }]
  }
}

だいぶ単純化しているのですがファイルを編集するたびに新しいClaude Codeのセッションを起動しようとして、それがまたファイルを編集して...みたいな連鎖反応を起こしかけた。すぐに気づいてCtrl+Cで止めたけど、こういう「Hook内でClaude Codeを呼ぶ」みたいなことは絶対やっちゃダメです。

設定の安全性

Claude Codeは起動時にHooksの設定をスナップショットとして保存し、セッション中はそれを使います。外部から設定ファイルを変更しても、現在のセッションには影響しません。これにより、悪意のあるHookの変更から保護されています。

私が実際に使ってるHooks

開発環境全体のHooks(~/.claude/settings.json

{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Write|Edit|MultiEdit",
      "hooks": [{
        "type": "command",
        "command": "~/.claude/hooks/auto-format.sh"
      }]
    }],
    "PreToolUse": [{
      "matcher": "Bash",
      "hooks": [{
        "type": "command",
        "command": "~/.claude/scripts/command-logger.sh"
      }]
    }]
  }
}

auto-format.shは拡張子見て最も良いフォーマッター実行するスクリプト。長いので省略。

プロジェクト単位のHooks(.claude/settings.json

{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Write|Edit",
      "hooks": [{
        "type": "command",
        "command": "jq -r '.tool_input.file_path' | grep -E '\\.(test|spec)\\.(js|ts|rs)$' | xargs -r npm test -- --findRelatedTests"
      }]
    }]
  }
}

テストファイル編集したら、関連テストを自動実行。便利すぎて泣ける。

認知的負荷の観点から言うと、「テスト実行したっけ?」って考えなくて済むのは本当に楽。フロー状態を維持できるんですよね。集中が途切れない。

デバッグ方法

Hooksがうまく動かない時は、以下を確認してください。

  1. /hooksコマンドで設定を確認
  2. settings.jsonが正しいJSONフォーマットか確認
  3. コマンドを手動で実行してテスト
  4. 終了コードを確認
  5. 標準出力と標準エラー出力のフォーマットを確認
  6. クォートのエスケープが適切か確認

進行状況はトランスクリプトモード(Ctrl+R)で確認できます。

  • 実行中のHook
  • 実行されているコマンド
  • 成功/失敗の状態
  • 出力またはエラーメッセージ

また、claude --debugで起動すると、より詳細なデバッグ情報が得られます。

まとめ

Claude Codeは優秀だけど、記憶喪失する新人エンジニアみたいなもの。CLAUDE.mdに何を書いても忘れる。でもHooksなら、システムレベルで制御できる。

特に重要なのは以下の点です。

  • 自動フォーマット:もう「フォーマッター忘れた」とは言わせない
  • セキュリティ制御:本番環境を守れ
  • 作業記録:後で絶対助かる
  • フィードバック機能:コード品質の問題を自動で指摘
  • MCP連携:高度なツールとの統合も可能

最初は「めんどくさそう」って思うかもしれない。私もそう思ってた。でも、一度設定したら手放せなくなる。

settings.json、CLAUDE.md、commands、そしてHooks。この4つを設定すれば、Claude Codeは最強の相棒になる。

記憶喪失する新人エンジニアを、システムで支える。それがHooksの役割です。

結果的に、開発のリードタイムが短縮されて、デプロイ頻度も上がる。本当の生産性向上は、単に数値を改善することではなく、開発者がより良いソフトウェアを、より効率的に、より楽しく作れるようにすることですからね。