Claude Code hooks の実装ガイド——PreToolUse で mv・sed の上書きを防ぐ設定例と全スクリプト
Claude Code を使っていると「CLAUDE.md に書いたのに守ってくれない」という経験を一度はするはずです。
その根本的な解決策として hooks があります。この記事では、実際に使えるスクリプトと settings.json の設定例をそのまま掲載します。設計の考え方については元記事(Claude Code の hooks はなぜ CLAUDE.md や指示文では代替できないのか)を参照してください。
hooks とは——1行で説明すると
Claude Code がツールを実行する直前・直後に、シェルスクリプトを自動実行できる仕組み。LLM を介さないので確定的に動く。
設定ファイルの場所
.claude/
├── settings.json ← hooks をここに書く(リポジトリ共有)
└── hooks/
├── check-mv-overwrite.sh
├── check-cp-overwrite.sh
└── check-sed-awk-inplace.sh
.claude/settings.json をコミットすると、このリポジトリを触るすべてのエージェント・メンバーに自動適用されます。
settings.json の基本構成
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/check-mv-overwrite.sh"
},
{
"type": "command",
"command": "bash .claude/hooks/check-sed-awk-inplace.sh"
}
]
}
],
"Stop": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/check-process-completion.sh"
}
]
}
]
}
}
matcher はツール名(Bash / Write / Edit)または *(全ツール)を指定します。
exit code と permissionDecision の早見表
| 返り値 | 動作 |
|---|---|
exit 0 |
通過(ツールをそのまま実行) |
exit 2 + stderr 出力 |
Claude へのフィードバック付きでブロック |
permissionDecision: "ask" |
ユーザーへの確認プロンプトを表示 |
permissionDecision: "deny" |
ツール呼び出しをキャンセルして Claude に理由を送る |
hooks スクリプトは 標準入力から JSON を受け取り、permissionDecision を含む JSON を標準出力に返すか、exit code で返します。
スクリプト①: mv の上書きを防ぐ(check-mv-overwrite.sh)
#!/usr/bin/env bash
# PreToolUse: mv コマンドで移動先ファイルが存在する場合に確認プロンプトを出す
set -euo pipefail
INPUT=$(cat)
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
# mv コマンドでなければ素通し
if [[ ! "$CMD" =~ ^[[:space:]]*mv([[:space:]]|$) ]]; then
exit 0
fi
# コマンドをトークン分割して最後の引数を DEST とみなす
read -ra TOKENS <<< "$CMD"
DEST="${TOKENS[-1]}"
# オプションフラグは除外
while [[ "$DEST" == -* ]]; do
DEST="${TOKENS[-2]}"
TOKENS=("${TOKENS[@]:0:${#TOKENS[@]}-1}")
done
# 移動先が既に存在する場合のみ確認
if [[ -e "$DEST" ]]; then
cat <<JSON
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "ask",
"permissionDecisionReason": "上書き警告: $DEST が既に存在します。上書きしますか?"
}
}
JSON
fi
exit 0
スクリプト②: sed の in-place 編集にバックアップを強制(check-sed-awk-inplace.sh)
#!/usr/bin/env bash
# PreToolUse: バックアップなしの sed -i を検知して確認プロンプトを出す
set -euo pipefail
INPUT=$(cat)
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
emit_ask() {
cat <<JSON
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "ask",
"permissionDecisionReason": "$1"
}
}
JSON
exit 0
}
# sed が含まれていなければ素通し
if [[ ! "$CMD" =~ sed ]]; then
exit 0
fi
# -i.<拡張子> 形式(-i.bak 等)はバックアップあり → 素通し
if [[ "$CMD" =~ -i\.[a-zA-Z0-9_-]+ ]]; then
exit 0
fi
# -i '' (macOS) または -i (GNU) はバックアップなし → 確認
if [[ "$CMD" =~ sed.*-i[[:space:]]+\'\' ]] || [[ "$CMD" =~ sed.*-i[[:space:]] ]]; then
emit_ask "sed -i はバックアップなしの in-place 編集です。-i.bak を推奨します。実行しますか?"
fi
exit 0
スクリプト③: 未完了プロセスがあればセッション終了をブロック(check-process-completion.sh)
#!/usr/bin/env bash
# Stop hook: 進捗ファイルに未完了チェックリストが残っていたら終了をブロック
PROGRESS_DIR="${CLAUDE_PROJECT_DIR:-.}/.claude/progress"
if [[ ! -d "$PROGRESS_DIR" ]]; then
exit 0
fi
for file in "$PROGRESS_DIR"/*.md; do
[[ -e "$file" ]] || continue
UNCHECKED=$(grep -n "^- \[ \]" "$file" 2>/dev/null || true)
if [[ -n "$UNCHECKED" ]]; then
echo "⚠️ 未完了のプロセスステップがあります: $file" >&2
echo "$UNCHECKED" >&2
exit 2
fi
done
exit 0
スクリプトに実行権限を付ける
chmod +x .claude/hooks/*.sh
マルチエージェント構成での使い方
.claude/settings.json をコミットするだけで、秘書エージェント・エンジニアエージェント・マーケターエージェントなどすべてのサブエージェントに同一のガードレールが適用されます。個々のエージェント定義に同じルールを書く必要はありません。
エージェントA ──┐
エージェントB ──┼──→ PreToolUse hook ──→ check-mv-overwrite.sh ──→ ブロック or 通過
エージェントC ──┘
CLAUDE.md に書いたルールはエージェントが「読んで判断する」(確率的)のに対し、hooks は「ランタイムが直接実行する」(確定的)という点が本質的な違いです。
元記事
Claude Code の hooks はなぜ CLAUDE.md や指示文では代替できないのか——マルチエージェント環境での安全設計3原則
設計の考え方(なぜ CLAUDE.md では代替できないのか)・ブロック vs 警告の判断基準・タイムアウト設計については元記事で詳しく解説しています。
