<!-- title: 造配置对账Skill：扫出缺失的环境变量 -->

# Claude Code Skills 开发实战：从零造一个配置对账 Skill

> 本文是 Claude Code 教程系列的第 2 轮，聚焦 **Skills 开发实战**。我们将从零构建一个名为 `env-reconcile` 的自定义 Skill，它扫描代码里所有环境变量引用，与 `.env` / `.env.example` 自动对账，找出"代码用了但没声明"的高危变量。

## 概述

学完本教程，你将独立完成一个**脚本型 Skill（Script Skill）**：理解 `SKILL.md` 的 frontmatter 字段、把核心逻辑下沉为 Python 脚本、用 `${CLAUDE_SKILL_DIR}` 跨路径引用、用 `!` 动态上下文注入把扫描报告直接喂给模型、用 `allowed-tools` 免审批运行脚本，并掌握渐进式披露（progressive disclosure）与触发准确率调试。适用人群：已跑通 Claude Code、想从"用现成 Skill"升级到"造自己的 Skill"的开发者，前端 / 后端 / 全栈 / DevOps 均可受益。

## 前置条件

- 已安装 Claude Code（v2.1.196 以上可体验 `paths` 字段；v2.1.202 以上可体验 Skill 内容去重），低版本核心功能也兼容；
- 本机有 `python3`（脚本仅用标准库，无需 `pip install`）；
- 一个真实工程目录（Node/TS、Python、Go、Ruby 任一），用来跑实战案例；
- 了解 YAML 基本语法。

---

## 第一步：先想清楚"为什么要做成 Skill"

很多人造 Skill 的第一反应是"把一段 prompt 存成文件"。但 Skill 真正的价值在于**可复用、可触发、可下沉**。判断一个需求该不该做成 Skill，问自己三个问题：

1. **你有没有反复粘贴同一段指令？** 比如"帮我把代码里所有 `process.env.X` 找出来，跟 `.env.example` 对一下"——这就是典型的 Skill 候选。
2. **这段逻辑里有没有"重活"？** 纯文本指令适合 Reference Skill；要扫几万行代码、要解析 AST、要画图，就值得下沉成脚本，做成 Script Skill。
3. **它是"知识"还是"动作"？** 知识类（编码规范）适合 Claude 自动加载；动作类（部署、提交、对账）更适合人工 `/` 触发，避免模型自作主张。

配置对账这个需求三条全中：要遍历工程（重活）、是动作（生产前自检）、且人人都会忘。它正是 Script Skill 的绝佳样本。

为什么不用一次性 prompt 解决？因为环境变量对账是**反复发生**的——每次加新功能、每次新人入职、每次部署报错都要做。固化成 Skill 后，一句 `/env-reconcile` 或一句"帮我检查环境变量有没有漏"，就拿到结构化报告。

## 第二步：搭好 Skill 目录结构

Claude Code 的 Skill 是一个**目录**，入口文件必须叫 `SKILL.md`。官方推荐的渐进式披露结构如下：

```text
env-reconcile/
├── SKILL.md          # 入口：frontmatter + 精简指令（必需）
├── reference.md      # 进阶说明：模式表、设计取舍（按需加载）
└── scripts/
    └── env-reconcile.py   # 真正干活的脚本（执行，而非读入上下文）
```

关键纪律：**`SKILL.md` 控制在 500 行以内**。一旦 Skill 被触发，它的正文会作为一条消息常驻整个会话上下文，每一行都是反复出现的 token 成本。所以"重活"放脚本、"详解"放 `reference.md`，`SKILL.md` 只放导航和最简指令。

创建目录（个人级，对所有项目生效）：

```bash
mkdir -p ~/.claude/skills/env-reconcile/scripts
```

三种存放位置决定生效范围，按需选择：

