用了 Claude Code 一段时间,有件事让我很头疼。
每次让它帮我写完一段代码,它要问我一句"能不能保存文件"。跑个测试,又问一句。创建个目录,再问一句。本来是来解放双手的,结果变成了另一种形式的"人肉确认机器"。
直到我彻底搞懂了它的权限配置系统,这个问题才真正解决。
为什么 Claude Code 会不断打断你
Claude Code 的设计哲学是谨慎优先。它默认对几乎所有文件操作、终端命令都会暂停下来问你一句,因为 AI 执行出错的代价,可能比慢一点高得多。
这个设计是对的。
但问题在于,"谨慎"和"高频打断"之间有巨大的差距。git status 这种只读命令,swift build 这种本地构建,根本不需要每次确认。真正需要谨慎的,是 git push、npm install、以及任何涉及破坏性的操作。
Claude Code 提供了一套精细的权限配置机制,让你自己来划这条线。
权限系统的三个层级
配置文件有三层,从全局到局部依次覆盖:
~/.claude/settings.json ← 全局,所有项目生效/.claude/settings.json ← 项目级,可提交到 git/.claude/settings.local.json ← 项目本地,加入 .gitignore
优先级:项目本地 > 项目级 > 全局。权限规则是合并生效的,但 deny 永远是最高优先级,任何地方的 deny 规则都不会被覆盖。
实际用法很清晰:
- • 全局配置放通用规则(git 操作、构建工具、读文件)
- • 项目配置放这个项目专属的规则(iOS 项目放 xcodebuild,前端项目放 vite)
- • 本地配置放个人习惯、或者不适合提交的临时规则
权限配置的结构
settings.json 里的 permissions 字段,包含三个数组:
{ "permissions": { "allow": [], // 自动执行,不询问 "ask": [], // 执行前暂停,等你确认 "deny": [] // 完全禁止,拒绝执行 }}
规则格式是 操作类型(匹配模式),支持通配符:
| 操作类型 | 说明 | 示例 |
|---|---|---|
| Bash | 终端命令 | Bash(git status) |
| Edit | 修改已有文件 | Edit(src/**/*.ts) |
| Write | 创建新文件 | Write(src/**/*.tsx) |
| Read | 读取文件 | Read(README.md) |
| WebFetch | 抓取网页 | WebFetch(domain:github.com) |
冒号在 Bash 规则里有特殊含义:Bash(git log:*) 表示允许 git log 加任意参数,而 Bash(git log) 只允许不带参数的裸命令。这个细节很容易忽略,但影响很大。
一份真正好用的全局配置
下面这份配置,是我在 iOS + Vue/TypeScript 的开发栈上磨合出来的版本。
核心策略:读操作全放行,源码编辑按路径放行,写操作和构建命令按风险分级,破坏性操作一律 deny。
{ "permissions": { "allow": [ "Bash(git status)", "Bash(git log:*)", "Bash(git diff:*)", "Bash(git add:*)", "Bash(git stash:*)", "Bash(git branch:*)", "Bash(git switch:*)", "Bash(git checkout:*)", "Bash(swift build:*)", "Bash(swift test:*)", "Bash(swift run:*)", "Bash(xcodebuild:*)", "Bash(npm test:*)", "Bash(npm run lint:*)", "Bash(npm run dev:*)", "Bash(npm run typecheck:*)", "Bash(npx tsc:*)", "Bash(bun run:*)", "Bash(bun test:*)", "Bash(node:*)", "Bash(python3:*)", "Bash(mkdir -p:*)", "Bash(cp:*)", "Bash(mv:*)", "Bash(cat:*)", "Bash(echo:*)", "Bash(ls:*)", "Bash(find:*)", "Bash(grep:*)", "Bash(wc:*)", "Bash(which:*)", "Bash(gh api:*)", "Bash(gh repo:*)", "Bash(xcodegen generate:*)", "WebSearch", "WebFetch(domain:developer.apple.com)", "WebFetch(domain:developers.figma.com)", "WebFetch(domain:github.com)", "WebFetch(domain:raw.githubusercontent.com)", "Read(**)", "Edit(src/**)", "Edit(Sources/**)", "Edit(Tests/**)", "Edit(tests/**)", "Write(src/**)", "Write(Sources/**)", "Write(Tests/**)" ], "ask": [ "Bash(git commit:*)", "Bash(git push:*)", "Bash(git merge:*)", "Bash(git rebase:*)", "Bash(npm install:*)", "Bash(npm run build:*)", "Bash(pod install:*)", "Bash(brew install:*)", "Edit(package.json)", "Edit(Package.swift)", "Edit(Podfile)", "Edit(tsconfig.*)", "Edit(vite.config.*)", "Write(**)" ], "deny": [ "Bash(rm -rf:*)", "Bash(sudo:*)", "Bash(curl * | *)", "Bash(wget * | *)", "Write(/Users/changyou/.ssh/*)", "Edit(.env.production)", "Edit(.env.prod)", "Read(.env.production)" ] }}
把这份内容写入 ~/.claude/settings.json,对你机器上的所有项目立即生效。
哪些操作留在 ask,不是随意的
ask 里的每一条规则都有原因,不是懒得配置:
** / **:提交和推送是有历史记录的操作,AI 生成的 commit message 质量参差不齐,push 之后改起来麻烦,留一个确认节点是值得的。
** / **:安装依赖会修改 lock 文件,引入外部包,需要人工感知,不应该静默发生。
****:构建产物可能直接影响部署,尤其是 CI/CD 流程里,一个无意触发的 build 可能带来副作用。
配置文件(、、):这些文件的改动是全局性的,牵一发动全身,每次都看一眼没坏处。
deny 的几个设计细节
** 而不是 **
很多人会把 .env* 全部 deny,然后发现 .env.local 也编辑不了了,开发寸步难行。只 deny 生产环境的配置文件,本地开发的 .env.local 和 .env.development 正常放行。
** 用绝对路径**
JSON 文件里的 ~ 不会被展开,~/.ssh/* 这条规则其实是无效的。用绝对路径才能真正生效。
** 和 **
拦截的是管道执行模式——把下载内容直接 pipe 给 shell 执行,这是最经典的供应链攻击手法。单纯的 curl 下载文件不受影响。
配置之后的实际变化
一个典型的功能迭代循环,配置前后的体验差别非常明显:
配置前:写代码 → 问你能不能保存 → 你确认 → 跑测试 → 问你能不能执行 → 你确认 → 检查 lint → 问你能不能运行 → 你确认。一个来回三次打断,Claude Code 退化成了一个需要人肉看管的工具。
配置后:Claude Code 独立完成"修改代码 → 保存 → 跑测试 → lint 检查"这整个循环,只在你要 commit 的时候停下来让你看一眼。你喝杯咖啡回来,任务已经完成了大半。
这才是 agentic 工具该有的样子。
一个要记住的判断原则
在决定一条规则放 allow 还是 ask 时,问自己一个问题:
这个操作如果 AI 执行出错,修复成本有多高?
- • 读文件、搜索、查看目录:成本为零,无脑 allow
- • 编辑源码:可以 git 回滚,allow 没问题
- • 安装依赖、提交代码:有副作用,放 ask
- • 删除文件、执行 sudo、修改生产配置:不可逆,放 deny
权限配置不是一次性工作。随着项目演进,你会遇到新的工具、新的目录结构,规则也要跟着调整。每次遇到"这个操作好像不该每次都问我",就是更新配置的信号。
把 AI 工具真正用好,从来不是"开箱即用"的事。
给它一个合理的边界,它才能在那个边界里跑得更快。
2026.03.25 17:06 沪 · 赵巷
📌 声明:本文由 AI 辅助完成