首页
/ learn-claude-code s03 実践:ツール実行前に「3ゲートの権限パイプライン」で Agent の安全性を実現する

learn-claude-code s03 実践:ツール実行前に「3ゲートの権限パイプライン」で Agent の安全性を実現する

2026-09-04 13:09:23作者:曹令琨Iris

本文は learn-claude-code の第 3 章(s03: Permission)の実装解説であり、Agent がツールを実行する前に「どの操作を禁止し、どの操作にユーザー承認を求め、どの操作をそのまま通すか」をコードが判断する仕組みを扱う。読むことで、硬拒否リスト(deny list)、ルールマッチング、ユーザー承認という 3 ゲートから成るパーミッションパイプラインの設計思想と、その agent loop への 1 行埋め込みの実装パターンを、実装ソース まで対応させながら理解できる。

背景:s02 のツールディスパッチに残っていた穴

s02 では Agent が 5 つのツール(bash / read_file / write_file / edit_file / glob)を持ち、TOOL_HANDLERS ディスパッチ表で一元管理されるようになった。ファイル系ツールは safe_path() によりワークスペース外へのアクセスが弾かれる(s02 の実装 を参照)。

しかし bash は制限なしだ。「プロジェクトを掃除して」と頼めば、モデルが rm -rf / を発行しかねない。s02 では run_bash 内に ["rm -rf /", "sudo", ...] といった簡易チェックが埋め込まれていたが、これはツール実装内に安全ロジックが混入した状态で、拡張性にも説明性にも欠ける。

s03 が打ち出す方針は明確だ:安全性はモデルを信頼することではなく、コードに頼る。判断はツール実行「前」に、ループとツール実装の両方から分離された独立したゲートとして挟まれる。

解決策:チェックはループに 1 行、ツールは不変

s03 Permission 全体像:ユーザー → LLM → 権限ゲート → ツールディスパッチの流れ

s02 のループ構造(LLM 呼び出し → stop_reason 判定 → ツール実行 → tool_result 追加)は完全に維持される。唯一の変更は、ツール実行の直前に check_permission() を挿入することである。各ツール呼び出しは固定順序で 3 つのゲートを通過する:ハード拒否が最優先 → ルールマッチング → 該当すればユーザー承認。どのゲートにも命中しなければ、そのまま実行される(日常の操作の大半はこの経路を通る)。

ゲート 役割 一致時
1. 拒否リスト 常に禁止される操作(rm -rf /sudo 即座に拒否、実行しない
2. ルールマッチング コンテキスト依存の操作(作業ディレクトリ外への読み書き、rm 文件) ゲート 3 へ
3. ユーザー承認 ゲート 2 が一致した場合、ユーザー確認を待機 ユーザーが許可または拒否を決定

この「3 段の判断を分離する」構成には設計上の狙いがある。Web 教材の設計判断メモ にある通り、ハード拒否・ルール一致・ユーザー確認を分けることで、「そのコマンドは絶対に禁止だったのか、リスクがあるのか、単に確認待ちなのか」を区別でき、ポリシーが説明可能になる。一方、単一の allow/deny 関数にすると短くなる代わりに、コマンドが止まった理由が隠れてしまう。

ゲート 1:ハード拒否リスト(DENY LIST)

最初に確認されるのが硬拒否表で、一致すればブロックメッセージを返し、以降のゲートには進まない。

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

注意すべきは、このリストが単純な文字列部分一致pattern in command)である点だ。原文書も明記しているが、これは「権限ゲートをループのどこに置くか」を示すための最小実装であり、完全なセキュリティ境界ではない。実際の 実装 でも同一の 7 パターンがそのまま使われており、"sudo" のように部分一致で弾く粒度である(したがって sudo を含む無害なコマンドもブロックされる可能性がある、という許容される粗さがある)。本番の完全な防御としては、シェルパーサによる AST 級マッチングや sandbox 執行が必要になる、という理解で読み替えるべきだ。

ゲート 2:ルールマッチング(PERMISSION_RULES)