| 位置 | 路径 | 生效范围 |
|:--|:--|:--|
| 个人级 | `~/.claude/skills/<name>/SKILL.md` | 你的所有项目 |
| 项目级 | `.claude/skills/<name>/SKILL.md` | 仅当前项目（可提交到 Git 共享给团队） |
| 插件级 | `<plugin>/skills/<name>/SKILL.md` | 启用该插件处 |

配置对账这种"团队共性"需求，建议最终落到项目级 `.claude/skills/` 并提交版本库，让所有同事 `git pull` 后即用。

## 第三步：写 frontmatter——决定"何时触发、能否自动触发"

把以下内容存为 `~/.claude/skills/env-reconcile/SKILL.md` 的开头部分（YAML 必须夹在两个 `---` 之间）：

```yaml
---
name: env-reconcile
description: 对账代码中的环境变量引用与 .env 配置文件，扫出"代码引用了但 .env 缺失"和"声明了但代码没用"的变量。用于上线前自检、新人拉取项目核对配置、应用启动报错排查环境变量缺失。当用户提到环境变量对账、.env 缺失、配置漂移、启动报 undefined/KeyError、核对配置时使用。
allowed-tools: Bash(python3 *)
paths: "**/*.{js,jsx,ts,tsx,mjs,cjs,py,go,rb,vue,svelte}"
---
```

逐字段解释——这些都是官方 frontmatter 支持的字段：

- **`name`**：展示名，也是 `/` 菜单里的命令名（个人级/项目级时，命令名取自**目录名**而非此字段，所以这里写不写不影响 `/env-reconcile`）。`description`（推荐）告诉 Claude 这个 Skill 干什么、**何时该用**。Claude 靠这段文本来判断要不要自动加载，所以**把关键触发词放最前面**。注意：`description` + `when_to_use` 合计在 Skill 列表里会被截断到 **1536 字符**，且全局还有一个按上下文窗口 1% 缩放的预算，技能太多时低频 Skill 的描述会被裁掉。把"环境变量对账、.env 缺失、配置漂移、启动报错"这些用户会自然说出口的词放前面。`allowed-tools`：列出该 Skill 激活期间**免审批**可用的工具。这里只放 `Bash(python3 *)`，让 Claude 重跑脚本或加参数时不再每次问你要权限。注意它**不限制**可用工具集，只是"预批准"；未列出的工具仍受你的全局权限规则约束。**`paths`**（v2.1.196+）：只有当你在编辑匹配的文件时，Claude 才会自动加载该 Skill。配置对账只在有源码的项目里才有意义，加上这条可显著降低误触发。

如果你想让它**只接受手动 `/env-reconcile` 触发**（避免 Claude 自作主张乱跑），再加一行：

```yaml
disable-model-invocation: true
```

这是动作型 Skill 的标配——和 `/deploy`、`/commit` 同理。本教程为了演示"自动触发"，先不加它；最后会讲什么时候该加。

> **常见坑**：frontmatter YAML 写错（比如缩进、中文冒号）时，Claude Code 会用空元数据加载 Skill，`/env-reconcile` 仍能手动跑，但**自动触发会失效**（因为读不到 description）。排查用 `claude --debug` 启动，能看到解析错误。

## 第四步：写正文——用动态上下文注入把报告喂给模型

`SKILL.md` 正文（接在 frontmatter 之后）：

````markdown
# 环境变量对账（env-reconcile）

## 当前项目的环境变量审计结果
```!
python3 ${CLAUDE_SKILL_DIR}/scripts/env-reconcile.py ${CLAUDE_PROJECT_DIR}
```

## 你的任务
基于上面已注入的对账报告，按下列要求处理：

1. **高危项优先**：列出所有"代码引用了但 `.env` 系列文件中缺失"的变量。每个标注：变量名、出现文件与行号、引用方式（如 `process.env` / `os.getenv`）。这是运行时崩溃的直接来源。
2. **给出建议**：对每个缺失变量，结合变量名与出现位置推断用途，给出推荐默认值；推断不出就写"需确认"，**不要编造值**。
3. **遗留项提醒**：列出"声明了但代码没引用"的变量，提示可能是历史遗留或应在其他环境声明。
4. **可执行的修复**：询问用户是否要生成一份补齐缺失变量的 `.env.example` 片段；用户同意后，按 `KEY=默认值  # 用途说明` 格式输出。

