Claude Code 权限配置详解:settings.json 的 allow/deny 实测 28 次

Claude Code 每次要跑命令、改文件都弹窗问你,点多了就想一键全放行;全放行又怕它哪天 rm 掉不该删的东西。折中的办法是 settings.json 里的 permissions:哪些命令直接放、哪些必须问、哪些一律拒绝。我们在本机用 Claude Code 2.1.309 跑了 28 次工具调用,把文档里最容易误解的几条规则逐条试了一遍。
前置条件
- 测试环境:北京时间 2026-10-26,Debian,Node.js 20.19.2,Claude Code 2.1.309(npm 装)。
- 没用付费模型。官方文档写明权限规则由 Claude Code 执行,跟模型无关,所以我们写了一个几十行的本地假 Messages API,按脚本让 Claude Code 发出指定的工具调用,再看它放行还是拒绝。这样每次发出的命令都是固定的,不受模型发挥影响。
- 测试目录是一个 git 仓库,里面有
.env(内容SECRET=abc123)和build/out.txt。用CLAUDE_CONFIG_DIR指向单独目录,没碰本机真实的~/.claude。 - 用
claude -p … --output-format stream-json --verbose跑,结果里的permission_denials字段会列出被拒的调用。-p模式没人点弹窗,需要确认的调用一律按拒绝处理。
四个文件各管一摊,列表会合并
装完 Claude Code 不会自带设置文件,下面四个按需自己建:
| 文件 | 影响谁 | 适合放什么 |
|---|---|---|
~/.claude/settings.json | 你,本机所有项目 | 个人习惯、自己的通用权限 |
.claude/settings.json | 这个项目的所有人(提交进 git) | 团队统一的权限、钩子、插件 |
.claude/settings.local.json | 你,只在这个项目 | 个人例外;弹窗里点「Yes, and don't ask again」也写这里 |
| 托管设置(managed-settings.json 等) | 组织下发到的所有人 | 安全策略,别的层改不了 |
同一个键写在多处时,优先级从高到低是:托管设置 → 命令行 --settings → 项目本地 → 项目共享 → 用户级。permissions.allow 这种列表不按优先级覆盖,而是几个文件的条目合在一起。改完在会话里输 /status,「Setting sources」一行会列出这次加载了哪些文件;/permissions 能看到每条规则来自哪个文件。
规则怎么写:deny 先于 ask,ask 先于 allow
一个够用的起步配置:
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"permissions": {
"allow": ["Bash(npm run *)", "Bash(git commit *)", "Bash(git log *)"],
"ask": ["Bash(git push *)"],
"deny": ["Read(./.env)", "Read(./.env.*)", "Read(./secrets/**)", "Bash(curl *)"]
}
}
判定顺序是固定的:先看 deny,再看 ask,最后看 allow,命中谁算谁,跟哪条写得更具体无关。所以 deny 了 Bash(aws *),再 allow Bash(aws s3 ls) 也开不了口子。几条写法上的细节:
- 星号放在子命令后面。
Bash(git log *)只放行git log;Bash(git *)等于放行所有 git 子命令。 - 空格有意义。
Bash(ls *)不匹配lsof,Bash(ls*)会匹配。Bash(ls:*)和Bash(ls *)等价。 - 路径前缀分四种。
//是文件系统根目录,~/是家目录,单个/是相对这个设置文件所在的位置,不写前缀是相对当前目录。写在用户级文件里的Read(/secrets/**)管的是~/.claude/secrets,不是项目里的 secrets。 - 没有 Write 规则。文件权限只认
Edit(path)和Read(path),写成Write(docs/**)不报错,但永远不会命中。
在 CLAUDE.md 里写「不要读 .env」,模型可以不照做;写进 permissions.deny,Read 工具和 cat 就都读不到了。下面会看到,换一种读法照样能读出来。
实测一:项目里的 allow 规则,可能根本没生效
同一条 "allow": ["Bash(rm *)"],同一条 rm build/out.txt,分别放进三个文件:

放在 .claude/settings.json 时,stderr 打出 Ignoring 1 permissions.allow entry from .claude/settings.json: this workspace has not been trusted,命令被拒,文件还在。放进没被 git 跟踪的 settings.local.json 或用户级文件,命令直接执行。把这个目录在配置里标成已信任(hasTrustDialogAccepted: true)之后,项目文件里的规则才生效。
官方的解释是:allow 规则是在给权限,所以项目文件里的 allow 要等你在交互界面接受「信任此文件夹」后才算数;claude -p 和 SDK 从不弹这个对话框。deny 和 ask 只会收紧,不受信任限制。在 CI 里跑 claude -p,又指望仓库里的 allow 放行命令,命令会全部被拒,原因只打在 stderr 里,很容易漏看。
实测二:deny 能拦住什么,拦不住什么

| 设置 | 命令 | 结果 |
|---|---|---|
allow Bash(git *) + deny Bash(git push *) | git push origin main | 拒绝 |
git -C . push origin main | 执行了(只是没有远端,git 自己报错) | |
allow Bash + deny Bash(rm *) | timeout 5 rm …、FOO=1 rm … | 拒绝 |
/bin/rm …、bash -c "rm …" | 拒绝 | |
find build -name out.txt -delete | 执行,文件被删 | |
python3 -c "import os; os.remove(…)" | 执行,文件被删 | |
allow Bash(python3 *) + deny Read(./.env) | Read 工具读 .env、cat .env | 拒绝 |
python3 -c "print(open('.env').read())"、grep -r SECRET . | 执行,输出了 SECRET=abc123 |
官方文档的例子表里,/bin/rm 和 bash -c 属于 deny 规则「不保证拦住」的写法,我们这个版本实测拦住了,但别指望它。文档的原话是:Bash 规则只匹配命令文本,不是围着程序的安全边界。find -delete、Python 脚本、git -C 这些换个写法的调用都能绕过去。
想要硬边界有两条路:一是开沙箱,在操作系统层限制 Bash 的文件和网络访问;二是用 PreToolUse 钩子自己检查完整命令,写法见站内的 Claude Code Hooks 实战。
实测三:几个跟直觉不一样的地方
- 组合命令要每段都放行。只 allow
Bash(touch *)时,touch a.txt能跑;touch b.txt && rm build/out.txt整条被拒,b.txt 也没建出来。 - 命令行放行压不过 deny。用户级 deny
Bash(rm *),启动时再加--allowedTools "Bash(rm *)",照样被拒。 - ask 在 -p 里等于 deny。allow
Bash再 askBash(rm *),无人值守时 rm 直接被拒。 - acceptEdits 会放行 rm。默认模式下 Write 新文件、
mkdir都要确认;--permission-mode acceptEdits下这两个加上rm build/out.txt全部直接执行。官方写明这个模式自动放行工作目录内的 mkdir、touch、rm、rmdir、mv、cp、sed,名字里的「编辑」包括删文件。
常见坑
- 团队仓库里的 allow 没生效。先看 stderr 有没有「has not been trusted」。交互模式下接受一次信任对话框;CI 里用
--settings传一份你自己控制的配置,别靠仓库文件放行。 - 以为 deny 了 .env 就安全了。同时 allow 了
Bash(python3 *)或Bash时,脚本照样能读。敏感文件要配沙箱的文件系统限制,或者干脆别放在 Agent 的工作目录里。 - JSON 写错,整个文件失效。设置文件是严格 JSON,不能写
//注释和结尾逗号。交互启动时会弹 Settings Error;单条规则写错只出 Settings Warning,跳过那条。拿不准就跑claude doctor。 - bypassPermissions 被人顺手打开。在托管设置或你自己的用户设置里加
"disableBypassPermissionsMode": "disable",这个模式就开不了。
完成标准
/status里能看到你改的那个文件被加载;/permissions里能看到每条规则和来源。- 用
claude -p … --output-format stream-json --verbose跑一次,permission_denials里出现你预期被拒的调用,stderr 没有「has not been trusted」。 - 敏感路径同时有
Readdeny 和沙箱或钩子兜底,不是只靠一条规则。
想先补概念,可以看 Claude Code 上手篇;接 MCP 工具时规则写法是 mcp__服务器名__*,见 MCP Tasks 实测。
最后
今天就在 ~/.claude/settings.json 里加三条 deny:Read(./.env)、Read(./.env.*)、Bash(git push *),然后用 /permissions 确认它们已经生效。