首页 › tutorials › 造配置对账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 均可受益。

前置条件


第一步:先想清楚"为什么要做成 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。官方推荐的渐进式披露结构如下:

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

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

创建目录(个人级,对所有项目生效):

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 必须夹在两个 --- 之间):

---
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 支持的字段:

如果你想让它只接受手动 /env-reconcile 触发(避免 Claude 自作主张乱跑),再加一行:

disable-model-invocation: true

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

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

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

SKILL.md 正文(接在 frontmatter 之后):

# 环境变量对账(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 的关键。注意几个细节:

(2) 两个内置变量:

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

第五步:把重活下沉成脚本

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

#!/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 的话:

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

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

方式 B:手动触发:

/env-reconcile

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

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

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 插件把这个回归测试自动化了:

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

装完 /reload-plugins,然后:

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

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

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

第九步:分享出去


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

准备一个最小混栈项目(Node + Python):

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,输入:

/env-reconcile

预期注入的报告(节选):

# 环境变量对账报告
根目录: `/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"这种事故拦在提交前。

工作流程图

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 模式表]

延伸阅读

训练营适用性评估

参考来源

  1. Extend Claude with skills — Claude Code Docs — 官方 Skill 开发权威文档,frontmatter 全字段、动态上下文注入 ${CLAUDE_SKILL_DIR}、paths/allowed-tools/disable-model-invocation 用法、skill-creator eval 流程均出自此处。
  2. Best practices for Claude Code — Claude Code Docs — SKILL.md 与 CLAUDE.md 的取舍、上下文成本与精简纪律的官方建议。
  3. Claude Code Skills: The Complete Guide (2026) — duet.so — 第三方对 Skill 目录结构、脚本型 Skill 模式的系统梳理。
  4. skill-creator: Test, measure and refine agent skills — Anthropic Blog — eval/基准/版本盲测的官方说明,对应第八步量化触发准确率。
  5. Agent Skills 开放标准 — 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学通信知识星球海报-扫码加入