DeepDojo.AI发布 2026-11-13更新 2026-11-13English

Claude Code 只读命令为什么还能改文件?

Claude CodeAI 编程

给 Claude Code 写自定义命令时,有一类需求特别常见:这个命令只看、不改。比如一个代码审查命令,你希望它读完 diff、给出意见就停手。

于是很多人在 allowed-tools 里只写 Read、Grep,再在正文里补一句“请只读,不要改代码”。可真正跑起来,它照样改了文件。

问题出在对这个字段的理解上。allowed-tools 是“免去弹窗询问”的快捷通道,用来预先批准列出的工具,并不会把其他工具关掉。如果会话本身允许自动编辑,模型发起的 Edit 请求照样能直接落盘,正文里那句“只读”只是一句提醒。

类似的误会还有两个。动态上下文里写着清理命令,正文写着“操作前先确认”,可清理命令在模型读到这句话之前就已经执行;一个叫 /push-all 的命令,名字就让人以为工作区里所有文件都可以推上去。

这几类问题有同一个解法:把三件事分开设定。

  • 谁有权启动这个流程;
  • 哪些调用可以跳过人工确认;
  • 哪些危险动作要彻底封住。

三件事各有各的开关;会推送、会部署的命令,还要在每次运行时做几项固定检查。

01 allowed-tools 只负责免确认

Claude Code 的 Skills 文档写得很清楚:allowed-tools 在调用这个 Skill 的那一轮里,让列出的工具不再弹窗询问;你发出下一条消息时,这项授权就失效了。它不限制可用的工具,每个工具都还能调用,没列出的工具继续走你的权限设置。

所以,想让命令从机制上只读,要用能“拿掉”工具的设置:

想做到什么用什么
这些调用不用每次确认allowed-tools
这个 Skill 运行期间不能用某些工具Skill 的 disallowed-tools 字段
所有任务里都禁止某类动作权限设置里的 deny 规则
告诉模型本次任务的职责边界提示词正文

allowed-tools 解决“免确认”;disallowed-tools 和全局 deny 规则负责“真拦截”;提示词正文让模型在获准的范围内明白本次任务该做什么。只在正文里写“只读”,从机制上并没有封住写操作。

还有一点容易被忽略:项目里的 Skill 也会给自己授权。官方提醒,仓库里提交的 Skill 可以通过 allowed-tools 给自己较宽的工具权限,在一个陌生仓库里运行 Claude Code 之前,先看看这些 Skill 写了什么。

02 谁能启动,由 disable-model-invocation 决定

一旦涉及部署发布、推送远端、批量删除或者修改权限,首先要把启动开关握在自己手里。disable-model-invocation: true 让 Claude 不会自动加载这个 Skill,只有人敲下斜杠命令才会执行。

示例:

---
name: deploy
description: 部署当前应用到生产环境
disable-model-invocation: true
allowed-tools: Bash(npm test *) Bash(npm run build *) Bash(git status *)
---

# Deploy

Before deployment:

1. Check git status.
2. Run tests.
3. Build the application.
4. Show a deployment summary.
5. Ask for explicit confirmation before any irreversible step.

这个示例只定义部署前检查,没有提供实际发布命令。要完成部署,还需明确目标环境、发布步骤和恢复办法;手动触发 Skill 也不等于确认了所有生产操作。

允许模型调用本地 Git 命令,和授权它往远端推送是两回事;由人手动敲命令启动,也省不掉对现场状态的事实核查。启动权、免确认和真拦截这几层拆清楚了,日后调用一旦出现偏差,你一眼就能看出是哪一层没设好。

03 注入命令在模型看到正文之前运行

动态上下文 !`command` 用起来很方便,但它的执行时间和正文完全不同。看一个错误写法:

## Cleanup
- !`rm -rf build/ && npm run build`

如果只顺着眼睛读 Markdown,你可能会觉得下面写着的“操作前先跟用户确认”能拦住那行删除命令。

顺着真实执行的时间线走,顺序是反过来的:动态上下文在提示词组装阶段就由 shell 执行,模型要等这些命令跑完、拿到输出之后,才看得到你写的那句“先确认”。

这条命令最后会不会真的跑,取决于权限设置。官方说明,注入命令执行前会逐条对照权限规则:命中 deny 规则就中止这次调用;不在 auto 模式时,只要检查结果不是“允许”,也会中止。反过来说,一旦你在 allowed-tools 或允许规则里放行了 rm,这行删除就会在你看到任何回复之前完成。

所以要把动作的先后顺序理顺:

  • 只读的状态采集,比如 git status、git diff,才放进注入区;
  • 带清理、构建、移动等副作用的操作,写进正文的任务步骤,等模型分析过、你明确批准之后再执行;
  • 有副作用的命令只在步骤里按需放行,注入区里不放。

04 MCP Prompt 不带工具权限

MCP server 也可以把 prompt 暴露成命令,形式通常是 /mcp__<server-name>__<prompt-name> [arguments],例如:

/mcp__project__list_tasks
/mcp__project__review_item 456
/mcp__tracker__create_issue "Bug title" high

MCP Prompt 提供提示内容;它可能引导 Claude 调用工具,但它本身不是工具,也不会自动授予工具权限。下面的规则控制 MCP 工具调用:

权限写法含义
mcp__project允许访问整个 MCP server
mcp__project__*允许访问该 server 的全部工具
mcp__project__get_item只允许访问某个具体工具

命令调用外部工具前,先确认实际需要哪些工具,再授予最小权限。这样既能完成任务,也能缩小误操作范围。插件提供的命令同理,团队使用时保留 /plugin-name:command-name 这样的命名空间,成员就能立刻看出命令来源。

05 一键推送命令每次要检查什么

在常见的命令里,/push-all 要再多想一步:结果会进入远端仓库,选错文件或分支,影响就越出了本机。先分析变更、查敏感文件与大文件、确认分支,再展示本次摘要。用户确认的应是这组看得见的改动,命令则负责让这些检查每次都发生。

一个安全的 /push-all 至少要检查:

  • .env*、*.key、*.pem、credentials.json、secrets.yaml 等敏感文件。
  • API key、secret、token 是否出现真实值;占位符可以保留,真实密钥要拦下。
  • 是否存在超过 10MB 且未使用大文件管理的文件。
  • 是否误包含 node_modules/、dist/、build/、__pycache__/、.venv/ 等构建或缓存产物。
  • 是否在受保护分支上操作。
  • 是否存在合并冲突。

如果还分不清哪些改动属于本次工作,就先停在暂存之前。选择性暂存或交互式暂存能帮人看清范围,再由用户确认。

把带密钥的临时文件顺手推上公共仓库,是“全自动”命令最常见的事故。越是“一键搞定”的命令,越需要每次都跑一遍检查;入口叫 push-all,授权范围仍然只是这次看过、确认过的那组文件。

写完之后,再用两种现象检查这个命令:

现象调整方法
读 diff 却给了整个 Shell,查文档却能提交推送按实际工作收紧工具权限
模板写了真实 token、密钥、密码或私有服务凭据改用受控凭据方式,文件中不存秘密

命令的基本写法、参数和执行链排查,见 把常用请求写成 Claude Code 斜杠命令。需要在动作发生前真正拦下危险命令时,那是 Hook 的工作,可以接着看 Claude Code Hook 该挂在哪个事件上。

回到开头那个“只读”命令:启动权交给 disable-model-invocation,免确认交给 allowed-tools,封住写操作交给 disallowed-tools 或 deny 规则。三件事各设各的,命令才真的照你想的那样运行。