本教程带你从零开发一个生产级 Claude Code 自定义 Skill——env-scan,它能扫描任意项目代码、自动提取所有环境变量引用,并生成 .env.example 模板与变量清单。通过这个真实案例,你将掌握 Skill 的渐进式披露(Progressive Disclosure)机制、description 触发工程、支持文件分层、动态上下文注入与 frontmatter 精准控场。适用人群:已跑通 Claude Code 基础用法、想把重复提示词沉淀为可复用能力的中级开发者。
paths 限定与 ${CLAUDE_PROJECT_DIR} 替换,低版本可省略)git 与 grep(或 rg)的 macOS/Linux 机器,Windows 用户需启用 CLAUDE_CODE_USE_POWERSHELL_TOOL=1process.env.*、Python 的 os.environ/os.getenv 均可)用于联调在动手前,必须想清楚一件事:Skill 不是一段常驻提示词,而是一套按需加载的能力包。
这与 CLAUDE.md 有本质区别。CLAUDE.md 的每一行在每次对话都会被读入上下文,属于"固定成本";而 Skill 采用分层披露:
name 与 description 进入技能清单,被 Claude 用来判断"现在要不要用我"。这一层在每次会话都加载,所以必须极简——官方把 description + when_to_use 合计截断在 1,536 个字符。/skill-name 时,SKILL.md 的正文才被读入。reference.md、scripts/ 等支持文件,只在 Claude 真正需要细节时才被读取。一句话记住设计原则:首层要"勾得到",正文要"压得紧",深水区要"藏得住"。 长篇参考资料不该污染每次对话的上下文。
常见问题:我把详细规则全塞进
description,为什么 Skill 反而不触发? 解决:description超过 1,536 字符会被截断,关键触发词可能被切掉。把"做什么+何时用"压在前两句,细节挪到正文。
Skill 能否被自动调用,90% 取决于 description 写得好不好。写之前,先回答三个问题:
把答案浓缩成一句话描述,把触发短语放进 when_to_use。env-scan 的定位草稿:
description: Scans a project for environment variable references and generates a .env.example template with a variable inventory. Use when the user asks what env vars they need, wants a .env template, or onboards to a codebase.
when_to_use: 触发短语如 "需要哪些环境变量" "生成 .env 模板" "项目要配什么变量" "onboarding env"
注意 description 我刻意用英文写主句——技能清单匹配对英文语义更稳;when_to_use 里塞中文触发短语,能提升中文场景下的命中率。两种语言混用是实战中常见的取巧手法。
Skill 的存放位置决定它的作用域。本教程做一个个人级 Skill(跨所有项目可用):
mkdir -p ~/.claude/skills/env-scan/scripts
mkdir -p ~/.claude/skills/env-scan/examples
得到如下结构(这是官方推荐的多文件布局):
env-scan/
├── SKILL.md # 入口指令(必需)
├── reference.md # 各语言提取规则详解(按需加载)
├── examples/
│ └── sample-output.md # 期望输出样例
└── scripts/
└── scan.sh # 实际执行扫描的脚本
官方文档明确:SKILL.md 建议控制在 500 行以内,详细参考材料拆到同级文件,由正文里写链接告诉 Claude "什么时候去读它"。这就是渐进式披露的落地。
打开 ~/.claude/skills/env-scan/SKILL.md,写入下面的内容。注意 frontmatter 与正文之间用 --- 分隔:
---
name: env-scan
description: Scans a project for environment variable references and generates a .env.example template with a variable inventory. Use when the user asks what env vars they need, wants a .env template, or onboards to a codebase.
allowed-tools: Bash(grep:*), Bash(rg:*), Read, Write
---
# 环境变量扫描
## 目标
找出当前项目中所有环境变量引用,产出:
1. 一份按"变量名 → 用途推断 → 来源文件"组织的清单(Markdown 表格)
2. 一个可直接使用的 `.env.example`,每行带注释占位
## 输入采集
先执行下面这行(动态上下文注入),把扫描结果内联进上下文:
!`${CLAUDE_SKILL_DIR}/scripts/scan.sh`
## 处理规则
- 变量名去重,按字母序排列
- 同名变量出现在多文件时,合并到一行,来源用逗号分隔
- 用途从最近的赋值/注释/上下文推断,不确定时标注 `(unknown)`
- 标准变量(PORT、NODE_ENV、DATABASE_URL)给出推荐默认值
## 输出
- 在仓库根写入 `.env.example`(已存在则先备份为 `.env.example.bak`)
- 在回复中粘贴清单表格
- 对任何看起来像密钥的变量(含 KEY/SECRET/TOKEN/PASSWORD)额外提醒"勿提交真实值"
## 深度参考
- 各语言的精确提取正则见 [reference.md](reference.md)
- 期望输出格式见 [examples/sample-output.md](examples/sample-output.md)
这里有几个关键机制值得逐一点破:
!`command` 是动态上下文注入:Claude Code 会在把正文交给 Claude 之前先执行命令、用输出替换这一行。于是 Claude 拿到的不是"去跑脚本"的指令,而是"带着扫描结果直接开干"的现成上下文。这是 Skill 区别于普通提示词的最强武器。${CLAUDE_SKILL_DIR} 是字符串替换变量,解析为 Skill 所在目录,保证脚本路径不依赖当前工作目录。allowed-tools 把权限收紧到"只允许 grep/rg/Read/Write",既减少授权打断、又构成最小权限围栏。把"重而少用"的内容挪出 SKILL.md。先写 scripts/scan.sh(脚本是被执行而非被读入的,所以可以写得详尽):
#!/usr/bin/env bash
# 扫描常见环境变量引用模式,输出 "变量名<TAB>文件:行号"
set -euo pipefail
ROOT="${CLAUDE_PROJECT_DIR:-$PWD}"
cd "$ROOT"
# 用 ripgrep 优先,回落到 grep -rE
if command -v rg >/dev/null 2>&1; then
GREP="rg --no-heading -n"
else
GREP="grep -rnE"
fi
{
$GREP 'process\.env\.([A-Z0-9_]+)' -g '!node_modules' -g '!.git' . \
| sed -E 's/.*process\.env\.([A-Z0-9_]+).*/\1/' || true
$GREP 'os\.(environ|getenv)\(["\x27]?([A-Z0-9_]+)' -g '!.git' . \
| sed -E "s/.*(?:environ|getenv)[\"\x27]?([A-Z0-9_]+).*/\1/" || true
$GREP '\$\{?([A-Z][A-Z0-9_]*)\}? *[:=-]' -g '*.sh' -g '*.env*' -g 'Makefile' . \
| sed -E 's/.*\$\{?([A-Z][A-Z0-9_]+).*/\1/' || true
} | sort -u
再写一份精简的 reference.md,专门放"哪种语言用什么正则"的查表内容,供 Claude 在遇到生僻框架(比如 Go 的 os.LookupEnv、Java 的 System.getenv)时按需查阅:
# 环境变量提取规则速查
| 语言/运行时 | 匹配模式 | 示例 |
|---|---|---|
| Node.js | `process\.env\.([A-Z0-9_]+)` | `process.env.DATABASE_URL` |
| Python | `os\.(?:environ|getenv)\(["']?([A-Z0-9_]+)` | `os.getenv("API_KEY")` |
| Shell | `\$\{?([A-Z][A-Z0-9_]*)\}?` | `${HOME}` |
| Go | `os\.LookupEnv\("([A-Z0-9_]+)"\)` / `os\.Getenv` | `os.LookupEnv("PORT")` |
| Java/Kotlin | `System\.getenv\("([A-Z0-9_]+)"\)` | `System.getenv("JWT_SECRET")` |
| .NET | `Environment\.GetEnvironmentVariable\("([A-Z0-9_]+)"\)` | — |
## 边界情况
- 动态变量名(`process.env[dynamic]`)无法静态提取,报告为 `(dynamic)`
- 测试桩里的假变量(mock)容易误报,优先排除 `__tests__`、`*.spec.*`、`*.test.*`
- `.env.example` 本身也要被解析,作为"已知声明变量"的补充来源
最后放一个 examples/sample-output.md,给 Claude 一个"好答案长什么样"的锚点,这对输出稳定性帮助极大:
## 变量清单(节选)
| 变量名 | 推荐默认/用途 | 来源 |
|---|---|---|
| DATABASE_URL | postgresql://user:pass@host:5432/db | src/db.ts:12 |
| JWT_SECRET | ⚠️ 勿提交真实值,生成 64 位随机串 | src/auth.ts:8 |
| PORT | 3000 | src/server.ts:5, docker-compose.yml:7 |
| NODE_ENV | development \| production | package.json:9 |
## 生成的 .env.example
DATABASE_URL=postgresql://user:pass@host:5432/db
JWT_SECRET=replace-with-64-char-random-string
PORT=3000
NODE_ENV=development
回到 SKILL.md 的 frontmatter,按需加上控制字段。env-scan 已用了 allowed-tools,再补两个高频字段说明用法:
paths(v2.1.196+):限定 Skill 只在处理匹配文件时自动触发。例如做一个"只在前端项目跑"的 Skill,可写 paths: ["src/**/*.tsx", "package.json"]。env-scan 故意不设 paths——因为它要跨语言、跨项目通用,任何项目都可能需要它。disable-model-invocation: true:禁止 Claude 自动触发,只能你手动 /env-scan。适用于部署、发版这类"绝不能误触"的动作。env-scan 希望能自动触发,所以不设此项。user-invocable: false:从 / 菜单隐藏,纯做背景知识。本 Skill 要保留手动入口,所以也不设。判断口诀:想自动→不设 disable;想手动→设 disable;想隐形→设 user-invocable: false;想限场景→设 paths。
Skill 不需要重启 Claude Code,保存即生效(官方称为 Live Change Detection)。打开一个含环境变量的项目,启动 claude,按下面顺序验证:
方式 A:自动触发(最该测)
输入一句贴近 when_to_use 的自然语言:
我刚接手这个项目,需要配置哪些环境变量才能跑起来?
预期:Claude 自主加载 env-scan,执行扫描脚本,给出清单并生成 .env.example。如果没触发,问题几乎都在 description——回到第二步重写。
方式 B:手动调用
/env-scan
预期:直接执行,无需 Claude 判断相关性。这条一定能跑通,可用于隔离"是脚本坏了还是描述没写好"。
方式 C:带参数调用(若声明了 arguments)
若把 frontmatter 加上 arguments: [path],正文里用 $0 或 $path 引用,就能 /env-scan src/ 限定扫描范围。
常见问题:方式 A 不触发,方式 B 正常。 排查清单:①
description是否前两句就点明"做什么+何时用";② 是否被 1,536 字符截断;③when_to_use的触发短语是否贴合你真实的问法;④ 是否误设了paths把当前文件挡在门外;⑤ 项目里确实有环境变量吗(脚本空输出时 Claude 可能判为无关)。
写到能跑只是及格。要稳定,得做触发评测。Claude Code 内置 /skill-creator(bundled skill)支持为 Skill 跑 eval:准备一批"应该触发"和"不该触发"的问法,看命中率与误报率,再据此微调 description。
迭代重点永远是 description:把漏触发的真实问法补进 when_to_use,把误触发的场景用更精确的措辞排除。这是一个"写-测-改"的小循环,通常 2–3 轮就能收敛。
以一个用了 process.env 的 Express 项目为例。在项目根目录启动 Claude Code 后,输入"帮我生成环境变量模板"。完整时序如下:
description,决定加载该 SkillSKILL.md,遇到 !`${CLAUDE_SKILL_DIR}/scripts/scan.sh`,先执行脚本src/,输出 DATABASE_URL\nJWT_SECRET\nPORT\n...Write 写出 .env.example(发现已存在则备份)JWT_SECRET 给出"勿提交真实值"的安全提醒整个流程对你而言是一句话,对 Claude 而言是"加载能力→采数据→产出→落盘"的完整闭环。这正是 Skill 把"重复多步操作"压成"一句话指令"的价值。
flowchart TD
U[用户:帮我生成 env 模板] --> M{Claude 匹配 description?}
M -->|命中| L[加载 SKILL.md 正文]
M -->|未命中| N[不触发,普通对话]
L --> I[动态注入:执行 scan.sh]
I --> S[扫描结果内联进上下文]
S --> R{需要正则细节?}
R -->|是| D[读 reference.md 第三层]
R -->|否| G[直接生成清单]
D --> G
G --> W[Write 写出 .env.example]
W --> O[回复清单 + 安全提醒]
style M fill:#fef3c7,stroke:#d97706
style I fill:#dbeafe,stroke:#2563eb
style W fill:#dcfce7,stroke:#16a34a
flowchart LR
subgraph 第一层[常驻 · 每次会话加载]
A1[name] --- A2[description / when_to_use ≤1536 字符]
end
subgraph 第二层[按需 · 触发时加载]
B1[SKILL.md 正文]
end
subgraph 第三层[深度 · Claude 主动读取]
C1[reference.md] --- C2[examples/] --- C3[scripts/]
end
第一层 -->|相关/手动调用| 第二层
第二层 -->|需要细节| 第三层
style 第一层 fill:#eff6ff
style 第二层 fill:#fef9c3
style 第三层 fill:#f3e8ff
当 env-scan 稳定后,你可能想分发给团队。两种途径:
.claude/skills/env-scan/ 整个目录提交,团队成员 clone 后自动可用。注意 .claude/skills/ 是项目级作用域,仅对该项目生效。.claude-plugin/plugin.json,它就升级为可挂载 agents、hooks、MCP 服务器的插件包,通过 marketplace 分发,并用 plugin-name:skill-name 命名空间避免冲突。分发前务必检查 scripts/ 里没有硬编码的本地路径,统一用 ${CLAUDE_SKILL_DIR} 与 ${CLAUDE_PROJECT_DIR} 保持可移植。
/skill-creator,引导式创建并跑触发评测[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。
