<!-- title: 硬编码密钥扫描Skill构建 -->

## 概述

本教程手把手带你从零构建一个**硬编码密钥扫描 Skill**——它能扫描代码仓库里泄露的 AWS Key、GitHub Token、私钥、高熵字符串等敏感凭据，并产出可修复的报告。你将完整走通 Claude Code Skill 的"三段式"结构：YAML frontmatter、Markdown 指令正文、可执行的捆绑脚本，并学会用 Evals 思路验证精度、用 description 调优让 Skill 被精准触发。适合已会基本使用 Claude Code、想进入"自定义能力扩展"阶段的开发者与安全工程师。

## 前置条件

- Claude Code 已安装并可在终端运行（`claude --version` 能出版本号）
- 基本的命令行与 Git 操作能力
- Python 3.8+（用于扫描脚本，纯标准库，无需 pip 安装）
- 一个用来练手的代码仓库（自己的小项目即可，**不要扫描公司生产仓库**，以免误把真实密钥输出到对话上下文）

> ⚠️ 安全提醒：本 Skill 扫描出的密钥会进入对话上下文。若扫描出**真实**密钥，请立即在源代码托管平台吊销并轮换，而非仅在本地删除。

## 一、先理解：Skill 到底是什么

很多人把 Skill、MCP、斜杠命令混为一谈。一句话区分：

| 机制 | 本质 | 触发方式 | 典型用途 |
|------|------|----------|----------|
| **斜杠命令** | 预定义的提示词模板 | 用户主动输入 `/xxx` | 固定流程的快捷入口 |
| **MCP 服务器** | 外部工具/数据源 | Claude 调用工具函数 | 接数据库、调 API、操作系统 |
| **Skill** | **按需加载的指令包 + 本地助手脚本** | Claude 自动判断相关，或 `/skill-name` | 扩展某类任务的"专业知识+流程" |

Skill 的核心机制叫**渐进式披露（Progressive Disclosure）**：Claude Code 启动时**只把每个 Skill 的 `name` 和 `description` 两个字段加载进上下文**，正文和脚本都不加载。只有当 Claude 判断当前任务与某个 Skill 的 description 匹配时，才把对应 `SKILL.md` 正文"翻出来"读。这保证了你可以装几十个 Skill 而不撑爆上下文。

理解这一点，你就明白为什么后面会反复强调 **description 是整个 Skill 最重要的字段**。

## 二、创建 Skill 骨架

Skill 存放在固定目录，有两个层级：

- **项目级**：`<项目根>/.claude/skills/<skill-name>/SKILL.md` —— 只对当前项目生效
- **个人级**：`~/.claude/skills/<skill-name>/SKILL.md` —— 对你所有项目生效

我们做一个项目级 Skill。在目标项目根目录下创建如下结构：

```
.claude/skills/hardcoded-secrets-scan/
├── SKILL.md                      # 必需：指令正文
├── scripts/
│   └── scan_secrets.py           # 可选：确定性扫描脚本
├── references/
│   └── false-positive-guide.md   # 可选：误判处理参考
└── .secretsallow                 # 可选：白名单
```

先创建 `SKILL.md`。**第一行必须是 YAML frontmatter，且 `---` 顶格**：

