
1. 从 s02 到 s03工具越多越需要一道闸如果你跟着 learn-claude-code 的教程一路写下来s01 让你跑通了一个最朴素的 Agent Loops02 让模型从只会调 bash 扩展到 read_file、write_file、edit_file、glob 这些专用工具。到这一步Agent 已经能读文件、改代码、跑命令看起来挺像回事了。但工具一多一个很现实的问题就冒出来了模型说执行什么程序就真的执行什么吗s02 里文件类工具其实已经有了一层保护比如 safe_path() 会把路径 resolve 之后检查是否还在工作区内防止模型通过../../etc/passwd这种路径逃出去。可 bash 工具不一样它天然就是一个很大的安全面。模型只要调用了 shell就可能提出rm -rf、sudo、shutdown这类命令。问题不在于模型会不会故意作恶而在于只要运行时把执行权完全交给模型风险就已经存在了。s03 Permission 这一节要解决的就是在模型和工具 handler 之间插一层判断模型仍然负责提出工具调用但运行时必须先判断这个调用能不能执行。它不新增业务工具也不重写 Agent Loop只是在工具分发前面加了一道权限闸。这篇笔记我会从 settings.json 的骨架入手把 allow/deny 规则怎么配、怎么接上统一的 Key 通道、怎么验证一次越权拦截完整走一遍。2. 前置准备把 Key 通道和运行环境先理顺在动手配权限之前得先把模型调用这条链路跑通不然权限配好了也没法验证。我这边习惯用 TaoToken 做统一的 Key 通道好处是 Claude Code、脚本、本地调试都走同一个入口不用在多个地方来回换 Key。先拿到 API Key。打开控制台 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 在 API Keys 页面创建一个新 Key复制出来存好。这个 Key 后面会写进环境变量不要直接硬编码进 settings.json 里提交到仓库。拿到 Key 之后把它配成环境变量。Linux/macOS 下可以写进~/.bashrc或~/.zshrcexport TAOTOKEN_API_KEYsk-你的key export ANTHROPIC_BASE_URLhttps://taotoken.net/api export ANTHROPIC_API_KEY$TAOTOKEN_API_KEYWindows PowerShell 下用$env:TAOTOKEN_API_KEYsk-你的key $env:ANTHROPIC_BASE_URLhttps://taotoken.net/api $env:ANTHROPIC_API_KEY$env:TAOTOKEN_API_KEY这里ANTHROPIC_BASE_URL指向 https://taotoken.net/api Claude Code 和兼容 Anthropic 协议的客户端都会读这个变量。配好之后可以先用模型对话页面 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 发一条消息确认 Key 是通的再往下做权限配置。注意环境变量里的 Key 只在本机生效别写进任何会提交到 Git 的文件。settings.json 里只放权限规则不放密钥。3. settings.json 骨架allow / ask / deny 三档规则Claude Code 的权限配置核心就是 settings.json。它有几个层级用户级在~/.claude/settings.json项目级在项目根目录的.claude/settings.json。项目级会覆盖用户级团队协作时把项目级配置提交进仓库大家就共享同一套权限规则。先看一个最小骨架{ permissions: { allow: [], ask: [], deny: [] } }三个数组分别对应三种决策字段含义命中后的行为allow明确放行的规则直接执行不打断ask需要确认的规则暂停等用户批准deny明确拒绝的规则直接拦截不执行判断优先级是 deny ask allow。也就是说一条命令如果同时命中 deny 和 allowdeny 赢。这个顺序很关键它保证了硬性禁止不会被宽松规则覆盖。规则本身用「工具名(匹配模式)」的形式写。工具名就是 Bash、Read、Write、Edit、Glob 这些。匹配模式支持通配符比如Bash(npm run test:*)表示所有以npm run test:开头的命令。一个稍微完整点的项目级配置长这样{ permissions: { allow: [ Read(//workspace/**), Glob(//workspace/**), Bash(git status:*), Bash(git diff:*), Bash(npm run lint:*), Bash(npm run test:*) ], ask: [ Write(//workspace/**), Edit(//workspace/**), Bash(git commit:*), Bash(git push:*), Bash(rm:*) ], deny: [ Bash(sudo:*), Bash(shutdown:*), Bash(reboot:*), Bash(mkfs:*), Bash(dd:*), Read(//etc/shadow), Read(~/.ssh/**), Write(//etc/**) ] } }这份配置的思路是只读操作和安全的 git 查询、测试命令直接放行写文件、提交、推送、删除这类有副作用的操作走 asksudo、关机、格式化、读敏感文件这类绝对不该发生的直接 deny。提示路径里的//表示绝对路径的根~表示用户主目录。写规则时尽量用具体路径别用太宽泛的通配符否则等于没设防。4. 把权限规则接进工具执行链路settings.json 配好只是第一步真正让规则生效的是运行时在工具执行前的那次检查。如果你在跟着 learn-claude-code 写自己的 harness可以在 s02 的 dispatch 逻辑前面插一个 check_permission()思路和 s03 一致先查 deny再查 ask最后才落到 handler。下面是一个可以直接跑的 Python 片段把 settings.json 读进来对每个 tool_use block 做判断import json import fnmatch from pathlib import Path SETTINGS_PATH Path(.claude/settings.json) def load_permissions(): if not SETTINGS_PATH.exists(): return {allow: [], ask: [], deny: []} data json.loads(SETTINGS_PATH.read_text(encodingutf-8)) return data.get(permissions, {allow: [], ask: [], deny: []}) def match_rule(rule: str, tool_name: str, args: dict) - bool: # rule 形如 Bash(git status:*) 或 Read(//workspace/**) if ( not in rule: return rule tool_name name, pattern rule.split((, 1) pattern pattern.rstrip()) if name ! tool_name: return False # 取工具的主要参数做匹配 if tool_name Bash: target args.get(command, ) else: target args.get(path, ) or args.get(file_path, ) return fnmatch.fnmatch(target, pattern) def check_permission(tool_name: str, args: dict) - str: perms load_permissions() for rule in perms.get(deny, []): if match_rule(rule, tool_name, args): return deny for rule in perms.get(ask, []): if match_rule(rule, tool_name, args): return ask for rule in perms.get(allow, []): if match_rule(rule, tool_name, args): return allow return ask # 没命中任何规则时默认走确认然后在 Agent Loop 里模型返回 tool_use 之后、调用 handler 之前插入判断for block in response.content: if block.type ! tool_use: continue decision check_permission(block.name, block.input) if decision deny: results.append({ type: tool_result, tool_use_id: block.id, content: Permission denied by settings.json. }) continue if decision ask: answer input(f允许执行 {block.name}({block.input})? [y/N] ).strip().lower() if answer not in (y, yes): results.append({ type: tool_result, tool_use_id: block.id, content: Permission denied by user. }) continue handler TOOL_HANDLERS.get(block.name) output handler(**block.input) if handler else fUnknown: {block.name} results.append({ type: tool_result, tool_use_id: block.id, content: output })这段代码的关键点在于无论工具看起来多安全都必须先过 check_permission()。安全请求被 allow 规则放行风险请求进 ask硬禁止直接 deny。模型下一轮会收到Permission denied这个 tool_result然后自己重新规划Agent Loop 的闭环没有被破坏。5. 验证一次越权拦截让 deny 规则真的挡住命令配置写完最该做的是验证它到底有没有生效。我一般会设计几个测试用例覆盖 allow、ask、deny 三种路径。先测 allow。在项目里让 Claude Code 读一个工作区内的文件Read the file README.md in the current directory因为Read(//workspace/**)在 allow 里这次调用应该直接通过不弹确认。再测 ask。让它写一个新文件Create a file called test.txt in the current directoryWrite(//workspace/**)在 ask 里所以会暂停等你确认。输入 y 之后文件才会真正创建。最后测 deny这是最关键的一步。让它执行一条 sudo 命令Run sudo rm -rf /tmp/build-cache因为Bash(sudo:*)在 deny 里这次调用应该被直接拦截返回Permission denied by settings.json.handler 根本不会被调用。你可以在终端看到类似这样的输出 Bash Permission denied by settings.json.如果这条命令真的被执行了说明 deny 规则没匹配上回去检查规则写法。常见问题是把Bash(sudo:*)写成了Bash(sudo rm:*)匹配范围太窄sudo后面跟别的参数就漏了。再补一个边界测试验证 deny 优先级高于 allow。假设你同时写了Bash(git push:*)在 allow、Bash(git push --force:*)在 deny那么执行git push --force origin main时应该被 deny 拦住而不是被 allow 放行。这个测试能确认你的判断顺序是对的。6. 本篇常见错排查规则写了但不生效。先确认 settings.json 的位置对不对。项目级是.claude/settings.json不是项目根目录的settings.json。另外 JSON 格式必须合法多一个逗号都会导致整个文件解析失败权限规则全部失效。可以用python -m json.tool .claude/settings.json检查一下。通配符匹配不上。Bash(git status:*)里的:*是 Claude Code 的约定写法表示「命令前缀匹配」。如果你自己写 harness用 fnmatch 的话*就够了别把两种语法混在一起。路径匹配里**表示递归*只匹配单层。ask 规则太多用起来很烦。这是配置粒度的取舍。把高频且安全的操作放进 allow比如git status、npm run test只把真正有副作用的操作留在 ask。否则每次读文件都要确认体验会很差。deny 规则被绕过。教学版的字符串匹配确实容易被 shell 展开、命令别名、路径变体绕过。比如sudo写成/usr/bin/sudo简单匹配就漏了。生产环境需要更严格的解析或者用工具自身的校验逻辑兜底。settings.json 的规则是第一道闸门不是唯一一道。Key 通道报 401。检查ANTHROPIC_BASE_URL是不是 https://taotoken.net/api 以及ANTHROPIC_API_KEY有没有正确指向你的 Key。环境变量改完记得重新开一个终端或者 source 一下配置文件。如果还是不通去 API Keys 页面 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 确认 Key 状态必要时重新生成一个。权限拒绝后模型卡住不动。这是正常的模型收到Permission denied之后需要重新规划。如果它反复尝试同一条被拒命令说明 prompt 里没有给它足够的替代路径。可以在系统提示里加一句「如果某个工具调用被拒绝请换一种方式或询问用户」引导它走出死循环。7. 继续往下走把权限当成运行时边界s03 这一节代码量不大但位置很关键。它第一次把「模型提出工具调用」和「工具真正执行」拆成了两个阶段。模型只拥有申请执行的能力真正是否落地由 harness 层的权限管线决定。deny 是绝对禁止ask 是需要确认allow 是安全放行这三种结果对应的是系统对模型意图的不同信任等级。如果你想把这条链路跑得更顺建议把 Key 通道和权限配置一起固定下来。日常调试模型行为可以用模型对话页面 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 快速验证长期跑编码任务或者 Agent 工作流可以看下 Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 把额度统一管理接入细节和参数说明都在接入文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里遇到规则写法或者协议兼容问题可以直接查。权限配好之后下一步就是 s04 Hooks。到那时候你会发现settings.json 里的规则是静态的而 Hooks 让你在工具执行前后插入动态逻辑两者配合起来Agent 的运行时治理才算真正立起来。