## 输出格式
用三张 Markdown 表格：缺失变量（高危）、声明未用（警告）、已对账（仅计数）。表头示例：
| 变量 | 文件:行 | 引用方式 | 建议默认 |

## 进阶
- 各语言引用模式与设计取舍见 [reference.md](reference.md)。
````

这段正文有两个**最值得学的设计**：

**(1) 动态上下文注入 `!`**：反引号前缀 `` !`command` `` 的代码块会在 Skill 内容发给 Claude **之前**先执行，命令输出**原样替换**占位符。也就是说，Claude 看到的不是"请运行脚本"，而是"这是扫描报告：……"。这把模型从"猜测代码状态"升级为"基于真实数据作答"，是 grounding 的关键。注意几个细节：

- 它是**预处理**，Claude 只看到结果；命令输出按纯文本插入，**不会被再次扫描**占位符；
- 行内式 `` !`cmd` `` 只有在 `!` 位于**行首或紧跟空白**时才被识别，`KEY=!`cmd`` 这种会被当字面量；
- 若组织通过 `disableSkillShellExecution: true` 策略禁用，占位符会被替换成提示文字而不执行。

**(2) 两个内置变量**：

- `${CLAUDE_SKILL_DIR}`：当前 Skill 的目录，保证 `python3 scripts/env-reconcile.py` 不管 Skill 装在个人级还是项目级都能找到脚本；
- `${CLAUDE_PROJECT_DIR}`：项目根目录，与 hooks、MCP 收到的 `CLAUDE_PROJECT_DIR` 同义，保证扫描的是当前工程而非你启动 Claude 时的目录。

**(3) 渐进式披露**：正文末尾把"各语言模式表"指向 `reference.md`，而不是塞进 `SKILL.md`。`reference.md` 只有在 Claude 觉得需要时才会读入，平时零成本。

## 第五步：把重活下沉成脚本

把下面这段存为 `scripts/env-reconcile.py`。它覆盖 JS/TS、Python、Go、Ruby 四大生态的常见写法，仅用标准库：

