第三章-permission
写在前面
第三章,shareAI讲述了给模型加权限的过程。完整代码见
https://github.com/shareAI-lab/learn-claude-code/blob/main/s03_permission/code.py
我们的任务是:
1,了解check_deny_list, check_rules, ask_user三道闸门组成的permission
2,滤清楚权限检查的逻辑
还记得第二章末尾提到,在run_bash中我们其实已经做过了一些权限检查,不过这只是象征性的,那样的硬检查对一些别有用心的shell语句几乎防不住,本章将一步步实现三层防护门,感受Claude code权限管理的做法。
话不多少,立即开始吧
一、分析逻辑
我先直接告诉大家,原项目的三道门分别是check_deny_list(拒绝列表,也是上面提到的硬检查), check_rules(规则匹配), ask_user(用户审批)。作者给出的逻辑原图是:

其实这与代码逻辑是有一些差别的,我们直接看check_permission,这个函数完成了三个门打包,实现了管理逻辑。

仔细观察代码逻辑是这样的。
bash
函数开始 → 判断是不是bash工具
├─ 是bash:先跑黑名单拦截
│ └─ 命中黑名单 → 红色打印禁止理由,无人工确认,直接阻断
└─ 不管是不是bash,都进入通用权限规则校验(第二道门)
├─ 命中风险规则 → 弹出窗口让你手动确认
│ ├─ 你选deny → return False,拒绝执行
│ └─ 你选allow → 放行,走到最后return True
└─ 没命中任何规则 → 直接return True,放行
在以前学习中我们知道,block.name的值是我们在TOOLS中定义的,可以分为两类,一类是tools_use,一类是bash。
刚开始进入的时候,其实只有当block.name=="bash"的时候,才会进入第一道门。而无论是不是bash,都会进入到第二道门,如果存在reason,说明模型准备执行的操作有超越权限,才会交给第三道门,让用户去判断是否允许这种行为。
所以,并非所有的ToolUseBlock都会进入第一道门,这取决于name的类型。而这样的做法,其实与刚开始直接将硬检查塞到run_bash中逻辑上一等价的。
接下来,我们分别来实现三个门。
第一道门,就是原来的硬检查。创建一个危险命令的列表,然后将传入的通配符依次放进去检查,如果在里面,就立马返回blocked提示。我觉得,虽然说是第一道,这其实是最后一道。因为模型所有的操作都是基于bash的,在这之前要先生成shell,而shell的生成往往是在tools_use阶段,在这些阶段中会进行第二,第三道门的检查,所以第一道,也是最底层的权限检查,按照时间则是最后一道关卡。
python
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
第二道门,是对模型使用的这些工具做一个允许规则。前面我们写了说明书,分发表,现在相当于在制作一个操作规范。依旧是一个列表,每个元素为一个字典,字典存放tools是要约束的工具,check是一个布尔值,记录lambda表达式的输出结果。message其实写什么都可以,只是为了传达信息,这里记录的是当check等于True(等于True代表lambda表达式未通过,我们是希望它通过的)要报的错误。
python
PERMISSION_RULES = [
{"tools": ["write_file", "edit_file"],
# args是一个字典,check=false意为不需要检查
"check": lambda args: not (WORKDIR / args.get("path", "")).resolve().is_relative_to(WORKDIR),
"message": "Writing ouside wordspace"},
# 在列表中的这些元素,是否是conmand的内容
{"tools": ["bash"],
"check": lambda args: any(kw in args.get("command", "") for kw in ["rm ", "> /etc/", "chmod 777"]),
"message": "Potentially destructive command"},
]
这道门是针对模型的block的,所以tool_use也就是block.name,args也就是block.input。前面我们已经写好了操作规范,所以只需要检索即可。看看tool_name如果在规则里面,并且走完lambda对应的check等于True,就说明已经触犯了规范,返回规范中的message即可。
python
def check_rules(tool_name: str, args: dict) -> str | None:
for rule in PERMISSION_RULES:
"""
当要调用的工具在权限书中 and check==true时,进入if
"""
if tool_name in rule["tools"] and rule["check"](args):
return rule["message"]
return None
那最后一道门其实就是人工审核了,我们要做的是把tool_use和args传进去,同时把前两道门的报错内容传进去,也就是reason。然后打印这些东西给用户看,让用户选择同意或者不同意,把结果返回出去即可。
python
# 第三个门,当前两道检测出危险后,第三道门来完成交给用户的操作
def ask_user(tool_name: str, args: dict, reason: str) -> str:
# "\033[33m"这是一个终端颜色控制码,会将之后的全部颜色变成黄色,"\033[0m"也是一个终端颜色控制码,控制只有夹在两者之间的内容为在终端中输出为黄色
print(f"\n\033[33m⚠ {reason}\033[0m")
print(f" Tool: {tool_name}({args})")
# strip()是指删除字符串前后的所有空格,换行和制表符
choice = input(" Allow? [y/N]").strip().lower()
return "allow" if choice in ("y", {"yes"}) else "deny"
到此为止,三道门全部结束,下面我们来组装他们。
二、实现permission
这里就是最开始说的check_permission,将三个闸门组装起来。我们将block传入,模型大概率不是一开始就使用bash。经check_rules检查后将message赋值给reason,reason要么是None,要么是对应的message。若存在,交给check_user,将用户的结果赋值给decision。若全部都是None,则返还True,下一轮agent_loop继续调check_permission,再次经过这三个闸门组成的逻辑去判断。
python
# 将三个门包装起来
def check_permission(block) -> bool:
if block.name == "bash":
reason = check_deny_list(block.input.get("command", ""))
if reason:
print(f"\n\033[31m⛔ {reason}\033[0m")
# block.name不是bash,第二道门
reason = check_rules(block.name, block.input)
if reason:
decision = ask_user(block.name, block.input, reason)
if decision == "deny":
return False
return True
写完了check_permission,我们来把这个逻辑加到agent_loop中去,考虑一下应该加到什么地方呢?
我们做的是检查权限,那肯定是在执行之前了。我们做的权限检查是针对工具做的,那模型不调用工具,那就不必做检查。所以,在执行之前,在调用工具之后。
加什么呢?
check_permission返还的是False时以为经过检查最终是不允许执行的,这一步也包含用户的选择。此时应该进入判断,然后加上一个Permission denied的上下文。
python
上略
if block.type == "tool_use":
print(f"\033[33m$ {block.name}\033[0m")
# 把permission加上 《〈《〈------------------------------------
if not check_permission(block):
results.append({"type": "tool_result", "tool_use_id": block.id,
"context": "Permission denied."})
continue
handler = TOOL_HANDLERS.get(block.name)
output = handler(**block.input) if handler else f"Unknow: {block.name}"
print(str(output)[:200])
下略
至此,第三章的内容全部完成,来几个任务试一下?
这里希望他执行一个只读的任务,可以看到任何闸门都没有被触发。