```markdown
---
name: hardcoded-secrets-scan
description: Scans a codebase for hardcoded secrets and credentials (AWS keys, GitHub tokens, private keys, high-entropy strings, etc.), produces a triaged Markdown/JSON report, and advises on rotation. Use when the user wants a security audit for leaked secrets, before committing/pushing, or when hardening a repository.
---

# 硬编码密钥扫描 Skill

当用户希望检查代码仓库中是否泄露了密钥/凭据时，按以下流程执行。

## 执行步骤

1. 确认扫描目标路径（默认当前仓库根目录）。若用户未指定，询问或使用 `git rev-parse --show-toplevel`。
2. 运行确定性扫描脚本，获取候选命中：
   `python3 scripts/scan_secrets.py <目标路径> --format json`
3. 阅读返回的候选列表，结合 `references/false-positive-guide.md` 做人工研判：
   - 排除明显的占位符（`example`、`test`、`your-key-here`、`xxxx`）
   - 排除已在 `.secretsallow` 白名单中的项
   - 标注每条命中的风险等级（高/中/低）
4. 产出报告：先给一份 Markdown 摘要表格，再列出需要轮换的高危项与修复建议。
5. 若发现疑似**真实**密钥，强烈建议用户立即吊销轮换，而不是仅删除。

## 注意

- 不要把超长的高熵字符串整段贴进报告，截断显示前 8 后 4 位即可，避免二次泄露。
- 扫描结果属于敏感信息，提醒用户不要把报告粘贴到公开渠道。
```

### Frontmatter 字段详解

- `name`：**必填**。kebab-case（小写+连字符），最长 64 字符，且必须与目录名一致。
- `description`：**必填**。Claude 判断"是否触发"的唯一依据。写法见第五节。
- `disable-model-invocation`（可选）：设为 `true` 可禁止 Claude 自动触发，只允许 `/hardcoded-secrets-scan` 手动调用——适合不想被误触的 Skill。

## 三、编写确定性扫描脚本

Skill 的正道是：**把"能确定的事情交给脚本，把"需要判断的事情交给 Claude"**。扫描密钥这种规则明确的任务，绝不该让 Claude 用肉眼一个个文件看——既慢又不稳定。我们用 Python 脚本做规则匹配 + 熵值计算，把候选结果交给 Claude 研判。

创建 `scripts/scan_secrets.py`（纯标准库，无需安装依赖）：

