我们把 Claude Code 装好、配好,也写好了 CLAUDE.md。在使用的时候会遇到一个问题。给一个天气 App 加"未来七天预报",我一边想放手让它去改,一边又怕它哪一步删错了文件,或者顺手把我没让它碰的网络层也一起重构了。于是就来回纠结:每步都点"允许",太烦;索性全自动,又不放心。
这篇文章就来拆这个问题。它其实是两件事。第一,动手之前,能不能先看看它打算怎么干?这就是计划模式。第二,动手当中,怎么把"哪些能干、哪些得先问我、哪些绝对不许"定成规则,而且是你说了算的规则?这就是权限系统。后一块也是最容易踩坑的地方,空格、分隔符、路径前缀,差一个字符,你以为配好的规则其实根本没生效。我尽量把它讲准。
文中说法以官方文档(code.claude.com)为准,截至 2026 年中。
一、计划模式:先出方案,不碰你的代码
计划模式(Plan mode)做的事很简单:让 Claude 只研究、只给方案,不动你的源码。
在这个模式下,它可以读文件,可以跑只读命令去摸清代码库,最后给你一份完整的改动计划,但不会编辑任何一个源文件。官方原话是 "reads files and runs read-only shell commands to explore but does not edit your source files"。
这恰好对症"不敢放手"这个病。碰上大改动,或者一个你自己都不熟的代码库,先让它把方案摆出来,你看看思路对不对、它打算动哪几个文件,确认没问题,再放它真改。
怎么进入,有三种方式:
# ① 会话里按 Shift+Tab 循环切到 plan(状态栏会显示当前模式) # ② 只想让某一条提问先走计划,在前面加 /plan /plan 帮天气 App 加一个未来七天预报,先别动手,给我方案 # ③ 启动时就进 plan claude --permission-mode plan方案出来之后怎么走,这一步很多人没留意。计划就绪时,Claude 会把方案摆给你,并列出五个选项让你挑下一步:
批准,并切到 auto 模式继续(它自己往下跑)
批准,并切到 acceptEdits(自动改文件,但跑命令仍要问)
批准,但每个改动都让你手动逐个确认
不批准,带着你的反馈继续打磨方案
用 Ultraplan 在浏览器里进一步细化(进阶,先不展开)
注意第 4 个:方案不满意,不用推倒重来,把意见直接甩给它接着改就行。而只要你批准了方案,就等于退出计划模式、切到你选的那个权限档,它才开始动手。要是你只想退出计划模式、什么都不批,再按一次Shift+Tab就行。
还有两个顺手的小点。方案太长、想自己动两笔,按Ctrl+G能把它在你默认编辑器里打开,改完再让它执行。如果某个项目你希望一进去就是计划模式,在.claude/settings.json里写一行就行:
{ "permissions": { "defaultMode": "plan" } }回到我那个七天预报。当时就是先/plan,它给的方案里写清楚要动WeatherApi.java、ForecastResponse.java两个文件,加一个v7/weather/7d的接口调用。我一看接口路径没错、也没去乱碰 UI 层,才放它改。大改动、不熟的代码,先扫一眼方案再放权,比改完再回头 review 省事得多,主动权也一直在你手里。
二、放权的地基:deny、ask、allow
计划模式管的是动手前,剩下的是动手当中的规则。Claude Code 的权限系统就三种规则,你随时能用/permissions命令可视化地看和改,它会把每条规则、以及规则来自哪个 settings 文件都列出来:
allow:免确认,直接用;
ask:每次用都问你一下;
deny:禁止,根本不给用。
关键在它们怎么裁决。官方原文一字不差是这样:
Rules are evaluated in order: deny, then ask, then allow. The first match in that order determines the outcome, and rule specificity does not change the order.
翻成人话是三句:按 deny、ask、allow 的顺序查;第一个命中的规则就定了结果;规则写得再具体,也不改变这个顺序。
第三句最容易栽跟头。不少人以为"更具体的规则优先级更高",其实不是。看官方那个例子:你写了个宽泛的 denyBash(aws *),又写了条具体的 allowBash(aws s3 ls),心想"除了列 S3,别的 aws 命令都禁"。结果aws s3 ls也被禁了,因为 deny 先查、先命中。deny 规则里开不出"白名单例外"。ask 也一样,命中了 ask 就一定弹窗,哪怕后面有更具体的 allow 也拦不住。
这个顺序跨文件也成立。deny 只要在任意一层出现(企业托管、项目、用户),别的层再 allow 也救不回来。
所以想开例外,靠的不是再补一条更具体的 allow,而是一开始就别用太宽的 deny 把它拦死。
三、规则怎么写:裸工具名和带括号
规则的格式就两种:Tool,或者Tool(具体内容)。
不带括号的裸工具名,匹配这个工具的所有用法。Bash匹配所有命令,Read匹配所有读取,WebFetch匹配所有联网抓取(Bash(*)和Bash完全等价)。
这里有个差别,不少人不知道,尤其拿来做 deny 的时候得分清:
裸名 deny(比如
Bash),会把这个工具从 Claude 的上下文里整个拿掉,它根本看不到有这么个工具;带范围的 deny(比如
Bash(rm *)),工具还在,只拦住匹配的那几种调用。
所以"我不想让它跑任何命令"和"我只想禁掉 rm",是两种完全不同的写法,别混。
带括号,就是加一个 specifier 做精细控制:Bash(npm run build)只匹配这一条命令,Read(./.env)匹配当前目录的 .env,WebFetch(domain:example.com)匹配这个域名。
简单记:裸名管的是"这个工具用不用",带括号管的是"具体哪些用法";而且裸名 deny 是让工具直接消失,不是拦截。
四、Bash 匹配里,一个空格的差别
这一节和下一节是全篇最需要盯细节的地方。你以为写对了的 Bash 规则,很可能因为一个空格、一个分隔符,根本没照你想的生效。
空格决定边界
直接看官方原文:
The space before
*matters:Bash(ls *)matchesls -labut notlsof, whileBash(ls*)matches both.
Bash(ls *),*前面有个空格,这个空格强制了一个"词边界":前缀后面必须跟空格,或者到此为止。所以它匹配ls -la,但不匹配lsof。Bash(ls*)没有空格,也就没有边界,lsof、lsblk这些 ls 开头的全被它圈进来。
就一个空格,放行的范围差出十万八千里。还有个等价写法,:*后缀等同于"空格加星号",Bash(ls:*)和Bash(ls *)一个意思。但注意:*只有在结尾才认,写成Bash(git:* push),中间那个冒号会被当成普通字符,什么 git 命令都匹配不到。
复合命令,每一段都得单独过关
老教程常这么吓你:"写了Bash(npm test),它跑npm test; rm -rf .就把你坑了。"现在不是这样了。官方讲得明白:
Claude Code is aware of shell operators, so a rule like
Bash(safe-cmd *)won't give it permission to run the commandsafe-cmd && other-cmd.
Claude Code 认识 shell 分隔符,它认的有&&、||、;、|、|&、&和换行。碰上复合命令,它会拆成一段段子命令,每一段都得各自命中规则,整条才放行。所以npm test && rm -rf .,不会因为npm test被允许就整条放过,那个rm还是要单独过一遍权限。
顺带说一句,你对一条复合命令点"Yes, don't ask again",它存下来的是每个需要批准的子命令各一条规则(比如批准git status && npm test,实际存的是npm test),单次最多存 5 条。
进程包装器会先被扒掉再匹配
这条能解释一个常见的困惑:"我只写了Bash(npm test *),怎么timeout 30 npm test也直接过了?"因为匹配之前,Claude Code 会先剥掉一组固定的进程包装器:timeout、time、nice、nohup、stdbuf,还有不带参数的xargs,剥完再拿里头真正的命令去匹配。
但要当心那些没在剥离名单上的"运行器":npx、docker exec、devbox run、direnv exec,这些不剥。它们会把后面一整串都当命令执行,于是Bash(devbox run *)就等于把devbox run rm -rf .也放行了。要用就写具体点:Bash(devbox run npm test)。
只读命令本来就不弹
最后一个省心的地方:有一组内置的只读命令,任何模式下都直接跑、不弹窗——ls cat echo pwd head tail grep find wc which diff stat du cd,还有 git 的只读形式。这些你不用专门写 allow。
一句话,ls *和ls*不是一回事,复合命令逐段过关,包装器会被先扒掉。规则光"看着对"不够,得"真能匹配上"才算数。
五、路径锚点:/开头,不是你以为的绝对路径
管"能读、能改哪些文件"的 Read、Edit 规则,路径写法照 gitignore 的规范来,一共四种锚点。这里藏着一个头号陷阱,先看表:
写法 | 含义 | 例子 | 实际指向 |
|---|---|---|---|
//path | 绝对路径(从文件系统根) | Read(//Users/alice/secrets/**) | /Users/alice/secrets/** |
~/path | 家目录起算 | Read(~/Documents/*.pdf) | 你家目录下的 Documents |
/path | 项目根相对 | Edit(/src/**/*.ts) | <项目根>/src/**/*.ts |
path或 | 当前目录相对 | Read(*.env) | 当前目录下的 .env |
陷阱就在第三行。官方专门加了句警告:
A pattern like
/Users/alice/fileis NOT an absolute path. It's relative to the project root. Use//Users/alice/filefor absolute paths.
以单个/开头,指的是"项目根目录",不是系统绝对路径。想写真正的绝对路径,得用两个斜杠//。这条我在第 2 篇提过,这次把它放进完整的四锚点里,因为太多人在这儿写错:本想 deny 掉系统里某个绝对路径,结果写成单斜杠,规则悄悄指到了项目里一个根本不存在的路径,等于白写。
再补两个常用的细节。裸文件名按 gitignore 在任意层级匹配,Read(.env)等价于Read(**/.env),当前目录连同所有子目录下的 .env 都算;而Read(//**/.env)才是匹配整个文件系统里的所有 .env。另外,Read/Edit 的 deny 也会管住 Claude 在 Bash 里识别出来的cat、head、sed这类读文件命令,但它管不了一个 Python 脚本自己open()文件,那种 OS 级别的封堵得靠 sandbox(后面的篇章再讲)。
记住//才是绝对路径、/是项目根就行。就这一个斜杠的差别,能让你的 deny 规则彻底落空。
六、六种权限模式,和"保护路径"这道兜底
第 2 篇入门时讲过常用的"四档"(default、acceptEdits、bypassPermissions、plan)。这篇补全到官方的六种:
模式 | 不用问就能做的 | 适合 |
|---|---|---|
default | 只读 | 起步、敏感工作 |
acceptEdits | 读 + 改文件 + 常见文件系统命令(mkdir/touch/mv/cp/rm/sed,仅限工作目录内,且保护路径与 | 边改边审 |
plan | 只读 | 动手前先探索、出方案 |
auto | 几乎全部,但有后台分类器做安全审查 | 长任务、减少打断 |
dontAsk | 仅预先 allow 的,其余自动拒(不弹) | CI / 锁定环境 |
bypassPermissions | 全部 | 仅隔离容器 / VM |
有两点容易被忽略。
一是Shift+Tab默认只在 default、acceptEdits、plan 这三档之间循环。auto、dontAsk、bypassPermissions 不在默认循环里:auto 要账号满足条件才会出现,bypass 得用--dangerously-skip-permissions(或者--permission-mode bypassPermissions)启动之后才进循环,dontAsk 干脆只能靠启动参数指定。其中 auto 这个新模式值得了解一下,它放行大部分操作,但后台有个独立的分类器模型在审查,挡住"下载并执行代码、把敏感数据发往外部、强推 main"这类危险动作。不过官方也明说了,它是研究预览,能少打断你几次,但不等于安全,别拿它当 review 的替代品。
二是最实用的那道兜底,保护路径。除了 bypassPermissions,对一批关键路径的写入永远不会被自动批准,连 allow 规则都没法预先放行(安全检查跑在 allow 之前)。这批路径包括.git、.claude、.vscode、各种 shell 配置文件(.zshrc、.bashrc、.envrc),还有gradle-wrapper.properties、maven-wrapper.properties。
还记得第 2 篇那张实拍吗?Claude 核对天气 App 时发现"仓库没有 gradle-wrapper"。而gradle-wrapper.properties恰好就是保护文件之一。就算你开了 acceptEdits,它也不会擅自帮你改 wrapper 配置,而是停下来问你。这套兜底,正是"放权但不至于失控"的最后一道防线。
所以日常用Shift+Tab那三档就够了。放权放得再大,.git、shell 配置、wrapper 这些保护路径也不会被自动改,除非你进了沙箱专用的 bypass。
七、三段可以直接抄的配置
机制讲了这么多,落到~/.claude/settings.json或者项目的.claude/settings.json里,给你三段能直接抄的。
① 日常提速:放行常用,守住危险
{ "permissions": { "allow": [ "Bash(npm run *)", "Bash(git commit *)", "Bash(git push origin *)" ], "deny": [ "Bash(git push * main)", "Read(.env)", "Read(.env.*)", "Read(./secrets/**)" ] } }npm run、git commit免确认;推 main 被 deny 兜底拦下;.env 和 secrets 目录对 Claude 直接隐身。记住第二节的教训,别想着在 allow 里加一条git push * main来开例外,deny 先查,例外开不出来。
② 不熟的项目,默认先出方案
{ "permissions": { "defaultMode": "plan" } }③ 用域名白名单替代 curl,更靠谱
{ "permissions": { "deny": ["Bash(curl *)", "Bash(wget *)"], "allow": ["WebFetch(domain:docs.qq.com)", "WebFetch(domain:*.mycompany.com)"] } }为什么不直接写Bash(curl https://xxx *)去限制 curl 的目标?因为那样很脆。官方列了一串绕过的法子:把-X GET放到 URL 前面、换成 https、用短链接重定向、用变量URL=... && curl $URL,甚至多打一个空格,都能让你的规则失效。靠谱的做法就是上面这样,deny 掉命令行的联网工具,改用WebFetch(domain:...)走域名白名单。
allow 放高频的、deny 守底线、敏感文件让它看不见、联网走域名白名单。这套组合下来,比全程点确认省心,比全自动安全。
八、两条边界,必须记住
这篇讲的是放权。越是讲放权,越得把边界说清楚,不然就是帮倒忙。
第一,权限规则是 Claude Code 在执行,不是模型在执行。你在 CLAUDE.md 或者提示词里写"不要碰数据库",那只影响它想不想去碰;真正决定允不允许它碰的,是这篇讲的权限规则、权限模式,或者 PreToolUse hook。想要硬边界,就写 deny 规则,别指望靠一句叮嘱。
第二,靠约束命令参数的规则天生脆弱,别拿它当安全墙。第七节那个 curl 的例子就是典型。真要封网络或者文件访问,靠域名白名单、PreToolUse hook,或者 OS 级的 sandbox,这些是后面进阶篇(第 7 篇)的内容。权限规则挡得住"Claude 主动想去做的事",挡不住一个它跑起来的脚本在底层自己干的事。
说到底,权限是"你说了算"的硬规则,不是"叮嘱它"的软约束。要硬边界,用 deny 和 hook,别拿脆弱的参数匹配凑数。
九、放权的本质,是把判断写成规则
回到开头那个纠结:全程确认太烦,全自动又不放心。
这篇给的其实是同一把钥匙的两面。动手前,用计划模式先看清它要干什么;动手中,用权限规则把"你能干、你先问、你绝对不能"固定下来。这两件事都到位,你既不用一步步盯着点"允许",也不用把方向盘整个交出去、还蒙着眼。
再往深一层看,这跟整个系列反复碰到的那条线是一致的:Claude Code 用得好不好,说到底是你能不能把对它的约束,从"脑子里的担心"变成"系统里的规则"。CLAUDE.md 是把上下文写成规则,权限配置是把放权写成规则。规则写准了,手才放得安心。