DeepDojo.AI发布 2026-11-12English

Skill 设了只读,为什么还能改文件?

Claude CodeAI 编程

你给一个只读审查 Skill 写了 allowed-tools: [Read],心想这下它只能读文件了。结果调用时,它通过 Bash 改写了一个配置文件。你回去翻 frontmatter,Read 写得好好的,没有任何拼写错误。

问题在于 allowed-tools 的意思和直觉相反。它的作用是“这些工具在本轮可以免确认使用”,并没有说“只能用这些工具”。其他工具仍在,只是照常走权限确认;如果会话本身已经放宽了权限,它们就能直接运行。

要给 Skill 划出真正的边界,得把三层设置分开看:

  • allowed-tools:本轮哪些调用可以免确认;
  • disallowed-tools:本轮从工具列表里移除哪些工具;
  • permissions.deny:普通对话和所有 Skill 都要遵守的长期底线。

只读要三层一起配。引入别人写的 Skill 时更要多看几处:它放行了哪些工具,正文里有没有会自动执行的 Shell 命令,附带的脚本访问什么,注册了哪些 Hook。下面用一个只读审查 Skill 逐项说明。

01 一个只读审查 Skill 的 frontmatter

SKILL.md 开头用两个 --- 包住 YAML 配置,这部分叫 frontmatter。下面这个审查 Skill 用它限定了工具能力:

---
name: Review Changes
description: 审查当前分支相对指定基线的代码改动,定位正确性、安全性和回归风险。用户要求审查 diff、提交或分支时使用。
argument-hint: "<base-ref>"
arguments:
  - base
disable-model-invocation: true
context: fork
agent: general-purpose
allowed-tools:
  - Read
  - Grep
  - Glob
  - Bash(git status)
  - Bash(git diff *)
disallowed-tools:
  - Edit
  - Write
---

审查当前工作区相对 `$base` 的改动。只读分析,不修改文件。

1. 缺少 `$base`、基线无法解析为有效 Git 引用,或不在 Git 仓库中时,说明原因并停止,不猜测基线。
2. 运行 `git status`,确认工作区状态;查看相对 `$base` 的 diff,并读取受影响实现和测试。未跟踪文件不在普通 diff 中,应单独列出未审查范围。
3. 只报告有实际行为影响且能够定位证据的问题。
4. 每条发现包含严重程度、文件与行号、触发条件、影响和最小修复建议。
5. 没有发现问题时,说明检查范围和尚未验证的风险。

最终只返回审查结果,不创建提交,不修改代码。

这里同时用到了放行和移除两种字段。context: fork 会把审查交给独立的子代理执行,交接时要注意什么,见怎样给子代理交代任务、核对结论。

02 三种工具设置各管一层

allowed-tools 提供当前调用轮次的免确认授权,不会移除其他工具。allowed-tools: [Read] 本身不能建立只读边界,还要区分:

allowed-tools:当前调用轮次可以免确认的调用
disallowed-tools:当前调用轮次从工具列表强行移除的工具
permissions.deny:普通对话和各个 Skill 都要遵守的长期底线拒绝

Claude Code 的 Skills 文档对 allowed-tools 的定义是:在调用这个 Skill 的那一轮里,Claude 可以不经确认使用的工具;你发出下一条消息时,这项授权就清除了。disallowed-tools 则是在 Skill 生效期间,从 Claude 可用的工具里移除的工具。

再看“只读审查”这个角色。只给 Bash(git diff *),能解释为什么需要这项权限;直接给整个 Bash,就多出了许多无关能力。即使移除 Edit、Write,Bash 仍可能写文件。手动调用只决定何时开始,真正的命令范围还要由会话权限约束,Bash 规则怎样写窄,见Claude Code 权限白名单的五个常见漏洞。

开头那个 allowed-tools: [Read] 的 Skill,缺的就是后两层:没有移除写入工具,也没有在会话权限里限制 Bash。

03 引入外部 Skill 先查四处

引入外部 Skill,先查四处:

  • frontmatter 放行了哪些 Bash、MCP 和写入工具。
  • 正文有没有 Shell 预处理。
  • scripts/ 会访问什么。
  • Skill Hooks 在哪些事件运行。

即使尚未信任工作区,Skill 调用时的 allowed-tools 仍可能生效,这部分授权也要单独核对。官方文档专门提醒了这一点:工作区信任不管这个字段,哪怕在一个从未信任过的文件夹里用 -p 运行,项目 Skill 的 allowed-tools 也会生效;Skill 可以给自己授予很宽的工具权限,所以在一个仓库里运行 Claude Code 之前,先审查仓库里 Skill 的 allowed-tools。

04 动态命令在模型看到正文前就执行

Skill 支持在正文里写 !`git status --short` 这样的动态命令,加载时先运行,再把输出放进正文。这些命令会在模型看到 Skill 前执行,模型无法临时决定跳过。所以审查外部 Skill 时,动态命令要和脚本一样认真看。

官方文档补充了几条执行细节:

  • 动态命令同样要过你的权限规则,命中拒绝规则的命令会让整次调用中止;
  • 任何一条命令失败,整次调用都会中止,Claude 看不到这次的 Skill 内容;
  • 组织可以用 disableSkillShellExecution 关闭这类预处理,命令会被替换成一段“已被策略禁用”的提示,不再执行;按设置字段参考,托管设置里把它设为 true 后,其他配置里的 false 覆盖不了。

动态命令应只读、快速、输出有上限,并避开密钥和无关数据。共享 Skill 应写清所需的 Shell、命令和本机环境条件,以及执行失败时的处理方式。动态注入该放什么、放多少,见SKILL.md 太长时,拆成按需读取的文件。

05 paths 和 Hooks 的边界

Skill 的 paths 限定自动适用范围,不限制文件访问。例如 paths: ["db/**"] 不能保证迁移 Skill 只访问 db/。官方字段说明也只说它“限定 Skill 何时被激活”:设置后,Claude 只在处理匹配文件时自动加载它。实际读写仍由权限与工具边界控制。

Skill 的 Hooks 从调用后持续到会话结束,一轮回答结束并不会清除。官方文档的说法是,Skill 被调用时注册这些 Hook,之后在本次会话的其余时间里都会运行,包括 Skill 那一轮之后的轮次。只服务这个流程的检查,要写清何时不再适用;多个任务都要执行的控制,放项目 Settings。

依赖特定 Shell、模型、命令或工具时,要声明前提和失败行为,避免共享 Skill 在不同环境中继续执行不适用的步骤。

免确认、移除工具、长期拒绝是三层不同的东西。给 Skill 定只读边界时三层一起看,引入别人的 Skill 时把四处都查过,开头那个“只读却改了文件”的情况就有了明确的解释和修法。