```python
#!/usr/bin/env python3
"""硬编码密钥扫描器：正则规则 + Shannon 熵 + 白名单。"""
import argparse, json, math, os, re, sys

# (规则名, 正则, 风险等级)
PATTERNS = [
    ("AWS Access Key ID",   r"AKIA[0-9A-Z]{16}",                              "高"),
    ("AWS Secret Key",      r"(?i)aws.{0,20}?(secret|sk).{0,5}?['\"][0-9a-zA-Z/+]{40}['\"]", "高"),
    ("Google API Key",      r"AIza[0-9A-Za-z\-_]{35}",                        "中"),
    ("GitHub Token",        r"\bgh[pousr]_[0-9A-Za-z]{36}\b|github_pat_[0-9A-Za-z_]{82}", "高"),
    ("Slack Token",         r"xox[baprs]-[0-9A-Za-z\-]{10,48}",               "中"),
    ("Stripe Live Key",     r"sk_live_[0-9a-zA-Z]{24}",                       "高"),
    ("Private Key Block",   r"-----BEGIN (?:RSA |EC |OPENSSH |PGP |DSA )?PRIVATE KEY-----", "高"),
    ("JWT",                 r"eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}", "中"),
]

# 明显占位符，命中则降级或忽略
PLACEHOLDER = re.compile(r"(?i)(example|sample|test|dummy|your[_-]?key|xxxx|<|\$?\{?[a-z_]+\}?|placeholder)")

SKIP_DIRS = {".git", "node_modules", "vendor", "dist", "build", ".venv", "__pycache__"}
MAX_FILE_BYTES = 1_500_000  # 跳过超大文件

def shannon_entropy(s: str) -> float:
    if not s:
        return 0.0
    probs = [s.count(c) / len(s) for c in set(s)]
    return -sum(p * math.log2(p) for p in probs)

def load_allowlist(path: str) -> set:
    allow = set()
    if os.path.isfile(path):
        for line in open(path, encoding="utf-8", errors="ignore"):
            line = line.strip()
            if line and not line.startswith("#"):
                allow.add(line)
    return allow

def walk(root: str):
    for dirpath, dirnames, filenames in os.walk(root):
        dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS]
        for fn in filenames:
            fp = os.path.join(dirpath, fn)
            if os.path.getsize(fp) > MAX_FILE_BYTES:
                continue
            yield fp

def scan(root: str, allow: set):
    hits = []
    compiled = [(n, re.compile(p), lvl) for n, p, lvl in PATTERNS]
    for fp in walk(root):
        try:
            text = open(fp, encoding="utf-8", errors="ignore").read()
        except OSError:
            continue
        lines = text.splitlines()
        for i, line in enumerate(lines, 1):
            for name, rx, level in compiled:
                for m in rx.finditer(line):
                    secret = m.group(0)
                    if secret in allow:
                        continue
                    placeholder = bool(PLACEHOLDER.search(line))
                    hits.append({
                        "rule": name,
                        "file": os.path.relpath(fp, root),
                        "line": i,
                        "risk": "低" if placeholder else level,
                        "preview": secret[:8] + "…" + secret[-4:] if len(secret) > 14 else secret,
                        "entropy": round(shannon_entropy(secret), 2),
                    })
            # 通用高熵检测：靠近 key/secret/token/password 等词的长串
            if re.search(r"(?i)(api[_-]?key|secret|token|password|passwd|pwd)", line):
                for tok in re.findall(r"['\"]([0-9a-zA-Z+/=_\-]{32,64})['\"]", line):
                    if tok in allow or PLACEHOLDER.search(line):
                        continue
                    if shannon_entropy(tok) >= 4.5:
                        hits.append({
                            "rule": "High-Entropy Secret",
                            "file": os.path.relpath(fp, root),
                            "line": i, "risk": "中",
                            "preview": tok[:8] + "…" + tok[-4:],
                            "entropy": round(shannon_entropy(tok), 2),
                        })
    # 去重
    seen, uniq = set(), []
    for h in hits:
        k = (h["file"], h["line"], h["rule"], h["preview"])
        if k not in seen:
            seen.add(k); uniq.append(h)
    return uniq

def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("path", nargs="?", default=".")
    ap.add_argument("--allow", default=".secretsallow")
    ap.add_argument("--format", choices=["json", "md"], default="json")
    a = ap.parse_args()
    root = os.path.abspath(a.path)
    allow = load_allowlist(os.path.join(root, a.allow)) if os.path.isdir(root) else set()
    hits = scan(root, allow)
    if a.format == "json":
        print(json.dumps({"count": len(hits), "findings": hits}, ensure_ascii=False, indent=2))
    else:
        print(f"| 规则 | 文件 | 行 | 风险 | 熵值 | 预览 |")
        print(f"|------|------|----|------|------|------|")
        for h in hits:
            print(f"| {h['rule']} | {h['file']} | {h['line']} | {h['risk']} | {h['entropy']} | `{h['preview']}` |")
    sys.exit(1 if hits else 0)  # 有命中返回非零，便于接入 CI

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

### 预期结果

在项目根执行：

```bash
python3 .claude/skills/hardcoded-secrets-scan/scripts/scan_secrets.py . --format md
```

若无命中，退出码为 `0`；有命中则输出 Markdown 表格并返回 `1`。一个典型输出：

```
| 规则 | 文件 | 行 | 风险 | 熵值 | 预览 |
| AWS Access Key ID | src/config.py | 12 | 高 | 3.84 | AKIAIOSF…BQ6HQ |
| High-Entropy Secret | .env.local | 3 | 中 | 4.91 | 9f3a2c1b…d7e8 |
```

### 常见问题

| 问题 | 解决 |
|------|------|
| 报错 `UnicodeDecodeError` | 已用 `errors="ignore"` 跳过；若仍异常，检查是否扫到二进制文件，可按扩展名白名单只扫文本文件 |
| 误报太多 | 把确认安全的字符串写入 `.secretsallow`（一行一个），或调整熵值阈值 `4.5` |
| 漏报 `.env` | 确认没被 `SKIP_DIRS` 排除；`.env` 默认在扫描范围内 |
| 扫描太慢 | `MAX_FILE_BYTES` 已限制单文件大小；超大仓库可加 `--include` 只扫特定目录 |

## 四、配置白名单与误判参考

在 Skill 根目录创建 `.secretsallow`：

```
# 每行一个已知安全字符串（取完整值）
AKIAIOSFODNN7EXAMPLE
9f3a2c1b0000000000000000000000d7e8
```

再创建 `references/false-positive-guide.md`，给 Claude 一份"如何判断误报"的清单。这是把领域知识喂给 Claude 的关键——脚本不懂语义，Claude 懂：

```markdown
# 误判研判指南
命中以下特征通常为**误报**，可标为低风险或忽略：
- 含 example / sample / test / dummy / placeholder 的行
- 文档与注释中的示例（路径含 docs/、README、_test.go、spec）
- 显式 mock/fixture 目录下的固定值
- K8s Helm 模板占位 `{{ .Values.xxx }}`

