主题
08.7 Skill 的文件读取规则与上下文占用
Skill 目录里放了 8 个文件,Claude 会读哪几个?答案比「全读」和「只读 SKILL.md」都更精确一点。
读完你能做什么:说清 Skill 目录里每类文件的读取时机,用五级强度的手段确保某个文件不被读取,并知道 Skill 正文进入上下文后的驻留规则。
一、只有一个文件是入口
SKILL.md 是唯一的入口文件,也是唯一被自动读取的文件。
一个 Skill 目录里的读取规则:
| 文件 | 是否自动读取 | 何时进入上下文 |
|---|---|---|
SKILL.md 的 YAML frontmatter | 是 | 会话启动时,进系统提示词(name + description) |
SKILL.md 的 markdown 正文 | 是 | Skill 被调用时,作为一条消息进入对话 |
同目录下的其他 .md | 否 | 只有当 Claude 决定读它时 |
scripts/ 下的脚本 | 否 | 代码永不进上下文,只有执行输出进 |
| 模板、示例、数据文件 | 否 | 只有当 Claude 决定读它时 |
README.md | 否 | 它不是入口,不会被当作 SKILL.md |
所以「Skill 文件夹里的每个 md 文件都会被读取吗」的答案是:不会。默认只有 SKILL.md 会。
官方对这一点的表述是:Claude 只在文件被引用时才访问它们。一个 Skill 可以捆绑几十个参考文件,未被读取的部分成本为零。
二、Claude 决定读一个文件的三个触发源
既然不会全读,那什么情况下会读某个文件?只有三种:
| 触发源 | 说明 | 你的控制力 |
|---|---|---|
SKILL.md 里引用了它 | 最主要的来源。正文里用相对链接指向 reference.md,Claude 就知道它存在、装了什么、什么时候该读 | 完全可控 |
| 任务需要,Claude 自主探索 | Claude 有 Glob / Grep / Read 工具。当它在排查问题、或觉得目录里可能有线索时,可能主动列目录并读文件 | 不完全可控 |
| 你显式让它读 | @ 提及、直接说文件名 | 完全可控 |
第二条是唯一的不确定性来源。这也是为什么下一节要分五个强度等级。
三、确保一个文件不被读取:五级强度
需求场景很常见:想在 Skill 目录里放一份给人类维护者看的说明(设计意图、变更历史、待办),但不希望它消耗上下文。
| 级别 | 手段 | 强度 | 代价 |
|---|---|---|---|
| L0 | 不在 SKILL.md 里引用它 | 弱(去掉主要触发源) | 零 |
| L1 | 用命名与位置表明用途 | 弱(叠加语义信号) | 零 |
| L2 | 在 SKILL.md 里显式写「不要读」 | 中(软指令) | 几十 token 常驻 |
| L3 | permissions.deny 里加 Read 规则 | 强(工具层硬拦截) | 配置一行 |
| L4 | PreToolUse hook 拦截 | 强,且可审计 | 写一个脚本 |
| L5 | 把文件放到 Skill 目录之外 | 完全保证 | 维护上分开放 |
实务上 L0 + L1 + L2 组合已经足够,涉及敏感内容再上 L3。
L0 + L1:约定层
text
my-skill/
├── SKILL.md # 唯一入口,只在这里引用需要 Claude 读的文件
├── reference.md # SKILL.md 里引用了 → Claude 需要时会读
├── scripts/
│ └── validate.py # 执行,代码不进上下文
└── MAINTAINERS.md # SKILL.md 里从不提及 → 默认不会被读README.md 与 MAINTAINERS.md 这类名字有额外好处:它们在语义上就表示「给人看的」,而 SKILL.md 才是给 Claude 的入口。
L2:在正文里下一条明确指令
在 SKILL.md 末尾加一小节,成本约 40 token:
markdown
## 读取原则
- 需要详细规范时读 `reference.md`,只读相关小节。
- `MAINTAINERS.md` 与 `CHANGELOG.md` 面向人类维护者,执行任务时不要读取。这比「不提它」更强:不提只是没给出理由,明确写出来相当于给了一条负向指令。
L3:权限规则硬拦截
在 ~/.claude/settings.json 或项目 .claude/settings.json 里:
json
{
"permissions": {
"deny": [
"Read(~/.claude/skills/my-skill/MAINTAINERS.md)",
"Read(~/.claude/skills/*/CHANGELOG.md)"
]
}
}路径用 gitignore 语法,四种前缀含义不同,写错是最常见的失败原因:
| 写法 | 含义 | 例子解析为 |
|---|---|---|
//path | 文件系统绝对路径 | Read(//Users/alice/secrets/**) → /Users/alice/secrets/** |
~/path | 从家目录开始 | Read(~/.claude/skills/x/NOTES.md) |
/path | 相对于配置文件所在位置,不是文件系统根 | 写在项目 settings.json 里 → <项目根>/path |
path 或 ./path | 相对于当前目录 | Read(*.env) → <cwd>/*.env |
三个必须知道的边界:
- 只有
Read(path)与Edit(path)规则会被文件权限检查采用。 写成Write(docs/**)、Glob(docs/**)会被接受但永远不被查询,并在启动时告警。用Edit(...)代替Write/NotebookEdit/MultiEdit,用Read(...)代替Glob。需 v2.1.210+。 Read的 deny 规则同时会挡住Edit(含在该路径新建文件)。Write与NotebookEdit不在覆盖范围内,需要彻底禁改就再加一条Edit规则。需 v2.1.208+。- 规则覆盖内置文件工具,以及 Claude Code 能识别的 bash 文件命令(
cat、head、tail、sed)。不覆盖任意子进程 —— 一个自己open()文件的 Python 脚本绕得过去。需要操作系统级别的强制,要启用沙箱。
L4:Hook 拦截并留痕
需要审计(谁试图读、什么时候)时用这一级:
json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Read",
"hooks": [
{ "type": "command", "command": "~/.claude/hooks/block-maintainer-docs.sh" }
]
}
]
}
}bash
#!/bin/bash
# ~/.claude/hooks/block-maintainer-docs.sh
input=$(cat)
path=$(echo "$input" | jq -r '.tool_input.file_path // ""')
case "$path" in
*/MAINTAINERS.md|*/CHANGELOG.md)
echo "$(date -Is) blocked: $path" >> ~/.claude/logs/blocked-reads.log
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"该文件面向人类维护者,不应进入上下文"}}'
;;
*)
echo '{}'
;;
esacbash
chmod +x ~/.claude/hooks/block-maintainer-docs.shHook 本身不占任何上下文,只有它的输出会进上下文。
L5:放到目录之外
唯一的完全保证。把维护文档放进仓库的 docs/skills/my-skill.md,Skill 目录里只留执行需要的东西。
代价是两处内容要同步维护,适合内容较多或确实敏感的场景。
四、目录扫描的边界
除了 SKILL.md,Claude Code 还会在 Skill 目录里主动看什么?
| 行为 | 范围 |
|---|---|
| 启动时的 Skill 发现 | 只解析 SKILL.md 的 frontmatter |
| 实时变更检测 | 只覆盖 SKILL.md 的文本内容。其他文件的改动不会被自动感知 |
| 目录是插件时 | 若目录下有 .claude-plugin/plugin.json,它会作为插件加载,hooks/、.mcp.json、agents/、output-styles/ 也会被识别(改动需 /reload-plugins 生效) |
| 符号链接 | 会跟随符号链接读取目标目录的 SKILL.md;同一目标从多个位置可达时只加载一次 |
所以:只要目录里没有 .claude-plugin/plugin.json,除 SKILL.md 外的文件在加载阶段完全不会被碰。
五、进入上下文之后:不会被释放
这是与「读取规则」同等重要的另一半。
Skill 被调用时,渲染后的
SKILL.md内容作为一条消息进入对话,并在本次会话的剩余时间里一直留在那里。Claude Code 不会在后续轮次重新读取该文件。
几条直接影响写法的规则:
| 规则 | 写法上的含义 |
|---|---|
| 正文常驻整个会话 | 每一行都是重复成本。说「做什么」,别说「为什么」 |
| Claude 不会重读文件 | 写成常驻约束(「本任务中始终…」),而不是一次性步骤(「现在请…」) |
| 重复调用会去重 | 渲染结果完全相同时只追加一句提示,不重复正文(需 v2.1.202+)。参数或动态注入变了则会追加完整副本 |
| 压缩后按配额重新附着 | 每个 Skill 保留前 5,000 token,所有 Skill 共享 25,000 token 总预算,从最近调用的开始填,超出的老 Skill 被整个丢弃 |
最后一条决定了排版:最重要的指令必须写在 SKILL.md 的开头,因为截断保留的是文件前部。
想让 Skill 用完即走,只有两条路:
yaml
# 路线一:在子代理里执行,内容从不进主上下文
---
name: deep-research
description: 深入调研一个主题并返回结论
context: fork
agent: Explore
---yaml
# 路线二:连 description 都不常驻,只有你手动 /deploy 时才加载
---
name: deploy
description: 把应用部署到生产环境
disable-model-invocation: true
---三种可见性组合的常驻成本:
| frontmatter | 你能调用 | Claude 能自动调用 | 常驻成本 |
|---|---|---|---|
| (默认) | 是 | 是 | description 常驻 |
disable-model-invocation: true | 是 | 否 | 零 |
user-invocable: false | 否 | 是 | description 常驻 |
六、一份推荐的目录布局
text
my-skill/
├── SKILL.md # 入口。前 5,000 token 放最重要的约束
├── reference.md # SKILL.md 引用,需要时才读
├── examples.md # SKILL.md 引用,需要时才读
├── templates/
│ └── endpoint.md # 模板,用到才读
├── scripts/
│ └── validate.py # 执行,代码不进上下文
└── MAINTAINERS.md # 不引用 + 在 SKILL.md 里声明不读 + 可选 deny 规则配套的 SKILL.md 收尾:
markdown
## 读取原则
- 需要完整字段规范时读 `reference.md`,只读用得上的小节。
- 需要写法参考时读 `examples.md`。
- 校验一律跑 `${CLAUDE_SKILL_DIR}/scripts/validate.py`,不要手写等价逻辑。
- `MAINTAINERS.md` 面向人类维护者,执行任务时不要读取。