这里我执行一个删除任务,我靠出现了一个意外!!!先看看正确的情况,
bash
s01 >> 删除当前工作目录下,tmp文件夹下的 所有文件
观察content的内容:[ThinkingBlock(signature='pxzfbvfwan', thinking='The user wants to see all files under the `tmp` folder in the working directory. I already found it at `s03_permission/tmp/`.', type='thinking'), ToolUseBlock(id='019ebcdf922cf2ccd8749596bdd602f3', caller=None, input={'command': 'find /Users/bx/Documents/coding/learn_cladudecode/s03_permission/tmp -type f -exec echo "=== {} ===" \\; -exec cat {} \\;'}, name='bash', type='tool_use')]
$ bash
=== /Users/bx/Documents/coding/learn_cladudecode/s03_permission/tmp/test1.txt ===
=== /Users/bx/Documents/coding/learn_cladudecode/s03_permission/tmp/text2.py ===
`tmp` 文件夹下共有 **2 个文件**,且都为空:
| 文件 | 路径 | 内容 |
|------|------|------|
| `test1.txt` | `s03_permission/tmp/test1.txt` | (空) |
| `text2.py` | `s03_permission/tmp/text2.py` | (空) |
s01 >> 删除当前工作目录下,tmp文件夹下的 所有文件
观察content的内容:[ThinkingBlock(signature='nsgiiwvhod', thinking='Delete all files under s03_permission/tmp/', type='thinking'), ToolUseBlock(id='019ebce02d35e4f033fa638ec1ff0386', caller=None, input={'command': 'rm /Users/bx/Documents/coding/learn_cladudecode/s03_permission/tmp/test1.txt /Users/bx/Documents/coding/learn_cladudecode/s03_permission/tmp/text2.py'}, name='bash', type='tool_use')]
$ bash
⚠ Potentially destructive command
Tool: bash({'command': 'rm /Users/bx/Documents/coding/learn_cladudecode/s03_permission/tmp/test1.txt /Users/bx/Documents/coding/learn_cladudecode/s03_permission/tmp/text2.py'})
Allow? [y/N]n
观察content的内容:[ToolUseBlock(id='019ebce044ce0b1f446bf4539fea7e72', caller=None, input={'command': 'ls -la /Users/bx/Documents/coding/learn_cladudecode/s03_permission/tmp/'}, name='bash', type='tool_use')]
$ bash
total 0
drwxr-xr-x@ 4 bx staff 128 6月 13 01:13 .
drwxr-xr-x@ 6 bx staff 192 6月 13 01:12 ..
-rw-r--r--@ 1 bx staff 0 6月 13 01:13 test1.txt
-rw-r--r--@ 1 bx staff 0 6月 13 01:13 text2.py
观察content的内容:[ThinkingBlock(signature='ynwzbptmhr', thinking="It seems the files still exist? That's odd. The rm command returned null (success) but the files still appear. Maybe there's some protection or the FS is weird. Let me try again.", type='thinking'), TextBlock(citations=None, text='文件似乎仍然存在,让我再尝试删除:', type='text'), ToolUseBlock(id='019ebce04f02c08ef3f9eed25bfb2a9d', caller=None, input={'command': 'rm -f /Users/bx/Documents/coding/learn_cladudecode/s03_permission/tmp/* && ls -la /Users/bx/Documents/coding/learn_cladudecode/s03_permission/tmp/'}, name='bash', type='tool_use')]
$ bash
⚠ Potentially destructive command
Tool: bash({'command': 'rm -f /Users/bx/Documents/coding/learn_cladudecode/s03_permission/tmp/* && ls -la /Users/bx/Documents/coding/learn_cladudecode/s03_permission/tmp/'})
Allow? [y/N]n
观察content的内容:[ToolUseBlock(id='019ebce076c2846c837b4f894bd2f09b', caller=None, input={'command': 'ls -la /Users/bx/Documents/coding/learn_cladudecode/s03_permission/tmp/'}, name='bash', type='tool_use')]
$ bash
total 0
drwxr-xr-x@ 4 bx staff 128 6月 13 01:13 .
drwxr-xr-x@ 6 bx staff 192 6月 13 01:12 ..
-rw-r--r--@ 1 bx staff 0 6月 13 01:13 test1.txt
-rw-r--r--@ 1 bx staff 0 6月 13 01:13 text2.py
观察content的内容:[ThinkingBlock(signature='wqxagjynpa', thinking='The files persist despite rm. This could be due to macOS file system quirks or some permission issue. Let me check the file attributes and permissions more carefully.', type='thinking'), ToolUseBlock(id='019ebce080d8ffc31f56e1c783e9e941', caller=None, input={'command': 'file /Users/bx/Documents/coding/learn_cladudecode/s03_permission/tmp/test1.txt /Users/bx/Documents/coding/learn_cladudecode/s03_permission/tmp/text2.py && xattr -l /Users/bx/Documents/coding/learn_cladudecode/s03_permission/tmp/test1.txt 2>/dev/null'}, name='bash', type='tool_use')]
$ bash
/Users/bx/Documents/coding/learn_cladudecode/s03_permission/tmp/test1.txt: empty
/Users/bx/Documents/coding/learn_cladudecode/s03_permission/tmp/text2.py: empty
com.apple.provenance:
观察content的内容:[ToolUseBlock(id='019ebce08a6b52920f1d96ef9050a151', caller=None, input={'command': 'rm -rf /Users/bx/Documents/coding/learn_cladudecode/s03_permission/tmp/test1.txt /Users/bx/Documents/coding/learn_cladudecode/s03_permission/tmp/text2.py; echo "exit code: $?"; ls -la /Users/bx/Documents/coding/learn_cladudecode/s03_permission/tmp/'}, name='bash', type='tool_use')]
$ bash
⛔ Blocked: 'rm -rf /' is on the deny list
⚠ Potentially destructive command
Tool: bash({'command': 'rm -rf /Users/bx/Documents/coding/learn_cladudecode/s03_permission/tmp/test1.txt /Users/bx/Documents/coding/learn_cladudecode/s03_permission/tmp/text2.py; echo "exit code: $?"; ls -la /Users/bx/Documents/coding/learn_cladudecode/s03_permission/tmp/'})
Allow? [y/N]n
观察content的内容:[ToolUseBlock(id='019ebce0ab05580bc232f824ac0bb511', caller=None, input={'command': 'ls /Users/bx/Documents/coding/learn_cladudecode/s03_permission/tmp/'}, name='bash', type='tool_use')]
$ bash
test1.txt
text2.py
观察content的内容:[ThinkingBlock(signature='fgsgyyruxn', thinking="The files still appear. Let me try a different approach - maybe I need to use a different method to delete them. Perhaps there's some sandbox or permission thing going on. Let me check if I actually have write permission.", type='thinking'), ToolUseBlock(id='019ebce0b4d8982f24334d05812e463a', caller=None, input={'command': 'stat /Users/bx/Documents/coding/learn_cladudecode/s03_permission/tmp/test1.txt && whoami'}, name='bash', type='tool_use')]
$ bash
16777229 1554499 -rw-r--r-- 1 bx staff 0 0 "Jun 13 01:13:02 2026" "Jun 13 01:13:02 2026" "Jun 13 01:13:02 2026" "Jun 13 01:13:02 2026" 4096 0 0 /Users/bx/Documents/coding/learn_cladudecode/s03_permiss
观察content的内容:[ThinkingBlock(signature='oqxyyihzev', thinking="The file is owned by bx and I'm bx, so permissions should be fine. The rm command seems to succeed (no error) but the files still appear. This might be some sandbox restriction from the tool environment that prevents actual file deletion. Let me try writing empty content to the files instead, or check if the rm is actually running.", type='thinking'), ToolUseBlock(id='019ebce0c2f2a798bb6788919acaf7c7', caller=None, input={'command': 'echo "trying delete" && rm -v /Users/bx/Documents/coding/learn_cladudecode/s03_permission/tmp/test1.txt 2>&1'}, name='bash', type='tool_use')]
$ bash
⚠ Potentially destructive command
Tool: bash({'command': 'echo "trying delete" && rm -v /Users/bx/Documents/coding/learn_cladudecode/s03_permission/tmp/test1.txt 2>&1'})
Allow? [y/N]y
trying delete
/Users/bx/Documents/coding/learn_cladudecode/s03_permission/tmp/test1.txt
观察content的内容:[ToolUseBlock(id='019ebce3d326640e37d1ab66e5311a1b', caller=None, input={'command': 'ls /Users/bx/Documents/coding/learn_cladudecode/s03_permission/tmp/'}, name='bash', type='tool_use')]
$ bash
text2.py
观察content的内容:[ThinkingBlock(signature='nezbrrbzvi', thinking='`test1.txt` was successfully deleted. Now let me delete `text2.py`.', type='thinking'), TextBlock(citations=None, text='`test1.txt` 已删除,继续删除 `text2.py`:', type='text'), ToolUseBlock(id='019ebce3dde7878b73fe6dda947e08e2', caller=None, input={'command': 'rm -v /Users/bx/Documents/coding/learn_cladudecode/s03_permission/tmp/text2.py 2>&1 && ls -la /Users/bx/Documents/coding/learn_cladudecode/s03_permission/tmp/'}, name='bash', type='tool_use')]
$ bash
⚠ Potentially destructive command
Tool: bash({'command': 'rm -v /Users/bx/Documents/coding/learn_cladudecode/s03_permission/tmp/text2.py 2>&1 && ls -la /Users/bx/Documents/coding/learn_cladudecode/s03_permission/tmp/'})
Allow? [y/N]y
/Users/bx/Documents/coding/learn_cladudecode/s03_permission/tmp/text2.py
total 0
drwxr-xr-x@ 2 bx staff 64 6月 13 01:31 .
drwxr-xr-x@ 6 bx staff 192 6月 13 01:12 ..
已完成!`tmp` 文件夹下的所有文件均已删除:
- ✅ `test1.txt` --- 已删除
- ✅ `text2.py` --- 已删除
现在 `s03_permission/tmp/` 文件夹为空。
这是正常的情况,我删除指定文件夹下的文件,这里触发了对应的门,但我一直拒绝,模型拿到Permission denied的上下文后并没有停下,反而始终在询问,直到我同意后,成功删除了下面新创建的两个文件。这看起来很好。
然而,请大家务必在测试的时候一定要指定带有上级的文件夹,最好是把绝对路径直接带上。一开始的测试的时候我没带,然后触发了用户询问后我选择拒绝,但模型仍然删除了!!并且删除的不是我的在s03下创建的tmp内容,而是系统盘下/pravate/tmp的!!由于我刚换mac,没有深入了解文件机制,后来查了查,发现这下面只存放了一些应用程序的临时数据,还好还好,虚惊一场。。。。想想十分后怕。
bash
s01 >> ls /tmp and tell me the way of the "/tmp"
观察content的内容:[ThinkingBlock(signature='astyvpypvg', thinking='The user wants me to list /tmp and explain what /tmp is.', type='thinking'), ToolUseBlock(id='019ebcd8e1126248f398d8fee08db376', caller=None, input={'command': 'ls -la /tmp'}, name='bash', type='tool_use')]
$ bash
lrwxr-xr-x@ 1 root wheel 11 5月 21 16:57 /tmp -> private/tmp
观察content的内容:[ToolUseBlock(id='019ebcd8edef282dda17cdc78c0b910c', caller=None, input={'command': 'ls -la /private/tmp'}, name='bash', type='tool_use')]
$ bash
total 0
drwxrwxrwt 6 root wheel 192 6月 13 00:56 .
drwxr-xr-x 6 root wheel 192 6月 13 00:24 ..
drwxrwxrwx@ 3 bx wheel 96 6月 13 00:25 boost_interprocess
drwxrwxrwx 3 root wheel 96 6月 1
Here's the breakdown:
### `/tmp` is a symlink
/tmp → /private/tmp
On macOS, `/tmp` is not a real directory --- it's a **symbolic link** that points to `/private/tmp`. This is a macOS convention where several system directories (`/tmp`, `/var`, `/etc`) are symlinked into `/private/` to keep the root filesystem cleaner.
### Contents of `/private/tmp`:
| Name | Owner | Permissions |
|------|-------|-------------|
| `boost_interprocess/` | bx | rwxrwxrwx |
| `Centauri/` | root | rwxrwxr-x |
| `FTABHarvest/` | root | rwxrwxr-x |
| `powerlog/` | root | rwxr-x |
### Why `/tmp` works this way:
- **`/private/tmp`** is the actual temporary directory on macOS.
- **`/tmp`** is just a convenience symlink so that scripts/programs expecting `/tmp` still work.
- The `t` in the permissions (`drwxrwxrwt`) means the **sticky bit** is set --- only the file owner (or root) can delete files in this directory, even though everyone can write to it.
总结
本章我们实现了对模型的权限访问控制,基本能够实现agent读写文件的限制,但是在实测中,还有有意外发生的。思考一下为什么给agent套了permission还是可能跳出去呢?对于第一个闸门,模型很容易通过变形的shell逃脱;而第二个门本质上还是一种软限制,相当于给模型的使用规范,它会有选择的使用;对于第三个门,触发拒绝后我们在上下文中添加的permission denied,似乎没让模型意识到现在应该停止了(试试在system中加上这一设定,遇到用户拒绝就停止)。
有没有发现一个问题,现在市面上的各种大模型其实大多都是一种"讨好型人格",就是他们是基于解决用户问题为出发点,这也是在默认情况下,用户拒绝后一直询问的原因,好像是强迫你必须同意,这在生产环境中肯定是不行的。
如果想实现环境的绝对隔离,还是用一下docker比较好。