先に結論だけ
Claude Codeのdenyルールだけでコマンド実行を制御しようとすると、厳密化するほどルールが肥大化して管理不能になり、緩めるとガバナンスが崩れるジレンマに陥ります。この記事では、危険なコマンドをdenyしつつラッパースクリプトで安全な代替を提供する「ラッパースクリプトパターン」を、実装例とともに解説します。deny + wrapper script + edit保護 + skillの4要素で、エージェントの自律性とセキュリティのバランスを取る設計ができるようになります。
この記事はClaude Code CLI v2.1.92(2026年4月時点)を前提としています。settings.jsonのdeny/allowルールの基本的な設定経験がある読者を想定しています。
はじめに
Claude Codeのsettings.jsonには、エージェントが実行できるBashコマンドを制御するdeny/allowルールがあります。
{
"permissions": {
"deny": ["Bash(rm *)"],
"allow": ["Bash(git status)"]
}
}
この記事では、denyルールの限界を補うパターンとして「ラッパースクリプト」を紹介します。元のコマンドをdenyでブロックしつつ、安全な代替コマンドをスクリプトとして提供するアプローチです。
deny rulesの限界
「特定オプションだけ禁止」がむずかしい
git pushは許可したいがgit push --forceは禁止したい、というケースを考えます。
{
"permissions": {
"deny": [
"Bash(git push --force*)",
"Bash(git push -f*)"
]
}
}
このようにdenyパターンを書くことはできますが、同じ意味を持つ別の書き方をすべて網羅するのは困難です。
git push --force-with-lease-
git push origin main --force(オプションの位置違い) -
git push --delete(別の危険なオプション)
オプションの位置やエイリアスのバリエーションを網羅しようとすると、denyルールが際限なく膨らみ管理が破綻します。
パターンマッチの脆弱性
denyルールは、Bashツールに渡されるコマンド文字列に対するパターンマッチで動作します。公式ドキュメントでも、このパターンマッチについて以下のように警告されています。
Bash permission patterns that try to constrain command arguments are fragile.
(Bashの権限パターンでコマンド引数を制約しようとするのは脆弱です)
実際に、CVE-2026-25724としてシンボリックリンク経由でファイルアクセス制御のdenyルールがバイパスされた事例もあります。この脆弱性はReadツールに対するものですが、パターンマッチベースの制御が持つ構造的な限界を示しています。denyルールは「意図的な攻撃」を防ぐためのものではなく、エージェントの「うっかりミス」を防ぐガードレールとして位置づけるのが適切です。
ラッパースクリプトパターン
動作原理:スクリプト内部はdenyルールの対象にならない
Claude Codeの権限チェックは、Bashツールに渡されるコマンド文字列に対して実行されます。つまり、エージェントがmy-script.shを実行する場合、チェックされるのはmy-script.shという文字列だけです。
スクリプト内部でrmやgit push --forceが実行されていても、それらは権限チェックの対象外です。この特性を利用して、危険なコマンドの「安全なバージョン」をスクリプトとして実装できます。
設計:deny + wrapper script + rule + skill
ラッパースクリプトパターンは4つの要素で構成されます。
- deny: 元の危険なコマンドをsettings.jsonでブロック
- wrapper script: 安全な代替をシェルスクリプトとして実装
- deny (Edit): ラッパースクリプト自体の編集をdenyでブロック
- rule + skill: ルールでラッパーの存在を常時認識させ、スキルで詳細な使い方を教える
4つの要素はそれぞれ単独では不十分です。
- denyだけでは代替手段がない
- スクリプトだけではdenyバイパスのリスクが残る
- 編集保護がなければエージェントが安全チェックを書き換えられる
- ルールとスキルがなければエージェントはラッパーの存在や使い方を認識できない
4つを組み合わせることで、安全かつ自律的な運用が実現します。
必須要件:ラッパースクリプトの編集禁止
ラッパースクリプトの安全性は、スクリプトの内容が信頼できることが前提です。エージェントがスクリプトを書き換えられる場合、安全チェックを無効化できてしまいます。
settings.jsonのdenyルールで、スクリプトファイル自体の編集を禁止します。ラッパースクリプトをシンボリックリンクで配置している場合、リンク先のオリジナルファイルも編集禁止にする必要があります。
{
"permissions": {
"deny": [
"Edit(~/.local/bin/*)",
"Edit(~/.claude/skills/trash-item/scripts/*)"
]
}
}
この例では、~/.local/bin/trash-itemは~/.claude/skills/trash-to/scripts/配下のスクリプトへのシンボリックリンクです。リンク元(~/.local/bin/)とリンク先(スキルのスクリプトディレクトリ)の両方をdenyしないと、エージェントがリンク先を直接編集することでガバナンスが崩れます。
実装例:trash-item(rmの代替)
rmコマンドは、ファイルを不可逆的に削除します。エージェントの操作ミスでファイルが消えた場合、復元は困難です。代替として、macOSのゴミ箱へファイルを移動するtrash-itemスクリプトを実装します。
#!/bin/bash
# trash-item: rm の安全な代替(macOS専用)
# ファイルを削除ではなくTrashへ移動する
if [ $# -eq 0 ]; then
echo "Usage: trash-item <path> [<path>...]" >&2
exit 1
fi
for item in "$@"; do
if [ ! -e "$item" ]; then
echo "Error: '$item' not found" >&2
exit 1
fi
osascript -e "tell application \"Finder\" to delete (POSIX file \"$(realpath "$item")\")" \
> /dev/null
echo "Trashed: $item"
done
このスクリプトをPATHの通ったディレクトリ(例: ~/.local/bin/)に配置し、実行権限を付与します。
この実装例はmacOSのFinderとAppleScriptに依存しています。ファイルパスにダブルクォートが含まれる場合はAppleScriptが壊れるため、本番運用ではパスのサニタイズが必要です。Linux環境ではgio trashやtrash-cliなど、OS固有のゴミ箱機能に読み替えてください。重要なのは「rmをdenyしてラッパーで代替する」というパターンであり、スクリプトの中身は環境に合わせて実装してください。
settings.jsonの設定は以下のようになります。
{
"permissions": {
"deny": [
"Bash(rm *)",
"Bash(rmdir *)",
"Edit(~/.local/bin/trash-item*)"
],
"allow": [
"Bash(trash-item *)"
]
}
}
最後に、skillのルールファイル(例: ~/.claude/rules/rm-alternative.md)でエージェントに指示します。
# rm / rmdir Command Policy
`rm` and `rmdir` are denied. Use `trash-item <path>` instead.
これでエージェントはrmを実行しようとするとdenyでブロックされ、ルールにしたがってtrash-itemを使用します。ファイルはゴミ箱に移動されるため、Finderの「元に戻す」で復元できます。
応用パターン:特定オプションのブロック
trash-itemは「コマンドの置き換え」でしたが、次のステップとして「コマンド内オプションのフィルター」があります。コマンド自体は許可しつつ、特定の危険なフラグだけを拒否するパターンです。
例としてgit pushのラッパーを考えます。以下は、危険なフラグを検出して拒否する擬似コードです。
#!/bin/bash
# safe-git-push: git push の安全なラッパー(擬似コード)
for arg in "$@"; do
case "$arg" in
--force|--force-with-lease|-f|--delete|--mirror)
echo "Error: '$arg' is blocked by safe-git-push" >&2
exit 1
;;
esac
done
# 危険なフラグなし → そのまま実行
git push "$@"
denyルールのパターンマッチではgit push origin main --forceのような引数の位置違いを網羅するのが困難です。スクリプト内のループですべての引数をチェックすれば、位置に関係なく危険なフラグを検出できます。
公式機能との比較
Claude Codeにはコマンド制御に関連する公式機能がいくつかあります。ラッパースクリプトパターンとの違いを整理します。
PreToolUse Hooks
PreToolUse Hooksは、ツール実行前にシェルスクリプトを呼び出す公式機能です。exit code 0で許可、exit code 2でブロックされます。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [{
"type": "command",
"command": "bash check-command.sh"
}]
}
]
}
}
Bashツールだけでなく、EditやReadなどすべてのツールに対して適用でき、ツールに渡される入力をJSON(stdin)で受け取れるため、denyルールのパターンマッチでは難しい複雑な条件判定が可能です。
ラッパースクリプトとの違いは、Hooksが「ブロックするだけ」であるのに対し、ラッパースクリプトは「エージェントの次の行動を誘導できる」点です。Hooksでコマンドをブロックした場合、エージェントは別の方法を自力で探す必要があります。ラッパースクリプトでは、denyされた複数コマンドの意図を安全な操作として1つのスクリプトにまとめられ、不適切な使い方にはエラーメッセージで正しい方法へ誘導できます。
CLAUDE_CODE_SHELL_PREFIX
CLAUDE_CODE_SHELL_PREFIXは、すべてのBashコマンドの前にプレフィックスを付加する環境変数です。コマンドの実行ログを記録するなど、包括的な監視や前処理に向いています。
ラッパースクリプトが特定の操作に対するピンポイントの制御であるのに対し、CLAUDE_CODE_SHELL_PREFIXはすべてのコマンドに一律の処理を挟む仕組みです。コマンドのフィルタリングや代替コマンドへの差し替えといった、この記事で扱うユースケースとは用途が異なります。
Skills allowed-tools
Skillsのallowed-toolsフロントマターは、スキル実行中にプロンプトなしで使用可能なツールを設定します。
---
allowed-tools: ["Read", "Grep", "Glob"]
---
この例ではスキル内でBashツールの実行に許可が必要になります。allowed-toolsにBashを含めると許可確認なしで実行されます。Bash(safe-cmd *)のようなワイルドカード指定で特定コマンドだけを許可する機能は、現時点では期待どおりに動作しません(Issue #14956)。
ラッパースクリプトがBashコマンドの中身を制御するのに対し、allowed-toolsはツール自体の使用可否を制御します。レイヤーが異なるため、組み合わせて使用できます。
比較表
| 機能 | 制御対象 | 制御方式 | 行動誘導 | 適用範囲 |
|---|---|---|---|---|
| deny rules | Bashコマンド | パターンマッチ | ❌ | 全セッション |
| ラッパースクリプト | 特定の操作 | スクリプトロジック | ✅ | allow設定に依存 |
| PreToolUse Hooks | 全ツール | exit codeで判定 | ❌ | 全セッション |
| CLAUDE_CODE_SHELL_PREFIX | 全Bashコマンド | プレフィックス付加 | ❌ | 全セッション |
| Skills allowed-tools | ツール種別 | ホワイトリスト | ❌ | スキル内のみ |
ラッパースクリプトパターンの特徴は「行動誘導」の列に表れています。他の機能はブロックや制限が主な役割であるのに対し、ラッパースクリプトはエージェントの行動を安全な方向へ誘導できます。エージェントの自走を止めずに安全性を確保できるのは、この誘導があるからです。
なお、PreToolUse Hooksとラッパースクリプトは排他的ではありません。Hooksで広範な監視を行いつつ、特定のコマンドにはラッパースクリプトで代替を提供する、という組み合わせも有効です。
まとめ
denyルールだけでエージェントのコマンド実行を制御しようとすると、厳密化と自律性のジレンマに陥ります。ラッパースクリプトパターンは、このジレンマに対する1つの解を提示します。
- 危険なコマンドを包括的にdenyでブロック
- ラッパースクリプトで安全な代替を提供
- skillでエージェントに使い方を教える
- allowへのラッパーコマンド追加で自律性を高める
スクリプト内部のコマンド実行は制御対象外という特性を活かし、エージェントの自律性を保ちながら安全な運用を実現できます。