```python
#!/usr/bin/env python3
"""env-reconcile.py — 对账代码中的环境变量引用与 .env* 配置文件。"""
import re, sys
from pathlib import Path

# (标签, 正则) 每条正则只留一个真正用于取名的捕获组
PATTERNS = [
    ("process.env",     re.compile(r"process\.env\.([A-Za-z_][A-Za-z0-9_]*)")),
    ("import.meta.env", re.compile(r"import\.meta\.env\.([A-Za-z_][A-Za-z0-9_]*)")),
    ("os.getenv",       re.compile(r"os\.getenv\(\s*['\"]([A-Za-z_][A-Za-z0-9_]*)['\"]")),
    ("os.environ.get",  re.compile(r"os\.environ\.get\(\s*['\"]([A-Za-z_][A-Za-z0-9_]*)['\"]")),
    ("os.environ[]",    re.compile(r"os\.environ\[\s*['\"]([A-Za-z_][A-Za-z0-9_]*)['\"]")),
    ("ENV[...]",        re.compile(r"ENV\[\s*['\"]([A-Za-z_][A-Za-z0-9_]*)['\"]|ENV\.fetch\(\s*['\"]([A-Za-z_][A-Za-z0-9_]*)['\"]")),
    ("os.Getenv",       re.compile(r"os\.Getenv\(\s*['\"]([A-Za-z_][A-Za-z0-9_]*)['\"]")),
]

SCAN_EXT = {".js",".jsx",".ts",".tsx",".mjs",".cjs",".py",".go",".rb",".vue",".svelte"}
IGNORE   = {".git","node_modules","__pycache__",".venv","venv","dist","build",".next",".turbo","coverage",".cache"}
ENV_FILES = [".env",".env.example",".env.sample",".env.schema",".env.template",".env.defaults"]
KEY_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")

def parse_env_files(root):
    declared = {}
    for name in ENV_FILES:
        p = root / name
        if not p.exists():
            continue
        for line in p.read_text(errors="ignore").splitlines():
            s = line.strip()
            if not s or s.startswith("#") or "=" not in s:
                continue
            key = s.split("=", 1)[0].strip()
            if KEY_RE.match(key):
                declared[key] = name
    return declared

def scan_sources(root):
    used = {}
    for path in root.rglob("*"):
        if not path.is_file() or path.suffix not in SCAN_EXT:
            continue
        if any(part in IGNORE for part in path.parts):
            continue
        try:
            text = path.read_text(errors="ignore")
        except Exception:
            continue
        rel = path.relative_to(root)
        for i, line in enumerate(text.splitlines(), 1):
            for label, pat in PATTERNS:
                for m in pat.finditer(line):
                    key = next((g for g in m.groups() if g), None)
                    if key:
                        used.setdefault(key, []).append((str(rel), i, label))
    return used

def main():
    root = Path(sys.argv[1]).resolve() if len(sys.argv) > 1 else Path.cwd()
    declared = parse_env_files(root)
    used = scan_sources(root)
    dk, uk = set(declared), set(used)
    missing, unused, matched = sorted(uk - dk), sorted(dk - uk), sorted(uk & dk)

    print("# 环境变量对账报告")
    print(f"根目录: `{root}`")
    print(f"声明 {len(dk)} 个 | 引用 {len(uk)} 个 | 已对账 {len(matched)} 个\n")

    print("## 缺失变量（高危：代码引用了但 .env 中没有）")
    if not missing:
        print("（无）")
    for k in missing:
        locs = used[k][:3]
        more = f" …另有{len(used[k])-3}处" if len(used[k]) > 3 else ""
        print(f"- **{k}** @ " + ", ".join(f"{f}:{ln}({lab})" for f, ln, lab in locs) + more)

    print("\n## 声明未用（警告：.env 里有但代码没引用）")
    print("\n".join(f"- `{k}`（在 {declared[k]}）" for k in unused) or "（无）")

    print(f"\n## 已对账: {len(matched)} 个（" + ", ".join(matched[:10]) + ("…" if len(matched) > 10 else "") + "）")

    if missing:
        print("\n## 建议 .env.example 补齐片段")
        for k in missing:
            print(f"{k}=  # 需确认：见 " + ", ".join(f"{f}:{ln}" for f, ln, _ in used[k][:2]))

if __name__ == "__main__":
    main()
```

**设计要点**：脚本只做"对账"这一件事，输出纯 Markdown，不直接改文件。是否生成 `.env.example` 由 Claude 在正文授权后执行——**把确定性计算交给脚本，把需要判断的决策交给模型**，这是 Script Skill 的黄金分工。

## 第六步：试跑——验证两种触发方式

在任一工程目录启动 `claude`，用两种方式验证：

**方式 A：自动触发**。直接说一句匹配 description 的话：

```text
帮我对账一下这个项目的环境变量，看看 .env 有没有漏配的
```

Claude 应当自动加载 `env-reconcile`（因为 description 里有"环境变量对账"".env 缺失"），脚本执行后报告被注入，模型基于报告回答。

**方式 B：手动触发**：

```text
/env-reconcile
```

两种方式都应得到三张表 + 修复建议。注意：

- **改了 Skill 不用重启**。Claude Code 会监听 Skill 目录的文件变化，编辑 `SKILL.md` 或脚本即时生效（仅 `SKILL.md` 文本层面；若 Skill 同时是插件、改了 hooks/MCP，需 `/reload-plugins`）。
- 想确认 Skill 到底有没有被加载、description 有没有被预算裁剪，跑 `/doctor` 或看 `/context` 里的 Skills 行。

## 第七步：调试触发的两个方向

