
【Claude Code中級編 #3】Hooksで自動化する——保存前バリデーション・通知・ログ
前回はHooksの仕組みを理解した。今回は実際に動くコードを3本作る。
「Hooksって面白そうだけど何に使えばいいかわからない」という段階から、「これ使えるかも」に変わることをゴールにする。
この記事のコードはmacOS環境で実際に動作確認済み。
Hookに渡されるデータの構造
まず知っておくべき前提がある。
Hookの command が実行されるとき、Claude Codeはツールの情報を標準入力(stdin)にJSON形式で渡す。環境変数ではない。
実際にstdinの中身を確認するとこんなデータが来る:
{
"hook_event_name": "PostToolUse",
"tool_name": "Write",
"tool_input": {
"file_path": "/path/to/file.ts",
"content": "..."
},
"tool_response": { ... },
"session_id": "...",
"cwd": "/Users/..."
}
tool_name にツール名、tool_input にツールへの入力内容が入っている。これを取り出してコマンドに使う。
JSONの解析にはPython3(macOSに標準搭載)を使う。
事前準備:スクリプト置き場を作る
Hookのコマンドはシェルスクリプトに切り出して管理するのがおすすめ。クォートの問題が起きにくく、編集もしやすい。
mkdir -p .claude/hooks
settings.jsonのpathは絶対パスで書く。相対パスは動作しないことがある。自分のプロジェクトのパスに置き換えること。
# 自分のプロジェクトパスを確認する
pwd
# 例: /Users/yourname/my-project
settings.jsonでは以下のように絶対パスで指定する:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "/Users/yourname/my-project/.claude/hooks/log-write.sh"
}
]
}
]
}
}
/Users/yourname/my-project の部分を pwd で確認した自分のパスに変える。
実装例①:ファイルを保存したらlintを自動実行する
Claude Codeにコードを書いてもらうとき、スタイルが崩れることがある。CLAUDE.mdに「Prettierで整形して」と書いても、たまに忘れる。
PostToolUse × Write で、ファイル保存のたびに自動でPrettierを走らせる。
まずPrettierが入っているか確認:
npx prettier --version
# 3.x.x と表示されればOK。入っていなければ npm install -D prettier
.claude/hooks/prettier.sh:
#!/bin/bash
input=$(cat)
file=$(echo "$input" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('tool_input',{}).get('file_path',''))" 2>/dev/null)
if [ -n "$file" ]; then
npx prettier --write "$file" 2>/dev/null || true
fi
chmod +x .claude/hooks/prettier.sh
.claude/settings.json:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "/Users/yourname/my-project/.claude/hooks/prettier.sh"
}
]
}
]
}
}
python3 -c "..." でstdinのJSONを解析し、書き込まれたファイルパスを取り出してPrettierに渡している。cd は不要——prettierはファイルの絶対パスを直接受け取れる。
ESLintの場合は npx prettier --write を npx eslint --fix に変えるだけ。
実装例②:危険なコマンドをブロックする
rm -rf・git push --force・本番環境への直接デプロイ——Claude Codeが自律的に動いているとき、こういったコマンドを止める仕組みがないと不安になる。
PreToolUse × Bash で、実行前に内容をチェックしてキャンセルできる。
.claude/hooks/guard.sh:
#!/bin/bash
input=$(cat)
cmd=$(echo "$input" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('tool_input',{}).get('command',''))" 2>/dev/null)
if echo "$cmd" | grep -qE 'rm -rf|git push --force|git push -f'; then
echo "危険なコマンドはブロックされています: $cmd" >&2
exit 2
fi
chmod +x .claude/hooks/guard.sh
.claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "/Users/yourname/my-project/.claude/hooks/guard.sh"
}
]
}
]
}
}
exit 2 を返すとPreToolUseはツールの実行をキャンセルする。exit 1 ではブロックされないので注意。>&2 でstderrに出力することでClaude Codeがブロック理由を受け取れる。
ブロック対象を増やしたいときはgrepのパターンに追記するだけ:
grep -qE 'rm -rf|git push --force|git push -f|DROP TABLE|kubectl delete'
実装例③:作業ログを自動で残す
Claude Codeを長く使っていると「さっきどのファイルを変更したっけ」「今日何をやったか振り返りたい」という場面が出てくる。
PostToolUse × Write で、ファイル保存のたびにログを記録する。
.claude/hooks/log-write.sh:
#!/bin/bash
input=$(cat)
tool=$(echo "$input" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('tool_name',''))" 2>/dev/null)
file=$(echo "$input" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('tool_input',{}).get('file_path',''))" 2>/dev/null)
echo "$(date '+%Y-%m-%d %H:%M:%S') [$tool] $file" >> ~/.claude/work-log.txt
chmod +x .claude/hooks/log-write.sh
実行すると ~/.claude/work-log.txt にこんな記録が積み上がる:
2026-03-22 10:15:32 [Write] /Users/me/project/src/utils.ts
2026-03-22 10:18:11 [Write] /Users/me/project/src/components/Button.tsx
全ツールに反応させたい場合は matcher: "" にする。ただしBash・Edit・Readなど全操作が記録されてログが膨大になるので、まずはWriteだけに絞るほうが実用的。
複数のHookを組み合わせる
実装例①②③を一つの settings.json にまとめる場合、同じタイミング(PostToolUseなど)のmatcherを配列で並べる。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "/Users/yourname/my-project/.claude/hooks/guard.sh"
}
]
}
],
"PostToolUse": [
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "/Users/yourname/my-project/.claude/hooks/prettier.sh"
}
]
},
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "/Users/yourname/my-project/.claude/hooks/log-write.sh"
}
]
}
]
}
}
PostToolUse の中に2つのオブジェクトが入っている形。どちらも条件が合えば順番に実行される。
Hookが動かないときの確認ポイント
実装してみたが何も起きない——よくある原因をまとめる。
まずstdinにデータが来ているか確認するcat >> ~/hook-debug.txt だけのHookを書いてJSONが届いているか確認するのが一番早い。
スクリプトに実行権限がないchmod +x .claude/hooks/xxx.sh を忘れると実行されない。
settings.jsonのpathが相対パスになっているcommand は必ず絶対パスで書く。pwd で確認したパスをそのまま使う。
exit 1 でブロックしようとしている
PreToolUseのブロックには exit 2 が必要。exit 1 は無視される。
matcherのスペルミス"write" ではなく "Write"(大文字始まり)が正しい。
まとめ
- HookのデータはstdinにJSON形式で渡される(環境変数ではない)
python3 -c "import sys,json; ..."でstdinをパースして値を取り出す- settings.jsonの
commandは絶対パスで書く(pwdで確認) - PostToolUse × Write → lint/フォーマットの自動実行
- PreToolUse × Bash + exit 2 → 危険コマンドの実行前ブロック(exit 1 では効かない)
Hooksは「書いたら即効く」のが強み。まず cat >> ~/hook-debug.txt だけのシンプルなHookで動作確認してから実装を進めると詰まりにくい。
← 第2回:Hooksとは何か——Claude Codeの動作に割り込む | 第4回:Plan Modeを使いこなす——大きなタスクを安全に進める →