2 番目のゲートは「いつユーザーに聞くべきか」を記述する。各ルールは対象ツール(tools)とチェック条件(check lambda)を指定し、条件を満たしたツール呼び出しに対しては理由メッセージを返す。

PERMISSION_RULES = [
    {
        "tools": ["read_file", "write_file", "edit_file"],
        "check": lambda args: not (WORKDIR / args.get("path", "")).resolve().is_relative_to(WORKDIR),
        "message": "Access 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":
            return rule["message"]
    return None

2 つのルールの要点:

  1. ワークスペース外アクセスルール:ファイル系ツールの pathWORKDIR / path で結合してから .resolve() し、結果が WORKDIR の相対パスに収まるか(is_relative_to(WORKDIR))を判定する。../ やシンボリックリンクによるエスケープも resolve() により正規化されるため、/etc/something への書き込みや ../../etc/hosts への参照がここで捕获される。
  2. 破壊的 bash コマンドルール:コマンド文字列に "rm "(末尾の空白を含む)、"> /etc/""chmod 777" のいずれかが含まれれば「Potentially destructive command」としてユーザー確認へ回す。

check_rules は「どのツールがどのルールに該当するか」をループ内で逐次照合する。ツールごとにチェックを書き散らすのではなく、ルール表にデータとして記述している点が、後述する拡張性の鍵になる。

ゲート 3:ユーザー承認(ask_user)

ルールが一致した呼び出しはここで停止し、ユーザー入力(y / yes のみ許可)を待機する。

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"

設計上の重要な既定動作は default deny である:strip().lower() した入力が ("y", "yes") でなければ deny を返す。つまり Enter のみで無应答にすると拒否され、曖昧な入力は安全側へ倒す。この関数は同期の input() でループを停止させるため、「承認前にツールが実行されることはない」ことを実行順で保証している。

3 ゲートを直列に接続する:check_permission とループへの挿入

3 ゲートのパーミッションパイプライン:deny list → rules → approval の判定フロー

3 つのゲートを 1 関数に束ねる:

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

そして s02 の agent loop に1 行だけ足す:

for block in response.content:
    if block.type == "tool_use":
        if not check_permission(block):           # ← 新規
            results.append({... "content": "Permission denied."})
            continue
        output = TOOL_HANDLERSblock.name  # s02 既存
        results.append(...)

ここには 1 つ、見落とされやすいが重要な設計がある。ブロックされた呼び出しも必ず tool_result を返すことだ。実装 では、check_permissionFalse を返すと {"type": "tool_result", "tool_use_id": block.id, "content": "Permission denied."} が messages に追加され、continue によって次へ進む。Anthropic API のツール呼び出しプロトコル上、発行された tool_use には対応する tool_result が必須であり、黙ってスキップすると会話状態が壊れる。また、モデル自身に「なぜ実行されなかったか」をフィードバックすることで、同じ危険リクエストを繰り返さず別の手段を選ぶことができる。設計判断メモ の 3 項目("Blocked Calls Still Produce Loop State")がまさにこの点を指しており、静かに skip するとモデルが同じ不安全リクエストを繰り返しうる、と説明されている。

ソースコードによる実装確認:ドキュメント例との差分と補足

上記コード例は s03_permission/README.ja.md の要約版であり、実際の s03_permission/code.py を読むと、さらにいくつかの実装事実が確認できる。

1. セキュリティチェックがツール実装から抜けている。s02 の run_bash は関数内に dangerous リストを埋め込んでいたが、s03 の run_bash にはそのチェックが残っていない(subprocess.run の 120 秒タイムアウトと 50000 文字への出力截断のみ)。同様にファイル系ツールも s02 の safe_path() を使わず、run_read / run_write / run_edit(WORKDIR / path).resolve() を直接利用する。つまり s03 では「パス境界・破壊的操作の判定」がツール実装から完全に外され、ゲート 1/2 が唯一の判定箇所になった。ポリシーは 1 か所で管理できるが、これはゲートを通さずにツールを直接呼ぶ実装は安全を失う、という構造上の依存を意味する。

2. SYSTEM プロンプトの修正。s03 の システムプロンプト は s02 の "Act, don't explain" から、"You are a coding agent at {WORKDIR}. All destructive operations require user approval." に変わっている。つまり「安全はコードが保証する」一方で、モデル側に「破壊的操作は承認フローがある」という事実も伝え、モデルが承認を前提に計画を立てやすくしている。

3. 実装でのメッセージ文言。実際の PERMISSION_RULES の 1 件目の message"Writing outside workspace"source)、ask_user / check_permission の絵文字ではなく ANSI カラーコード(\033[33m 黄 / \033[31m 赤)で [permission] / [blocked] を表示する。ターミナル上の視認性への配慮である。

4. ループの残りは s02 と同一agent_loopmax_tokens=8000 でメッセージを送り、stop_reason != "tool_use" なら終了、それ以外なら response.content の各 tool_use block を原本の順序で 1 つずつ「権限チェック → TOOL_HANDLERS.get(block.name) でハンドラ取得 → 実行 → tool_result 収集」し、最後に一括して user メッセージとして追加する。s02 で確立した「1 ツール追加 = TOOLS 1 エントリ + HANDLERS 1 行」の構造は一切変更されていない。

s02 との変更点まとめ

コンポーネント 変更前 (s02) 変更後 (s03)
セキュリティモデル 一部ツール内チェックのみ(safe_path、bash 内蔵リスト。モデルを信頼する部分大) 3 ゲート権限パイプラインが唯一の判定層
新規関数 check_deny_list, check_rules, ask_user, check_permission
ループ すべてのツールを直接実行 実行前に check_permission() を挿入
拒否された呼び出し 該当なし tool_result: "Permission denied." でループ状態を維持

試してみる:実行環境と検証プロンプト

requirements.txt によれば必要な依存は anthropic>=0.25.0python-dotenv>=1.0.0 のみ。.envANTHROPIC_API_KEYMODEL_ID(任意で ANTHROPIC_BASE_URL を設定すれば、AUTH_TOKEN が自動的に pop されプロキシベースの接続に切り替わる、という 初期化処理 がある)を用意する:

pip install anthropic python-dotenv
cd learn-claude-code
python s03_permission/code.py

起動後は s03 >> プロンプトで対話し、q で終了する。以下の 4 プロンプトで 3 パス(自動通過 / 確認 / 即拒否)を観察するのが推奨される:

  1. Create a file called test.txt in the current directory — ワークスペース内の書き込みでルールに命中しないはず → そのまま通過
  2. Delete the file test.txt — bash + rm がルール 2 に命中 → ゲート 2 が発動し y/N 確認
  3. What files are in the current directory? — 読み取り系のみ → すべて通過
  4. Try to write a file to /etc/something — ワークスペース外への書き込み → ゲート 2 が発動し y/N 確認

観察の焦点は:どの操作がそのまま通過したか、どこで確認が求められたか、そして(意図的に sudo ... を試すと)どこが即座に拒否されたか、の 3 分類を自分の目で確認することである。

動作シナリオの追跡

Web 教材の s03 シナリオ定義 が、このパイプラインの完全な 1 サイクルを可視化している。ユーザーが "Delete the temporary build directory." と要求し、モデルが bash: rm -rf /tmp/build-cache を発行すると:

  1. ゲート 1(deny list)→ 該当なし、通過;
  2. ゲート 2(rules)→ "rm " に一致、「destructive command → ask user」;
  3. ゲート 3 → 実行前に停止しユーザー承認を待つ。承認されると通常通り handler に渡り、tool_result: (no output) が返る。

このシナリオは「モデルはアクションを要求できるが、実行を許可するのは harness である」という s03 の核心を端的に示している。

次のステップ:s04 Hooks への移行

s03 の check_permission() はループ内にハードコードされている。これ以上「bash の各呼び出しをログにしたい」「write 後に git add を自動実行したい」のような拡張を足し始めると、ループは直ぐに膨張し illegible になる。learn-claude-code の次の章である s04 Hooks では、ツール実行の前後にフックを吊るす機構を導入し、拡張ロジックをループの外へ出すことで、ループ自体を安定したコアとして保つ設計へ移行する。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
504
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384