**Skill 不触发**：① description 没包含用户会自然说的词——补关键词；② 问问 Claude "What skills are available?"，看它在不在列表；③ 直接 `/env-reconcile` 排除是不是 frontmatter 解析挂了（`--debug` 看报错）；④ 描述被预算裁剪——用 `skillOverrides` 把低优先级 Skill 设成 `name-only` 腾预算，或调大 `skillListingBudgetFraction`。

**Skill 触发太频繁**：① 把 description 写得更窄（加限定词，如"仅用于 Node/Python/Go/Ruby 工程"）；② 给动作型 Skill 加 `disable-model-invocation: true`，只留 `/` 手动触发。配置对账如果是"团队 CI 前必须跑一次"的流程，强烈建议加这条，免得 Claude 在你聊天时擅自扫一遍。

## 第八步：用 eval 量化触发准确率（可选但推荐）

"Skill 能触发"不等于"该触发时触发、不该触发时不触发"。官方 `skill-creator` 插件把这个回归测试自动化了：

```text
/plugin install skill-creator@claude-plugins-official
```

装完 `/reload-plugins`，然后：

```text
用 skill-creator 评估我的 env-reconcile Skill
```

它会引导你写测试用例（该触发 / 不该触发的 prompt），每个用例起一个**干净子代理**独立跑，记录 token 与耗时，按断言判 pass/fail，最后输出 `benchmark.json` 做"有 Skill vs 无 Skill"的对比，还能盲测两个版本的 A/B。配置对账这种 Skill，典型该触发用例："启动报 `Cannot read undefined`，怀疑环境变量没配"；典型不该触发用例："帮我看看这段排序算法对不对"。跑一轮就能发现 description 是不是把无关请求也招惹来了。

> 说明：上一轮教程已专门讲过"用 eval 基准打磨 Skill 触发准确率"，这里只做衔接，不展开。

## 第九步：分享出去

- **团队共享**：把 `.claude/skills/env-reconcile/` 整个提交到 Git，同事拉代码后即用（首次需接受工作区信任对话框，因为项目级 Skill 的 `allowed-tools` 在信任后才生效）；
- **跨项目自用**：留在 `~/.claude/skills/`；
- **打包分发**：加一个 `skills/` 目录做成 plugin，通过 marketplace 安装。

---

## 实战案例：在混栈项目里跑通对账

准备一个最小混栈项目（Node + Python）：

```bash
mkdir demo && cd demo
cat > app.js <<'EOF'
const db = process.env.DATABASE_URL;
const port = process.env.PORT;
const secret = process.env.JWT_SECRET;   // 故意不在 .env
EOF
cat > worker.py <<'EOF'
import os
redis = os.getenv("REDIS_URL")           # 故意不在 .env
EOF
cat > .env.example <<'EOF'
DATABASE_URL=postgres://localhost/app
PORT=3000
ORPHAN_KEY=leftover                      # 故意声明但没用
EOF
```

在这个目录里启动 `claude`，输入：

```text
/env-reconcile
```

预期注入的报告（节选）：

```markdown
# 环境变量对账报告
根目录: `/path/to/demo`
声明 4 个 | 引用 4 个 | 已对账 2 个

## 缺失变量（高危：代码引用了但 .env 中没有）
- **JWT_SECRET** @ app.js:3(process.env)
- **REDIS_URL** @ worker.py:2(os.getenv)

## 声明未用（警告：.env 里有但代码没引用）
- `ORPHAN_KEY`（在 .env.example）

## 已对账: 2 个（DATABASE_URL, PORT）

## 建议 .env.example 补齐片段
JWT_SECRET=  # 需确认：见 app.js:3
REDIS_URL=  # 需确认：见 worker.py:2
```

Claude 据此给出高危清单、用途推断、修复片段。一次 `/env-reconcile`，把"上线时才发现 `JWT_SECRET` 是 undefined"这种事故拦在提交前。

## 工作流程图

