2026-03-07 追記: 本記事で紹介したHooksベースの安全設計は、現在Permissions + deny rulesベースの構成に移行しています。
「え、mainにpushされてる...誰が...あ、Claude Codeか」
AIエージェントにコードを書かせるの、もう日常ですよね。でも権限まわりの設計をサボると、ある日突然「本番ブランチに直pushされました」事件が起きる。人間ならgitのブランチルール叩き込まれてるけど、AIはそんな空気読めません(読めたら怖い)。
settings.jsonのpermissions.denyに「git push --forceは禁止ね」と書いておけば安心? いや、AIは賢いからgit push -fとかgit push origin HEAD:refs/heads/mainとか、微妙にパターンを変えてすり抜けてくるんですよ。正確には「すり抜けようとしている」わけじゃなくて、単にバリエーションが多すぎてdenyリストの網目から漏れるだけなんですが。結果は同じ。
「denyリスト書いたのに防げなかった」「毎回の承認ダイアログが多すぎて作業が進まない」...こんな経験、ありませんか?
私たちのdotfilesリポジトリでは、この問題に対して3つの防御レイヤーで対処しています。denyリスト(静的ブロック)、Pythonフック(動的検査)、auto-approveパターン(安全な操作の自動承認)。この記事では、実際に運用しているコードと設計意図を全部見せます。
この記事で学べること
- settings.jsonの
permissions.denyを170超のルールに体系化する設計手法 protect-branches.pyの正規表現・refspecパース・gh API連携の仕組み- auto-approveフックで「安全な操作」だけを自動承認するパターン
- PreToolUse / SessionStart / Notification、4種のHookの使い分け
前提知識
- Claude Codeの基本操作を理解している
- settings.jsonのhooks設定について概要を把握している
- gitのrefspec記法(
src:dst)を知っていると理解が早い
全体アーキテクチャ:3層の防御ライン
まず全体像から。Claude Codeのツール実行に対して、3つのレイヤーで安全性を確保しています。
| レイヤー | 仕組み | 役割 |
|---|---|---|
| L1: 静的deny | permissions.deny | パターンマッチで即座にブロック |
| L2: 動的フック | PreToolUse + Python | コマンド内容を解析して判定 |
| L3: 自動承認 | PreToolUseのallow応答 | 安全な操作の承認ダイアログを省略 |
L1で大半の危険操作を止め、L2でL1の隙間を埋め、L3で安全な操作のUXを改善する。**「止めるべきものは確実に止め、通すべきものはスムーズに通す」**がコンセプトです。
L1:170超のdenyルール設計
なぜこんなに多いのか
「git push --forceを禁止」だけで済むと思いますよね。ところがClaude Codeは以下のようなバリエーションでpushしてくることがある。
git push --force origin main # 明示的なforce push
git push -f origin feature # 短縮フラグ
git push origin HEAD:refs/heads/main # refspecで直接指定
git push --set-upstream origin main # upstream設定と同時にpush
git push --force-with-lease origin HEAD:refs/heads/dev # 安全そうに見えて保護ブランチ宛て
git push origin main:main # ブランチ名:ブランチ名形式
git push origin --delete main # ブランチ削除
1つのdenyルールでは1パターンしかブロックできません。全バリエーションをカバーするには、組み合わせ爆発に付き合うしかないんです。
7カテゴリの分類体系
170超のルールを整理するために、以下の7カテゴリに分類しています。
1. Git危険操作(force push・保護ブランチ)
最も重要なカテゴリ。保護ブランチ(main, master, dev, develop, development)への全pushパターンを網羅します。
"Bash(git push --force:*)",
"Bash(git push -f:*)",
"Bash(git push origin HEAD:refs/heads/main)",
"Bash(git push --set-upstream origin main)",
"Bash(git push --force-with-lease origin HEAD:refs/heads/main)",
"Bash(git push origin main:main)",
"Bash(git push origin --delete main)",
保護ブランチ5種 x pushパターン7種 = 35ルール。ここだけでルール数の2割を占めます(几帳面すぎ?いや、事故った時のダメージを考えたら安い)。
2. Git破壊的操作
"Bash(git reset --hard:*)",
"Bash(git clean -f:*)",
"Bash(git clean -fd:*)",
"Bash(git checkout -- .:*)",
"Bash(git restore .:*)",
"Bash(git branch -D:*)",
git reset --hardとgit cleanの組み合わせで未コミットの変更が全部消える。AIが「きれいにしておきますね」と善意でやってくれることがあるので、確実にブロックします。
3. GitHub CLI / API操作
"Bash(gh repo delete:*)",
"Bash(gh pr merge:*)",
"Bash(gh api --method DELETE:*)",
"Bash(gh api -X POST:*)",
"Bash(gh api graphql:*)",
gh CLIの読み取り操作(gh pr view、gh issue list)は許可しつつ、変更・削除系のAPIメソッドはブロック。MCP経由の操作も同様に制御します。
"mcp__gh__merge_pull_request",
"mcp__gh__delete_repo",
"mcp__gh__delete_ref",
"mcp__gh__secret_set",
MCPツール名はmcp__gh__プレフィックスで識別できるので、ツール名そのものをdenyリストに入れます。
4. ファイル破壊・権限変更
"Bash(rm -rf:*)",
"Bash(rm -fr:*)",
"Bash(rm -r -f:*)",
"Bash(/bin/rm -rf:*)",
"Bash(/bin/rm -r:*)",
"Bash(chmod -R:*)",
"Bash(chown -R:*)",
ここで注目してほしいのが/bin/rmのパターン。シェルエイリアスでrmを安全な削除コマンド(ripなど)に置き換えている場合、AIが「エイリアスが効かないのでフルパスで実行します」と/bin/rm -rfを叩いてくることがある。エイリアスのバイパスも想定して塞ぐのがポイントです。
5. ネットワーク・パッケージ管理
"Bash(curl:*)",
"Bash(wget:*)",
"Bash(ssh:*)",
"Bash(nc :*)",
"Bash(brew:*)",
"Bash(pip install:*)",
"Bash(npm publish:*)",
curlやwgetは開発中に使いたい場面もありますが、AIが勝手に外部からスクリプトをダウンロードして実行する可能性がある。確認ダイアログを出すprompt相当にしたいところですが、denyリストは即座にブロックなので、必要な場合はpermissions.allowで個別に穴を開ける運用にしています。
6. システム・OS操作
"Bash(sudo:*)",
"Bash(reboot:*)",
"Bash(shutdown:*)",
"Bash(defaults write:*)",
"Bash(diskutil:*)",
"Bash(launchctl unload:*)",
"Bash(security find-generic-password:*)",
"Bash(security dump-keychain:*)",
macOS固有のコマンドも入っています。defaults writeでシステム設定を変えたり、securityコマンドでキーチェーンにアクセスしたり。開発ツールの設定中に「ちょっと最適化しておきますね」とAIがシステム設定を弄り始める事故は、実際に起きます(体験談)。
7. 機密ファイルの読み取り保護
"Read(./.env)",
"Read(./.env.*)",
"Read(./.ssh/**)",
"Read(./.gnupg/**)",
"Read(./.aws/**)",
"Read(./.config/gh/**)",
denyリストはBash実行だけでなく、Claude CodeのReadツールにも適用できます。.envにAPIキーが入っている場合、AIにそれを読ませるとコンテキストウィンドウに載ってしまう。読ませないこと自体が防御です。
L2:Pythonフックによる動的検査
denyリストはパターンの完全一致なので、どうしても漏れが出ます。そこでPreToolUseフックのPythonスクリプトが動的にコマンドを解析して判定します。
protect-branches.py:3段構えのブランチ保護
最も重要なフックがこれ。80行ほどのPythonで3つの検査を行います。
検査1:gh pr mergeの宛先チェック
PROTECTED = {"main", "master", "dev", "develop", "development"}
if re.match(r"^(?:env\s+\S+=\S+\s+)*gh\s+pr\s+merge\b", norm):
result = subprocess.run(
["gh", "pr", "view", "--json", "baseRefName", "-q", ".baseRefName"],
capture_output=True, text=True, timeout=10,
)
base = result.stdout.strip()
if base in PROTECTED:
deny(f"Blocked: merging into protected branch {base}")
gh pr mergeが実行されようとしたら、そのPRのマージ先ブランチをgh APIで確認する。denyリストでは「どのブランチにマージしようとしているか」までは判定できないので、実際にAPIを叩いて動的に判定しています。
ポイントはenv \S+=\S+のプレフィックスも許容していること。AIがenv GH_TOKEN=xxx gh pr mergeのように環境変数付きで実行するケースにも対応します。
検査2:保護ブランチからのpush検出
result = subprocess.run(
["git", "rev-parse", "--abbrev-ref", "HEAD"],
capture_output=True, text=True, timeout=5,
)
br = result.stdout.strip()
if br in PROTECTED:
deny(f"Blocked: pushing from protected branch {br}")
現在のブランチが保護ブランチなら、pushそのものをブロック。feature branchからのpushは通す。
検査3:refspecの宛先パース
ここが一番複雑で、一番重要な部分です。
parts = norm.split()
# "push"キーワードの位置を特定
i = max(j for j, p in enumerate(parts) if p == "push")
# オプションフラグをスキップしてリモート名を飛ばし、refspecを収集
j = i + 1
while j < len(parts) and parts[j].startswith("-"):
j += 1
j += 1 # skip remote
refspecs = []
while j < len(parts):
if parts[j].startswith("-"):
j += 1
continue
refspecs.append(parts[j])
j += 1
for rs in refspecs:
rs = rs.lstrip("+") # force prefixを除去
if ":" in rs:
_, dst = rs.split(":", 1) # src:dst の dst を取得
else:
dst = rs
if dst.startswith("refs/heads/"):
dst = dst.rsplit("/", 1)[-1] # refs/heads/main → main
if dst in PROTECTED:
deny(f"Blocked: pushing to protected branch {dst}")
refspecはsrc:dst形式で「ローカルのsrcブランチをリモートのdstブランチにpushする」という指定。+プレフィックスはforce pushを意味します。このパーサーは以下のパターンを全部検出します。
git push origin feature:main # feature を main に push
git push origin +feature:main # force push で main に
git push origin HEAD:refs/heads/main # HEADを main に
git push -u origin feature:dev # upstream 設定付きで dev に
denyリストだけではgit push origin feature:mainのようなローカルブランチ名が可変のパターンに対応できません。このPythonパーサーがあることで、denyリストの「網目」を補完しています。
auto-approve-git-safe.py:安全なgit操作の自動承認
denyリストとprotect-branches.pyで危険を止めた上で、今度は安全な操作のUXを改善します。
SAFE_SUB = re.compile(
r"^git\s+(?:add\b.*|commit\b.*|status\b.*|restore\s+--staged\b.*)$"
)
# チェーンされたgitコマンドも全部安全なら自動承認
parts = re.split(r"\s*(?:&&|\|\||;)\s*", norm)
if parts and all(SAFE_SUB.match(p) for p in parts if p):
allow("auto-approve safe git sequence")
git add && git commit -m "..."のようなチェーンコマンドを&&、||、;で分割し、全パートが安全なコマンドなら自動承認。1つでも安全リストに含まれないコマンドが混じっていたら承認しません。
git commitはさらに個別の正規表現でもカバーしています。
if re.search(
r"(^|\s)(env\s+\S+=\S+\s+)*git(\s+-c\s+\S+=\S+)*\s+commit(\s+(-m|-F)\b|\b)",
norm,
):
allow("auto-approve git commit")
env GIT_AUTHOR_NAME=xxx git -c user.email=xxx commit -m "msg"のような環境変数・config付きコミットにも対応。Claude Codeがco-author情報を付けてコミットするケースを想定しています。
auto-approve-gh-mcp.py:MCPツールの読み取り操作を自動承認
DANGEROUS = re.compile(r"(merge|delete|transfer|archive|secret|token|ref|workflow)")
if name.startswith("mcp__gh__") and not DANGEROUS.search(name):
allow("auto-approve safe gh MCP tool")
gh MCP経由のツール呼び出しで、ツール名に破壊的キーワードが含まれていなければ自動承認。mcp__gh__list_issuesやmcp__gh__get_pull_requestは通して、mcp__gh__merge_pull_requestやmcp__gh__delete_refはブロック。シンプルだけど効果的。
Hookイベントの使い分けガイド
settings.jsonで使用可能な4種のHookイベントと、それぞれの適切な使い所を整理します。
PreToolUse:ツール実行前の門番
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [{
"type": "command",
"command": "python3 \"$HOME/.claude/hooks/protect-branches.py\""
}]
}
]
使い所:
- コマンドの内容を検査して許可/拒否を判定
- 安全な操作の自動承認でUXを向上
matcherでツール名を絞り込み、不要な発火を防止
matcherには前方一致で"Bash"や"mcp__gh__"を指定できます。全ツールに発火させたい場合は空文字""を使いますが、パフォーマンスへの影響を考えて絞り込む方がベターです。
SessionStart:セッション開始時のコンテキスト注入
"SessionStart": [
{
"matcher": "compact",
"hooks": [{
"type": "command",
"command": "echo 'Context compacted. Reminder: Read CLAUDE.md for project context. Run git status before making changes.'"
}]
}
]
使い所:
- トークン圧縮後のリマインダー注入
- セッション固有の環境情報の提供
matcher: "compact"でコンテキスト圧縮時のみ発火
長い会話でトークン圧縮(compact)が走ると、冒頭のCLAUDE.mdの内容が薄まることがあります。compactのタイミングで「CLAUDE.md読み直してね」とリマインドすることで、会話が長くなっても品質を維持できます。
Notification:人間への通知
"Notification": [
{
"matcher": "",
"hooks": [{
"type": "command",
"command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'"
}]
}
]
使い所:
- 承認待ちなどでAIが止まった時のデスクトップ通知
- macOSの
osascript、Linuxならnotify-sendを使用 matcher: ""で全通知に対応
Claude Codeを裏で走らせてると、承認ダイアログで止まってることに気づかない。**数分間放置してから「あ、待ってたの?」**となる。Notification Hookでデスクトップ通知を飛ばせば、その無駄な待ち時間がなくなります(地味だけど効果抜群)。
実践Tips:deny設計で陥りがちな罠
罠1:ワイルドカードの:*を忘れる
// NG: 完全一致のみブロック
"Bash(git push --force)",
// OK: 後続の引数も含めてブロック
"Bash(git push --force:*)",
:*をつけないと「git push --force」という完全一致のみブロックし、「git push --force origin main」はすり抜けます。ほぼ全てのdenyルールに:*が必要です。
罠2:bare pushの扱い
// これをdenyに入れると feature branchのpushも全部止まる
"Bash(git push:*)",
// 代わりにbare pushだけを狙い撃ち
"Bash(git push)",
"Bash(git push origin)",
:*なしの完全一致で「引数なしのgit push」だけをブロックし、git push -u origin feature/xxxは通す。ワイルドカードの有無で挙動が大きく変わるので注意。
罠3:allowとdenyの優先順位
"permissions": {
"allow": ["Bash", "mcp__*"],
"deny": ["mcp__gh__merge_pull_request"]
}
Claude Codeではdenyが常にallowより優先されます。allowでmcp__*を全許可しても、denyに入っている特定ツールはブロックされる。この仕様を理解した上で、allowは広めに取ってdenyで穴を塞ぐ設計がお勧めです。
Nixで設定をコード管理する
ここまでの設定を手動で管理するのは現実的じゃない。私たちはNix(home-manager)でsettings.jsonとhookスクリプトをdotfilesリポジトリから自動デプロイしています。
# home-manager activation script(抜粋)
# settings.json へのシンボリックリンク
ln -sf "$DOTFILES_CLAUDE/settings.json" "$CLAUDE_DIR/settings.json"
# hooks ディレクトリ内のスクリプトへのシンボリックリンク
if [ -d "$DOTFILES_CLAUDE/hooks" ]; then
mkdir -p "$CLAUDE_DIR/hooks"
for f in "$DOTFILES_CLAUDE"/hooks/*.py; do
target="$CLAUDE_DIR/hooks/$(basename "$f")"
ln -sf "$f" "$target"
done
fi
nix run .#update一発で、settings.jsonもhookスクリプトも最新の状態にデプロイされる。新しいマシンでも同じセキュリティ設定が再現されます。dotfilesでのNix管理については別記事で詳しく紹介しています。
このアプローチの良いところは、セキュリティルールの変更がGitの履歴で追跡できること。「いつ、誰が、どのルールを追加/変更したか」が全部残ります。170超のルールをメンテナンスするには、この追跡可能性が欠かせません。
まとめ
Claude Code Hooksの安全設計は、「止めるべきものを確実に止め、通すべきものをスムーズに通す」の一言に尽きます。
- L1の静的denyで170超のパターンを即座にブロック
- L2のPythonフックでrefspecパースやAPI連携による動的検査
- L3の自動承認で安全な操作のUXを改善
どれか1つでは穴がある。3つを組み合わせて初めて「AIに任せても安心」な環境ができます。settings.jsonは1回書いたら終わりじゃなくて、新しいパターンを発見するたびに育てていくもの。170ルールは多く見えますが、それぞれに「こういう事故があったから追加した」という理由がある。AIエージェント時代の安全設計は、地道なパターン潰しの積み重ねです。
なお、本記事で紹介したHooks + Pythonスクリプトによる安全設計は、その後Permissions + deny rulesベースの構成に移行しました。
あわせて読みたい
- 【2026年版】AIコーディングツール完全比較 — Claude Code・Codex・Antigravityの選び方
- 【Claude Code Hooks】テスト自動化で品質を仕組み化 — テスト忘れゼロ件を実現する設定ガイド
AIコーディングツール活用 完全ガイド — この記事を含む11本の記事で、AIコーディングツールの比較・導入から実践活用までを体系的に解説しています。



