s01 → s02 → s03 → s04 → s05 → ... → s20
"ツール実行前に権限を判断" — 権限パイプラインは、どの操作に承認が必要かを決める。
Harness レイヤー: 権限 — ツール実行前に一つのゲートを追加。
s02 の Agent は 5 つのツールを持つ。file tools は safe_path で保護されるが、bash は制限なし。「プロジェクトを掃除して」と頼むと、rm -rf / を実行しかねない。
安全性はモデルを信頼することではなく、コードに頼る — ツール実行前に判断を挟む。
s02 のループは完全に維持される。唯一の変更は、ツール実行前に check_permission() を挿入すること — 各ツール呼び出しは 3 つのゲートを固定順序で通過する:ハード拒否が最優先、次にソフト確認、どちらも一致しなければ許可。
3 つのゲートは 3 つの決定に対応する:
| ゲート | 役割 | 一致時 |
|---|---|---|
| 1. 拒否リスト | 常に禁止される操作(rm -rf /、sudo) |
即座に拒否、実行しない |
| 2. ルールマッチング | コンテキスト依存の操作(作業ディレクトリ外への書き込み、rm ファイル) |
ゲート 3 へ |
| 3. ユーザー承認 | ゲート 2 が一致した場合、ユーザー確認を待機 | ユーザーが許可または拒否を決定 |
3 つのゲートのどれにも一致しない → 直接実行。日常の操作の大部分はこの経路を通る。
ゲート 1:ハード拒否リスト。最初に確認し、一致すればブロックメッセージを返す。(教育デモ:単純な文字列マッチングは信頼できるセキュリティ機構ではない — コマンドの変種やシェル展開で回避される可能性がある。CC のアプローチは付録を参照。)
DENY_LIST = [
"rm -rf /", "sudo", "shutdown", "reboot",
"mkfs", "dd if=", "> /dev/sda",
]
def check_deny_list(command: str) -> str | None:
for pattern in DENY_LIST:
if pattern in command:
return f"Blocked: '{pattern}' is on the deny list"
return Noneゲート 2:ルールマッチング — 「いつユーザーに聞くべきか」を記述する。各ルールはツールとチェック条件を指定する。
PERMISSION_RULES = [
{
"tools": ["write_file", "edit_file"],
"check": lambda args: not (WORKDIR / args.get("path", "")).resolve().is_relative_to(WORKDIR),
"message": "Writing outside workspace",
},
{
"tools": ["bash"],
"check": lambda args: any(kw in args.get("command", "") for kw in ["rm ", "> /etc/", "chmod 777"]),
"message": "Potentially destructive command",
},
]
def check_rules(tool_name: str, args: dict) -> str | None:
for rule in PERMISSION_RULES:
if tool_name in rule["tools"] and rule["check"](args):
return rule["message"]
return Noneゲート 3:ルールが一致した後、ユーザー入力を待機。
def ask_user(tool_name: str, args: dict, reason: str) -> str:
print(f"\n⚠ {reason}")
print(f" Tool: {tool_name}({args})")
choice = input(" Allow? [y/N] ").strip().lower()
return "allow" if choice in ("y", "yes") else "deny"3 つのゲートを直列に接続、ツール実行前に挿入する:
def check_permission(block) -> bool:
# ゲート 1: ハード拒否
if block.name == "bash":
reason = check_deny_list(block.input.get("command", ""))
if reason:
print(f"\n⛔ {reason}")
return False
# ゲート 2 + 3: ルールマッチング → ユーザー承認
reason = check_rules(block.name, block.input)
if reason:
decision = ask_user(block.name, block.input, reason)
if decision == "deny":
return False
return True
# agent_loop で — s02 のループに 1 行追加するだけ:
for block in response.content:
if block.type == "tool_use":
if not check_permission(block): # ← 新規
results.append({... "content": "Permission denied."})
continue
output = TOOL_HANDLERS[block.name](**block.input) # s02 既存
results.append(...)| コンポーネント | 変更前 (s02) | 変更後 (s03) |
|---|---|---|
| セキュリティモデル | なし(モデルを信頼) | 3 ゲート権限パイプライン |
| 新規関数 | — | check_deny_list, check_rules, ask_user, check_permission |
| ループ | すべてのツールを直接実行 | 実行前に check_permission() を挿入 |
cd learn-claude-code
python s03_permission/code.py以下のプロンプトを試してみよう:
Create a file called test.txt in the current directory(そのまま通過するはず)Delete all temporary files in /tmp(bash + rm でゲート 2 が発動)What files are in the current directory?(読み取り専用、すべて通過)Try to write a file to /etc/something(作業ディレクトリ外への書き込みでゲート 2 が発動)
観察のポイント:どの操作がそのまま通過するか? どれに確認が必要か? どれが即座に拒否されるか?
権限チェックは実装された — しかし、毎回ループ内に check_permission() をハードコードしている。ツール実行の前後にログを追加したい場合は? 特定の操作後に自動的に git commit をトリガーしたい場合は? このような拡張ロジックがループ内に散らばると、ループはすぐに膨張する。
→ s04 Hooks:ループにフックを追加する。拡張ロジックはフックにぶら下げ、ループはクリーンに保つ。
CC ソースコードを深掘り
以下は CC ソースコード
types/permissions.ts、utils/permissions/permissions.ts、toolExecution.ts、utils/permissions/yoloClassifier.ts、tools/AgentTool/forkSubagent.tsの検証に基づく。
教育版の 3 つのゲート(deny → ask → allow)は CC と完全には対応しない。CC の PermissionResult には 4 つの behavior がある(types/permissions.ts:241-266):
| behavior | 意味 | 教育版の対応 |
|---|---|---|
allow |
直接許可 | ゲート 3 通過 |
deny |
直接拒否 | ゲート 1 一致 |
ask |
ユーザーにダイアログを表示 | ゲート 2 一致 |
passthrough |
ツールが意見を表明せず、汎用パイプラインに委ねる | 教育版にはなし |
CC のツール呼び出しは 3 つのゲートを通るのではなく、checkPermissionsAndCallTool()(toolExecution.ts:599-1745)、hooks、hasPermissionsToUseToolInner()(utils/permissions/permissions.ts:1158-1310)、classifier ロジックに分散する複数の段階を経る:
- Zod schema 検証(
toolExecution.ts:614-680)— パラメータの型チェック - validateInput()(
toolExecution.ts:682-733)— ツールレベルの意味的検証 - backfillObservableInput()(
toolExecution.ts:784)— レガシーフィールドの補完 - PreToolUse hooks(
toolExecution.ts:800-862)— フックが allow/deny/ask を返す - resolveHookPermissionDecision()(
toolExecution.ts:921-931)— フック + パイプラインの決定を調整 - hasPermissionsToUseToolInner()(
permissions.ts:1158-1310)— 多層ルールチェック:- ツール全体が deny rule で無効 →
deny - ツール全体が ask rule でマーク →
ask tool.checkPermissions()ツール自身の判断- ツール自身が deny を返す →
deny requiresUserInteraction()→ask- コンテンツ関連の ask ルール →
ask(バイパス不可) - セキュリティチェック違反 →
ask(バイパス不可) - bypassPermissions モード →
allow - ツール全体が allow rule で許可 →
allow - passthrough →
askに変換
- ツール全体が deny rule で無効 →
CC には単一の deny list はない。権限ルールは 8 つのソースから来る(types/permissions.ts:54-62):
| ソース | 設定場所 |
|---|---|
userSettings |
~/.claude/settings.json |
projectSettings |
.claude/settings.json |
localSettings |
settings.local.json |
flagSettings |
フィーチャーフラグ |
policySettings |
企業管理ポリシー |
cliArg |
--allowedTools / --deniedTools |
command |
インラインコマンド |
session |
セッション内一時承認 |
各ルールの形式:{ toolName: "Bash", ruleBehavior: "deny", ruleContent: "npm publish:*" }。複数ソースのルールは統合され、高優先度ソースが低優先度を上書きする(低→高:user < project < local < flag < policy、さらに cliArg、command、session)。
CC では isDestructive(Tool.ts:405-406)は純粋に UI 表示用 — ツール一覧に [destructive] ラベルを表示するだけ。権限決定には参加しない。デフォルトではすべてのツールが false を返す。ExitWorktree(remove 時)と MCP ツール(annotations.destructiveHint に依存)のみがオーバーライドする。
CC の auto モードでは、毎回ダイアログを表示するわけではない。classifyYoloAction(utils/permissions/yoloClassifier.ts:1012)はツール呼び出し + 会話コンテキストを分類器 LLM に送って安全性を判断する。まず acceptEdits モードのシミュレーションを試み(permissions.ts:620-656、acceptEdits が許可すれば → 自動承認)、次にセーフツールホワイトリストを確認し(permissions.ts:658-686)、最後に分類器を呼び出す。分類器が連続して拒否しすぎた場合 → 手動承認にフォールバック。
サブ Agent(AgentTool 経由でフォークされたもの)の permissionMode は 'bubble' に設定される(forkSubagent.ts:50)。これは権限ダイアログが親 Agent のターミナルにバブルアップすることを意味する。サブ Agent で黙って拒否されるのではない。Bash 分類器はこの過程で引き続き実行され — 権限ダイアログを表示しつつ、バックグラウンドで自動承認可能か判断する。
- 多段階パイプライン → 3 ゲート:理解のハードルが大幅に下がる
- 8 ルールソース → 1 つのローカル DENY_LIST:概念量を制御可能
- isDestructive → 省略(教育版には UI レイヤーがなく、CC でも権限決定には参加しない)
- YoloClassifier → 省略(追加の LLM 呼び出しとテレメトリに依存)
- 権限バブリング → 省略(s15 でマルチ Agent を扱う)