```mermaid
flowchart TD
    U[用户: 帮我对账环境变量] -->|自动匹配 description| S[env-reconcile Skill 激活]
    U2[/手动输入 /env-reconcile/] --> S
    S -->|预执行 ! 注入| P[scripts/env-reconcile.py]
    P -->|扫描源码引用| P1[process.env / os.getenv / os.Getenv / ENV]
    P -->|解析 .env*| P2[声明变量集合]
    P1 --> R[(对账报告 Markdown)]
    P2 --> R
    R -->|输出替换占位符| C[Claude 收到已注入报告]
    C --> A1[高危变量清单 + 行号]
    C --> A2[声明未用提醒]
    C --> A3[询问是否生成 .env.example 补齐片段]
    A3 -->|用户确认| F[输出 KEY=默认 # 用途]
    S -.按需加载.-> REF[reference.md 模式表]
```

## 延伸阅读

- **官方 Skills 文档**：`code.claude.com/docs/en/skills`——frontmatter 全字段、动态注入、子代理运行的最权威来源；
- **skill-creator 插件**：用于量化 Skill 触发准确率与输出版本对比，本系列第 5 轮会深入；
- **Agent Skills 开放标准**：`agentskills.io`，Claude Code 的 Skill 遵循的跨工具规范；
- **官方最佳实践**：`code.claude.com/docs/en/best-practices`——SKILL.md 与 CLAUDE.md 的取舍、上下文成本控制；
- 进阶可探索 `context: fork` 让 Skill 在隔离子代理里跑、`hooks` 字段把生命周期钩子挂到 Skill 上、`skillOverrides` 从 settings 控制可见性。

## 训练营适用性评估

- **适合的训练营类型**：全栈 / 后端 / DevOps / 云原生方向的工程师训练营，尤其面向"有真实工程经验、想用 AI 提效"的中级学员；也可作为前端训练营的进阶模块。
- **教学时长**：建议 **90–120 分钟**。30 分钟讲 Skill 三要素（目录/frontmatter/脚本）+ 渐进式披露；30 分钟带做 `env-reconcile`；20 分钟跑实战案例与 eval；剩余时间留作"造你自己的对账型 Skill"练习。
- **前置依赖**：需先完成第 1 轮（装通 Claude Code）。
- **可延伸的学员作业**：① 扩展支持 Java `System.getenv`、PHP `getenv`、Shell `$VAR`；② 增加 `.env.schema` 的类型/必填校验，把"建议默认值"升级为"类型不符告警"；③ 给 Skill 加 `disable-model-invocation: true`，做成 CI 前的强制自检门禁。
- **不适合**：纯零基础（应先上第 1 轮）；纯算法/科研方向（收益低）。

## 参考来源

1. [Extend Claude with skills — Claude Code Docs](https://code.claude.com/docs/en/skills) — 官方 Skill 开发权威文档，frontmatter 全字段、动态上下文注入 `${CLAUDE_SKILL_DIR}`、`paths`/`allowed-tools`/`disable-model-invocation` 用法、skill-creator eval 流程均出自此处。
2. [Best practices for Claude Code — Claude Code Docs](https://code.claude.com/docs/en/best-practices) — SKILL.md 与 CLAUDE.md 的取舍、上下文成本与精简纪律的官方建议。
3. [Claude Code Skills: The Complete Guide (2026) — duet.so](https://duet.so/guides/claude-code-skills-complete-guide) — 第三方对 Skill 目录结构、脚本型 Skill 模式的系统梳理。
4. [skill-creator: Test, measure and refine agent skills — Anthropic Blog](https://claude.com/blog/improving-skill-creator-test-measure-and-refine-agent-skills) — eval/基准/版本盲测的官方说明，对应第八步量化触发准确率。
5. [Agent Skills 开放标准 — agentskills.io](https://agentskills.io) — Claude Code Skill 遵循的跨工具规范与 Skill 编写最佳实践。

> [!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 等场景全覆盖。

![51学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