命中以下特征通常为**真实泄露**，需立即轮换：
- 位于 .env / config / settings 等运行时配置文件
- 熵值 > 4.5 且无明显占位词
- 出现在最近 git 提交中（`git log -p` 可查）
```

## 五、让 Skill 被精准触发

`description` 写不好，Skill 要么该触发时不触发，要么不该触发时乱触发。三条原则：

1. **第三人称、写清"做什么+何时用"**：好——"Scans a codebase for hardcoded secrets… Use when the user wants a security audit for leaked secrets, before committing/pushing"；坏——"这是一个很厉害的扫描工具"。
2. **埋入用户会实际说的词**：`audit`、`leaked secrets`、`credentials`、`before push`。Claude 是靠语义匹配，把你预期用户会说的话写进去。
3. **避免与其它 Skill 描述撞车**：如果你还装了"依赖升级侦察 Skill"，别让两者的 description 都泛泛地说"扫描代码"。

验证触发是否准确，最简单的办法：开一个新会话，输入自然语言而非斜杠命令，看 Claude 是否自动激活本 Skill：

> "帮我看看这个仓库有没有泄露什么密钥，提交前想确认一下"

若 Claude 主动运行了 `scan_secrets.py`，说明 description 生效了。若没触发，回去改 description。

## 六、用 Evals 思路验证精度

Skill 上线前必须验证"查准率"和"查全率"，否则误报一堆用户会失去信任。建一个隔离的测试仓库：

```
secrets-eval/
├── cases/
│   ├── real/        # 真实泄露样本（用假但格式正确的值）
│   │   ├── aws.py   # AKIA...
│   │   └── key.pem  # -----BEGIN RSA PRIVATE KEY-----
│   └── safe/        # 应被忽略的安全样本
│       ├── readme.md  # 文档里的 example
│       └── mock.py    # test fixture
└── expected.json   # 期望命中清单
```

跑一遍：

```bash
python3 .../scan_secrets.py secrets-eval/cases/real/   # 期望：real/* 全命中
python3 .../scan_secrets.py secrets-eval/cases/safe/   # 期望：0 命中
```

把"期望命中数 vs 实际命中数"做成一个小对照表。若 `safe/` 出现命中，说明 `PLACEHOLDER` 规则或白名单要补；若 `real/` 漏报，说明正则要加。这个闭环就是 Evals 的精髓：**用固定输入+期望输出，量化 Skill 质量**。后续每次改正则都重跑，确保没引入回归。

## 七、实战案例：把扫描接入提交前门禁

把 Skill 与 Git 的 `pre-commit` 钩子结合，实现"提交即拦截"。在仓库根新建 `.git/hooks/pre-commit`（或用 pre-commit 框架）：

```bash
#!/usr/bin/env bash
# 提交前扫描暂存区，命中高危密钥则阻止提交
STAGED=$(git diff --cached --name-only --diff-filter=ACM | tr '\n' ' ')
[ -z "$STAGED" ] && exit 0

