Claude Code 权限系统到底是怎么工作的

Rye Articles 2026-08-14T01:02:49.242128

实现说明

本文基于 2026 年年中观察到的 Claude Code 行为、本地文件布局以及插件源码整理而成。这些属于实现细节,会随版本变化。文中在合适的地方会引用 Codex CLI 做对比。

当你看到 Claude Code 询问“是否允许此工具调用?”并点击“批准”时,提示背后其实还有几件事在同时发生:静态规则、插件钩子、模式匹配,以及一些会随着你批准的操作越来越多而不断增多的本地文件。

这很重要,因为 Claude Code 不只是一个聊天窗口。它会在你的工作站上读取文件、写代码、执行 shell 命令、发起网络请求、派生子代理,还能管理各种任务。它用的是你的凭据和你的 shell 环境。权限系统就是模型请求与机器实际执行之间的主要闸门。

三种权限决定

每次工具调用都会归入以下三类之一:

这些决定由多个来源按优先级顺序综合评估;任何来源的“拒绝”都会覆盖其他来源的“允许”。当没有规则匹配时,默认行为取决于当前权限模式。

权限规则保存在哪里

Claude Code 会从两个位置读取权限规则,并合并使用:

全局用户设置,位于 ~/.claude/settings.json

{
  "permissions": {
    "allow": [
      "Bash(cargo check)",
      "Bash(cargo build --release)",
      "Bash(cargo run:*)",
      "Bash(cargo build:*)"
    ],
    "deny": [],
    "ask": [],
    "defaultMode": "default"
  }
}

项目级设置,位于工作目录下的 .claude/settings.local.json

{
  "permissions": {
    "allow": [
      "Bash(npm run:*)",
      "Bash(npx tsc:*)",
      "Bash(bun add:*)",
      "Read(/mnt/disks/data-disk/code/rye/web/**)",
      "WebFetch(domain:github.com)"
    ]
  }
}

针对同一条规则模式,项目设置会覆盖全局设置。两个文件使用相同的规则格式。

规则模式语法

权限规则遵循 ToolName(pattern) 的格式,其中 pattern 的具体写法取决于工具:

