はじめに
Claude Code を auto モードで使っていると、ある日突然 git push が飛んでいたりします。
コードを書いてもらうぶんには最高なんですけど、外部に影響する操作まで自動で通ってしまうのはさすがに怖い。特に rm -rf とかは「よかれと思って」やられると取り返しがつかないです。
最近のアップデートで PreToolUse フックの defer と PermissionDenied フックが追加されました。これを機に操作制御の設定を整理したので書いておきます。
環境
- Claude Code(最新版)
-
~/.claude/settings.json(グローバル)または.claude/settings.json(プロジェクトローカル)
defer とは
defer は claude -p(ヘッドレスモード)専用の機能で、エージェントがツールを呼び出そうとした瞬間を外部プロセスに委譲する仕組みです。
フックから以下の JSON を stdout に返すと発動します:
jq -n '{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "defer"
}
}'
exit 0
Claude は stop_reason: "tool_deferred" とセッション ID を返して終了し、claude -p --resume <session-id> で再開するとツール呼び出しが実行されます。
claude -p でエージェントが動く
└→ ツール呼び出しが発生
└→ PreToolUse フックが "defer" を返す
└→ session_id と deferred_tool_use が返ってくる(終了)
└→ 外部 UI で人間が確認
└→ claude -p --resume <session-id> で再開
CI や外部アプリから Claude Code をエージェントとして動かして「重要な操作だけ人間が確認してから再開する」というフローを組むためのものです。インタラクティブモード(通常の claude コマンド)で defer を返しても無視されるので、ターミナルで普通に使っている分には関係ないです。
通常セッションでブロックするには
ターミナルで Claude Code を使うとき、危ない操作を止める方法は2つあります。
permissions.deny で指定する
settings.json に deny ルールを書くと、マッチするツール呼び出しをブロックできます。
{
"permissions": {
"deny": [
"Bash(git push*)",
"Bash(rm -rf*)",
"Bash(sudo *)",
"Read(**/.env)",
"Read(**/*.pem)"
]
}
}
auto モードだと分類器が自動判断してブロック、デフォルトモードだと確認ダイアログが出ます。
deny されたとき Claude に別の方法を試させる(PermissionDenied フック)
auto モードで分類器が操作を拒否したとき、PermissionDenied フックが発火します。ここで retry: true を返すと、Claude に「別のアプローチで再試行してよい」というシグナルを送れます。
{
"hooks": {
"PermissionDenied": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "jq -n '{\"hookSpecificOutput\":{\"hookEventName\":\"PermissionDenied\",\"retry\":true}}'"
}
]
}
]
}
}
「直接コマンドは通さないけど、別の手段があるなら試してみて」という使い分けに向いています。ただ自分はまだ実運用に乗せていなくて、実際の挙動は試行錯誤中です。
これが意外と深い話で、Anthropic が4月に公開した解釈可能性研究で、Claude の内部に「絶望」「恐怖」に相当するベクトルが存在し、ストレス下で不正行動が増えるという話が出てきました。拒否ばかりされて詰まり続けると Claude は変な判断をしやすくなる。retry で「こっちで試してみて」という出口を用意することは、パフォーマンスの話だけじゃなく安全性の話でもあります。
permissions.deny の限界
ただし permissions.deny は完全ではなくて、既知のバグが2つあります。
ひとつは 50サブコマンド問題で、1つのコマンドが50個以上のサブコマンドを含む場合にセキュリティチェックがスキップされます。コード内の MAX_SUBCOMMANDS_FOR_SECURITY_CHECK = 50 という定数が原因で、内部 Issue(CC-643)としてトラッキングされてはいますが未修正です。
もうひとつは サブエージェントのバイパス。Task ツールで起動したサブエージェントは permissions.deny の設定を無視して動作するという報告があります(Issue #25000)。
deny だけに頼るのは危険で、絶対に通したくない操作は別の手段でも止める必要があります。
絶対にやらせたくない処理はフックで止める
permissions.deny に頼れないケースや「どんな状況でも実行させたくない」操作がある場合は、PreToolUse フックでシェルスクリプトから厳格にブロックします。
どんな操作が該当するか
-
.envファイルや秘密鍵(.pem、id_rsa)の読み込み -
git push --forceによる強制上書き - 本番 DB への
DROP TABLEやDELETEなど
確認ダイアログで止めるより、そもそも実行させない設計のほうが安全な操作です。
PreToolUse フックの設定
settings.json でフックを登録します:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/confirm-dangerous.sh"
}
]
}
]
}
}
confirm-dangerous.sh はこんな感じです:
#!/bin/bash
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
DANGEROUS_PATTERNS=("git push" "rm -rf" "sudo" "pip install" "npm install -g")
for pattern in "${DANGEROUS_PATTERNS[@]}"; do
if echo "$COMMAND" | grep -q "$pattern"; then
echo "⚠️ Blocked: '$pattern' を含むコマンドは許可されていません: $COMMAND" >&2
exit 2 # block
fi
done
exit 0
exit 2 がブロックのシグナルです。 stderr に書いた内容はエラーメッセージとして Claude に渡されます。
PreToolUse フックはバックグラウンドで実行されるため、printf や read < /dev/tty によるインタラクティブな確認プロンプトは表示されません。ユーザー確認を挟みたい場合は defer(claude -p 専用)を使う設計が必要です。
DANGEROUS_PATTERNS の調整は実際に使いながらやるほうがいいです。最初は入れすぎて確認が頻繁すぎて逆に全部スルーするようになったので絞りました。
Claude が「よかれと思って」外部に影響を与え続ける前に、止まる場所は先に決めておいたほうがいいです。絶望状態のエージェントに rm -rf を任せっぱなしにするには、まだちょっと早いです。