TMP=$(mktemp -d)
for f in $STAGED; do cp --parents "$f" "$TMP" 2>/dev/null; done

OUT=$(python3 .claude/skills/hardcoded-secrets-scan/scripts/scan_secrets.py "$TMP" --format json)
rm -rf "$TMP"

if echo "$OUT" | grep -q '"risk": "高"'; then
  echo "❌ 检测到高危硬编码密钥，提交已阻止。请在 Claude Code 中运行 /hardcoded-secrets-scan 复核。"
  echo "$OUT" | python3 -c "import sys,json; [print(f\"  {h['file']}:{h['line']} {h['rule']}\") for h in json.load(sys.stdin)['findings'] if h['risk']=='高']"
  exit 1
fi
exit 0
```

`chmod +x .git/hooks/pre-commit` 后，下次 `git commit` 会自动扫描暂存文件。命中高危密钥直接拦下，并提示用 `/hardcoded-secrets-scan` 进入 Skill 做完整复核。这就把"按需的 Skill"和"强制的门禁"串成了一条质量闭环。

## 八、整体工作流

```mermaid
flowchart TD
    A[用户: 帮我查密钥泄露] --> B{description 匹配?}
    B -- 是 --> C[Claude 加载 SKILL.md 正文]
    B -- 否 --> Z[不触发, 常规对话]
    C --> D[运行 scripts/scan_secrets.py]
    D --> E[读取 .secretsallow 白名单]
    E --> F[规则匹配 + 熵值计算]
    F --> G[候选命中 JSON]
    G --> H[Claude 读 false-positive-guide.md 研判]
    H --> I[Markdown 报告 + 风险分级]
    I --> J{有高危真实密钥?}
    J -- 是 --> K[建议立即吊销轮换]
    J -- 否 --> L[输出报告, 完成]
    K --> L
```

## 训练营适用性评估

- **适合**：Claude Code 进阶训练营、企业安全开发训练营、DevSecOps 实战课。本教程一个 Skill 串起了"frontmatter→脚本→白名单→Evals→触发调优→CI 门禁"完整链路，适合作为"自定义能力扩展"模块的核心实验。
- **教学时长**：约 90–120 分钟。其中创建骨架与脚本约 40 分钟，白名单与误判指南约 20 分钟，Evals 验证约 20 分钟，接入门禁与触发调优约 20–30 分钟。
- **分组建议**：可让不同小组扫描各自的小项目，互相交换误报清单，练习 `.secretsallow` 与 `false-positive-guide.md` 的迭代——这是体会"脚本定规则、Claude 做判断"分工的最佳方式。
- **延伸方向**：把扫描结果接 MCP 推送到 Slack/工单系统；扩展规则覆盖 K8s Secret、Terraform state 文件；做成 Plugin 分发给团队。

## 参考来源

1. [Extend Claude with skills — Claude Code Docs](https://code.claude.com/docs/en/skills) — 官方 Skill 创建、管理与渐进式披露机制的权威说明
2. [Skill authoring best practices — Claude API Docs](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices) — 官方对 frontmatter 字段（name/description）与 description 撰写规范的指导
3. [SKILL.md frontmatter reference — ClaudSkills](https://claudskills.com/learn/skill-md-frontmatter-reference/) — 逐字段解释 name、description、disable-model-invocation 等可选项
4. [Inside Claude Code Skills: Structure, prompts, invocation — Mikhail Shilkov](https://mikhail.io/2025/10/claude-code-skills/) — 深入解析 Skill 的发现、加载与脚本定位机制
5. [Avoid dependency hell for Claude SKILLs — Medium](https://medium.com/@michaelyuan_88928/avoid-dependency-hell-for-claude-skills-62658982ebb4) — 捆绑可执行脚本（Python/Bash/Node）的依赖管理与确定性实践

> [!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)