Bash(cargo check)           # Exact command match
Bash(cargo:*)               # Prefix match: any cargo subcommand
Bash(npm run:*)             # Prefix: npm run with any arguments
Read(/repo/**)              # Recursive glob: any file under /repo
WebFetch(domain:github.com) # Domain restriction for web fetches

Bash 模式末尾的 * 通配符用来匹配任意剩余参数;ReadWrite 模式中的 ** 表示递归匹配任意层级;限定域名的 WebFetch 规则则用来限制助手可以访问哪些主机。

当你以交互方式批准一次工具调用时,Claude Code 可能会询问是否要「始终允许」。如果同意,新规则就会追加到 settings.local.json 中。时间一长,这个文件会越来越大,记录下你在这个项目里批准过的所有操作。

Hook 系统

除了静态规则,Claude Code 还通过插件提供了一套 Hook(钩子)系统。Hook 本质上是 shell 命令,它们会在工具调用生命周期的特定事件触发时执行:

PreToolUse 钩子会把工具名称和输入参数以 JSON 格式通过标准输入(stdin)传给它:

{
  "tool_name": "Bash",
  "tool_input": {
    "command": "rm -rf /tmp/build-artifacts",
    "timeout": 120000
  },
  "hook_event_name": "PreToolUse"
}

钩子随后可以返回一个权限决定:

{
  "hookSpecificOutput": {
    "permissionDecision": "deny"
  },
  "systemMessage": "Recursive deletion blocked by policy."
}

Hook 在插件配置文件中注册,以子进程方式运行。超时时间一般很短(10 秒)。如果某个 Hook 报错或超时,系统会采取「失败即放行」的策略——工具调用照常执行,就像这个 Hook 不存在一样。

Hookify:用 Markdown 写规则

官方的 hookify 插件在此基础上扩展出了一套规则引擎:它使用带 YAML frontmatter 的 Markdown 文件来定义规则:

---
name: block-dangerous-rm
enabled: true
event: bash
action: block
pattern: rm\s+-rf
---

Dangerous recursive deletion detected.

更复杂的规则支持多个条件同时判断:

---
name: block-env-secrets
enabled: true
event: file
action: block
conditions:
  - field: file_path
    operator: regex_match
    pattern: \.env$|credentials
  - field: new_text
    operator: contains
    pattern: API_KEY
---

这类规则以 .claude/hookify.*.local.md 文件的形式存放在项目目录中,支持 regex_matchcontainsequalsstarts_withends_withnot_contains 等操作符,可以作用于 commandfile_pathnew_textold_textuser_prompt 这些字段。

作为本地机制,这套方案确实方便,但也有个明显的局限:它只在本地生效。团队没有一份集中的规则清单,规则变更没有内置的审计记录,你也无法轻易证明某条规则在某个会话中是否真的生效过。

批准是如何一点点堆积起来的

风险是慢慢显现的。

每当开发者在批准某个工具调用时点下「始终允许」(always allow),项目权限文件里就会追加一条新规则。几周下来,这个文件会自然地不断膨胀:

{
  "permissions": {
    "allow": [
      "Bash(cargo:*)",
      "Bash(sqlite3:*)",
      "Bash(npm run:*)",
      "Bash(npx tsc:*)",
      "Bash(bun add:*)",
      "Bash(wc:*)",
      "Bash(xargs ls:*)",
      "Bash(xargs cat:*)",
      "Read(/mnt/disks/data-disk/code/rye/web/**)",
      "WebFetch(domain:github.com)",
      "WebFetch(domain:claude.ai)"
    ]
  }

单看每一条批准,在当时可能都合情合理。但把它们放在一起,就成了一张不断扩大的白名单,而几乎没有人会去审查它。

对比 Codex

Codex CLI 也有同样的模式,只是换了一种表达方式。它的 ~/.codex/rules/default.rules 文件会持久化前缀匹配的批准规则:

prefix_rule(pattern=["rg"], decision="allow")
prefix_rule(pattern=["sed"], decision="allow")
prefix_rule(pattern=["cargo", "run", "--bin", "rye-worker"], decision="allow")
prefix_rule(pattern=["sqlite3"], decision="allow")
prefix_rule(pattern=["find"], decision="allow")
prefix_rule(pattern=["git", "add"], decision="allow")
prefix_rule(pattern=["git", "commit"], decision="allow")
prefix_rule(pattern=["docker", "build"], decision="allow")
prefix_rule(pattern=["docker", "run"], decision="allow")

实际使用中,这个文件会变得越来越乱。一次对数据库查询的批准,最终可能沉淀为一条跨多行的规则,里面带着连接字符串、SQL 和环境变量引用。下面是一个真实案例(已精简):

prefix_rule(pattern=["/usr/bin/zsh", "-lc",
  "set -a; source .env; set +a; psql \"$DATABASE_URL\" -X
   -v ON_ERROR_STOP=1 -c \"SELECT table_name, column_name
   FROM information_schema.columns
   WHERE table_schema = 'rye_proxy'...\""],
  decision="allow")

这条规则只靠一次点击就生成了,从此成为后续会话中允许执行的命令集的一部分。它引用了环境变量文件和数据库地址,而这一切唯一的记录,只是一个本地的纯文本文件。

权限系统不做的事

Claude Code 的权限系统对想快速推进的独立开发者来说足够好用:日常操作少了很多确认弹窗,危险动作依然会被拦截。但它解决不了下面这些团队层面的问题:

没有集中式的策略分发。 权限规则按用户、按项目各自存在每台机器上。组织层面没有任何机制,可以把「拒绝所有 docker run 命令」或「任何引用 .env 文件的 Bash 调用都必须先审批」这样的策略,推送到每位开发者的 Claude Code 安装中。

没有审批人留痕。 当一条规则出现在 settings.local.json 里时,你无法查证是谁批准的、何时批准的、当时响应的是哪次工具调用。文件里只有当前生效的规则集。

没有过期机制。 规则一旦添加就会一直存在,直到有人手动改动文件。没有限时审批,没有定期复查,权限也不会自动收窄。

跨工具没有统一格式。 Claude Code 使用 Bash(cargo:*) 这样的模式;Codex 使用 prefix_rule(pattern=["cargo"], decision="allow");Cursor 又是另一套格式。团队如果同时用多个 AI 编程助手,就要分别理解三套互不兼容的权限系统。

没有独立校验。 权限系统只记录它同意了什么,并不会独立确认工具调用实际做了什么。如果你放行了 Bash(npm run:*),而 npm run build 里的 postinstall 脚本借机窃取并外传环境变量——这一层,权限系统是看不到的。

失败开放的钩子:如果 PreToolUse 钩子超时或崩溃,对应的工具调用照样继续。这对开发效率来说是好事,但也意味着,除非助手之外还有别的机制强制它生效,基于钩子的策略只是建议性质的。

--dangerously-skip-permissions 标志

Claude Code 提供了 --dangerously-skip-permissions 标志,用来彻底关闭权限系统。这个名字是故意起得这么吓人的,官方文档也写明它只适用于 CI/CD 和自动化工作流——这些场景没法进行交互式批准。

当这个标志启用时,所有工具调用——文件读取、写入、shell 命令、抓取网页——都不会经过任何权限检查。剩下的唯一护栏只有模型自身的指令,以及宿主环境提供的那点沙箱隔离。

这个标志存在自有道理:CI 任务、批量重构或其他无人值守运行,确实没法跑去点确认。但代价太粗糙:你要么拿到交互式权限,要么完全没有权限。没有内建的中间地带——比如“不每次问人、但强制某套集中策略”的模式。

对团队的影响

对于个人开发者来说,Claude Code 的权限系统绰绰有余:它在做任何有风险的操作前先询问,记住你的偏好,日常操作又不碍事。

挑战出现在团队规模下:权限系统本身有用,但团队还需要在它之上再叠一层——集中策略分发、审批审计、跨工具规范化、对实际发生的操作做独立核查,以及在无人值守运行中仍然有效的强制机制。

查看原文