Claude Code セーフティガイド:Hooks・権限・Git Worktrees で誤削除を防ぐ
TL;DR:Claude Code を本番ツリーで走らせるなら、settings.json の permissions.deny、PreToolUse Hooks、--worktree フラグの三層を必ず重ねる。Hook は hookSpecificOutput.permissionDecision: "deny" を返すだけでツール呼び出しを止められるので、rm -rf や git push --force を JSON 数行で物理的にブロックできる。Worktree は誤削除のブラスト半径を別ディレクトリに閉じ込めるので、acceptEdits や bypassPermissions モードを安全に使う前提になる。
なぜ Claude Code に「安全装置」が必要なのか
Claude Code は強力です。/goal でゴール条件を満たすまで走り続け、Agent View でバックグラウンドにセッションを 10 個並べることもできます。ただ、エージェントが自律的に走るほど、人間が画面を見ていない時間が増える。rm -rf node_modules のつもりが rm -rf . になっていた、git checkout . で 3 時間分の未コミット変更が吹き飛んだ、.env.production を間違って Git に push してしまった。対話 UI では「実行しますか?」のダイアログで止められたはずの事故が、自律ループでは止まらない構造に変わった。これが Claude Code 時代の事故パターンです。
毎回人間に確認を取らせるのでは、Claude Code を使う意味が消えます。だから設定ファイルに「危険なパターンを実行前に物理的に拒否する仕組み」を焼き付けるしかありません。Claude Code はそのために、公式で三種類の防御層を用意しています。
- 権限ルール:
settings.jsonのallow / deny / askで、ツール呼び出しをパターンマッチで絞る - Hooks:
PreToolUseで実行直前にカスタムロジックを差し込み、deny判定を返す - Worktrees:
--worktreeで別ディレクトリにチェックアウトし、誤操作の影響範囲を切り離す
それぞれ単独でも効きますが、本番リポジトリで Claude Code を回すなら全部入れます。下から順に組んでいきます。
第 1 層:権限モードと settings.json
Claude Code には 6 つの権限モードがあります。Shift+Tab で巡回切替できるのはデフォルトでは default → acceptEdits → plan の 3 つだけで、auto / dontAsk / bypassPermissions は --permission-mode フラグや設定で明示的に有効化したときだけ顔を出します。永続的なデフォルトは .claude/settings.json または ~/.claude/settings.json の defaultMode で指定します。
| モード | 振る舞い | 使いどころ |
|---|---|---|
default | 読み取りのみ自動承認、編集・実行は毎回確認 | 初見のリポジトリで様子見するとき |
acceptEdits | ファイル編集と mkdir / touch / mv / cp / rm 等の基本 filesystem コマンドを自動承認、その他 Bash は確認 | 設計合意済みの実装フェーズ |
plan | 読み取り+読み取り専用シェルコマンドのみ。ソース編集は止まる | コードレビュー・設計議論 |
auto | 別モデルの分類器が安全性を判定して自動承認(要件あり、研究プレビュー) | 長時間タスク、確認疲れ削減 |
dontAsk | permissions.allow で事前許可したものだけ実行、それ以外は自動拒否 | CI パイプライン・閉じた環境 |
bypassPermissions | deny ルールと rm -rf / 等のサーキットブレーカー以外は全部自動承認 | コンテナ・VM 内の隔離タスク |
allow / deny / ask の評価順
permissions ブロックの中で、ルールは deny → ask → allow の順で評価されます。最初にマッチしたルールが勝ち。つまり deny に書けば、どれだけ allow を積んでも上書きされます。本番リポジトリで最初に書くべきは deny です。
{
"permissions": {
"defaultMode": "default",
"deny": [
"Bash(rm -rf *)",
"Bash(rm -rf /*)",
"Bash(git push * --force*)",
"Bash(git push * -f*)",
"Bash(git reset --hard *)",
"Bash(git checkout .*)",
"Bash(DROP TABLE *)",
"Write(.env*)",
"Write(./secrets/**)",
"Read(.env.production)"
],
"ask": [
"Bash(npm publish*)",
"Bash(docker push*)",
"Bash(gh pr merge*)"
],
"allow": [
"Bash(git status)",
"Bash(git diff*)",
"Bash(git log*)",
"Bash(npm run test*)",
"Bash(npm run lint*)",
"Read(./**)",
"Edit(./src/**)",
"Write(./src/**)"
]
}
}
パターンに * を含めるとき、これは正規表現ではなく Claude Code 独自のワイルドカードで、* 一つで空白を含む任意の文字列にマッチします。Bash(rm -rf *) は rm -rf node_modules も rm -rf . も両方マッチするので、rm -rf を一切禁じる方向で割り切るなら一行で済みます。なお bypassPermissions モードでも rm -rf / や rm -rf ~ のようなルート・ホーム削除はサーキットブレーカーで確認プロンプトが必ず出るので、Bash(rm -rf /*) は二重の保険程度の意味合いになります。
Write(.env*) を入れておくと、.env、.env.local、.env.production まで一括で書き込み禁止になります。.gitignore に書いてあっても Claude Code はファイルを書けてしまうので、書き込み側で塞ぐ必要があります。
ask は確認プロンプトを出すので「絶対やらせたくない」ではなく「自分が見ているときだけ通したい」ものに使います。npm publish や gh pr merge のような不可逆操作が代表例です。
チーム共有と個人設定の分離
.claude/settings.json はリポジトリにコミットしてチーム全員に効きます。.claude/settings.local.json は gitignore 済みで個人用です。誰がやっても危険なルール(rm、DB drop)は前者に、個人の ~/secrets/ パスを守るルールは後者に。~/.claude/settings.json はマシン横断のデフォルトで、ここに deny を書いておくと、新しいリポジトリで Claude Code を立ち上げた瞬間から最低限の防御がかかります。
第 2 層:PreToolUse Hooks で「物理的に」止める
権限ルールは静的なパターンマッチなので、文脈を見て判断する芸当はできません。「master ブランチ上での git push --force は禁止、feature ブランチなら OK」みたいな条件分岐は Hooks の役目です。
Claude Code Hooks はライフサイクルイベントに任意のスクリプトを差し込む仕組みで、PreToolUse / PostToolUse / UserPromptSubmit / SessionStart / WorktreeCreate など 30 以上のイベントポイントが用意されています。安全用途で使うのは主にこの 4 つです。
| イベント | 発火タイミング | できること |
|---|---|---|
PreToolUse | ツール実行直前 | allow / deny / ask を返してブロック・許可・確認 |
PostToolUse | ツール成功直後 | lint・型チェック・テスト、結果を Claude に返す |
UserPromptSubmit | プロンプト送信直後 | 秘密情報を含む入力を遮断、追加コンテキスト注入 |
SessionStart | セッション開始時 | git 状態・open issues をコンテキスト注入 |
rm -rf を完全にブロックする最小例
settings.json に Hook を登録します。matcher は Bash だけに絞り、if 句で rm を含むコマンドだけスクリプトを呼びます。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(rm *)",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-destructive.sh",
"timeout": 10
}
]
}
]
}
}
スクリプト本体(実行ビットを忘れずに chmod +x):
#!/bin/bash
COMMAND=$(jq -r '.tool_input.command' < /dev/stdin)
if echo "$COMMAND" | grep -qE '(rm\s+-rf|git\s+push.*-{1,2}f|git\s+reset\s+--hard)'; then
jq -n --arg cmd "$COMMAND" '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: "破壊的コマンドをポリシーでブロック: \($cmd)"
}
}'
exit 0
fi
exit 0
挙動はこうです。Claude が rm -rf node_modules を実行しようとすると、スクリプトが標準入力で tool_input.command を受け取り、正規表現でマッチを判定。マッチすれば permissionDecision: "deny" を含む JSON を stdout に返す。Claude Code はそれを読んでツール呼び出しを中止し、permissionDecisionReason を Claude のコンテキストに戻すので、Claude は「ブロックされたから別のアプローチを考える」という挙動になります。
exit 2 を使う書き方もあります(stderr が Claude に渡され、ツールがブロックされる)。ただ JSON で permissionDecision を返すほうが理由メッセージを構造化して渡せるので、長く運用するなら JSON 形式を推奨します。
PostToolUse で「壊れたら戻す」より「壊さない」
PostToolUse は実行後に走るので、ファイルが書き換わった後に lint や型チェックを走らせ、失敗したら decision: "block" で Claude に再修正を促せます。ロールバックの仕組みではなく、書き直しを強制するワークフローです。
#!/bin/bash
FILE=$(jq -r '.tool_input.file_path' < /dev/stdin)
if [[ "$FILE" == *.ts || "$FILE" == *.tsx ]]; then
ERRORS=$(npx tsc --noEmit 2>&1 | head -20)
if [ -n "$ERRORS" ]; then
jq -n --arg err "$ERRORS" '{
decision: "block",
reason: "TypeScript 型エラー:\n\($err)"
}'
fi
fi
ここでは PreToolUse のような hookSpecificOutput ではなく、トップレベル decision: "block" を使います。PostToolUse はツールが既に動いた後なので「拒否」ではなく「Claude に通知して次のターンで直させる」のが正しいセマンティクスです。
Hooks の網羅的な活用例は別記事「Claude Code の Hooks・Subagents・Skills 徹底活用」を参照してください。本記事は安全用途に絞っています。
第 3 層:Git Worktrees でブラスト半径を分離する
権限と Hook を完璧に書いても、ルールに穴があれば事故は起きます。Bash(rm -rf *) を deny に入れていても、Bash(find . -delete) が抜けていたら同じ結果になる。だから最終層として「事故が起きても本番ツリーには触れない」という空間分離を入れます。Git Worktrees です。
claude —worktree で 1 コマンド分離
Claude Code には標準で worktree サポートが入っています。--worktree または -w フラグを付けて起動すると、.claude/worktrees/<名前>/ 配下に新しいブランチを切り、別ディレクトリで Claude が立ち上がります。
# 名前付きで分離セッションを開始
claude --worktree feature-auth
# 別ターミナルでもう一個、独立して走る
claude --worktree bugfix-payment
# 名前を省略すると自動命名
claude --worktree
ワークツリーは origin/HEAD から派生するので、最新の master をベースにきれいな状態で始まります。途中のコミットを含めて派生したい場合は settings.json で "worktree": { "baseRef": "head" } を指定します。
.env を持ち込む .worktreeinclude
ワークツリーは新規チェックアウトなので、.env や .env.local のような gitignore 済みファイルは入りません。これだと開発サーバーが起動しないので、リポジトリルートに .worktreeinclude を置いて持ち込み対象を指定します。
.env
.env.local
config/secrets.json
これでワークツリー作成時に該当ファイルが自動コピーされます。gitignore 済みのファイルだけが対象なので、トラックされているファイルを二重に持つ事故は起きません。
サブエージェントごとに分離する
Claude Code のサブエージェント(/agents で定義する自前のエージェント)にも isolation: worktree をフロントマターに書いておくと、起動のたびに一時的な worktree が割り当てられます。変更がなければセッション終了時に自動削除されるので、/code-review のような読み取り中心のエージェントを並列で走らせても、本番ツリーが汚れません。
---
name: safe-refactor
description: 大きめのリファクタリングを worktree で隔離して試す
isolation: worktree
tools:
- Edit
- Write
- Bash
---
Bypass モードは worktree の中だけで使う
bypassPermissions モードは deny リスト以外を全自動承認するので速いです。ただし本リポジトリで直接有効にすると、deny ルールの穴がそのままファイル削除につながります。Bypass を使うのは worktree の中だけ、というルールを徹底すれば、事故が起きてもブランチを捨てるだけで終わります。
クリーンアップの挙動を理解しておく
--worktree で作ったセッションを終了するとき、Claude Code は変更の有無を見ます。
- 変更なし:ワークツリーとブランチを自動削除
- 変更あり:保持するか削除するか確認プロンプト
-p非対話モード:自動削除されない(git worktree removeで手動清掃)
非対話モードで CI に組み込むときは、後始末を忘れると .claude/worktrees/ が肥大化します。cleanupPeriodDays を settings.json で指定しておくと、サブエージェント起動時に起きた孤児ワークツリー(変更・未追跡ファイル・未 push コミットがないもの)は次回起動時に自動で掃除されます。ただし --worktree で自分で作ったものはこの掃除対象外なので、git worktree remove で明示的に消す必要があります。
推奨セットアップ:最小構成でコピペする
ここまでの 3 層を全部入れた settings.json の最小構成です。新規リポジトリで Claude Code を導入するときの出発点として使ってください。
{
"permissions": {
"defaultMode": "default",
"deny": [
"Bash(rm -rf *)",
"Bash(git push * --force*)",
"Bash(git reset --hard *)",
"Write(.env*)",
"Write(./secrets/**)"
],
"ask": [
"Bash(npm publish*)",
"Bash(gh pr merge*)",
"Bash(docker push*)"
]
},
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-destructive.sh",
"timeout": 10
}
]
}
]
},
"worktree": {
"baseRef": "fresh"
},
"cleanupPeriodDays": 7
}
そして .gitignore に一行追加:
.claude/worktrees/
これで、リスクの大きいタスクは claude --worktree feature-name で別ツリーに送り、本ツリーでは default または acceptEdits モードで普通に作業 ─ という運用が回り始めます。
API キーの保護も忘れない
Claude Code は Anthropic API キーを ANTHROPIC_API_KEY 環境変数から読み込みます。OAuth ログインを使う場合の認証情報は ~/.claude.json に格納され、外部の鍵管理ツールから動的に取得したいときは apiKeyHelper 設定でスクリプトを差し込めます。設定ファイルに鍵を直書きしないこと、Hook スクリプト内で echo "$ANTHROPIC_API_KEY" のようなデバッグを残さないこと ─ これは Claude Code 固有ではなく、CLI ツール全般の作法です。
ofox.ai のような OpenAI 互換ゲートウェイ経由で Claude を呼ぶ構成にしておくと、API キーのローテーションをゲートウェイ側で吸収できるので、誤って Hook スクリプトに鍵が混入するリスクを下げられます。Claude Code は ANTHROPIC_BASE_URL 環境変数で API エンドポイントを差し替えられるので、Anthropic 直叩きから ofox 経由に切り替えるのもキー再発行だけで済みます。
まとめ
| 層 | 仕組み | 防げる事故の例 |
|---|---|---|
| 1 | permissions.deny でパターン拒否 | rm -rf、git push --force、.env* への書き込み |
| 2 | PreToolUse Hook で動的判定 | 条件付き拒否(master でのみ禁止、など)、複雑な正規表現マッチ |
| 3 | --worktree で別ディレクトリ | deny ルールの穴を突かれても本ツリー無傷、bypass モードを安全に使える |
三層全部入れた状態で初めて、Claude Code を「画面を見ていないときも走らせていい」道具に変わります。逆に言えば、どれか一つでも欠けている状態で bypassPermissions を有効にするのは、シートベルトもエアバッグもない状態でレースに出るのと同じです。
Claude Code v2.1 系の Agent View で複数セッションを並列に回す前に、まず settings.json のこの 30 行を埋めてください。書き終えるのに 30 分かかりません。
Sources:


