首页 › tutorials › 硬编码密钥扫描Skill构建

概述

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

前置条件

⚠️ 安全提醒:本 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 存放在固定目录,有两个层级:

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

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

先创建 SKILL.md。第一行必须是 YAML frontmatter,且 --- 顶格:

---
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 字段详解

三、编写确定性扫描脚本

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

创建 scripts/scan_secrets.py(纯标准库,无需安装依赖):

#!/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()

预期结果

在项目根执行:

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 懂:

# 误判研判指南
命中以下特征通常为**误报**,可标为低风险或忽略:
- 含 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   # 期望命中清单

跑一遍:

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 框架):

#!/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"和"强制的门禁"串成了一条质量闭环。

八、整体工作流

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

训练营适用性评估

参考来源

  1. Extend Claude with skills — Claude Code Docs — 官方 Skill 创建、管理与渐进式披露机制的权威说明
  2. Skill authoring best practices — Claude API Docs — 官方对 frontmatter 字段(name/description)与 description 撰写规范的指导
  3. SKILL.md frontmatter reference — ClaudSkills — 逐字段解释 name、description、disable-model-invocation 等可选项
  4. Inside Claude Code Skills: Structure, prompts, invocation — Mikhail Shilkov — 深入解析 Skill 的发现、加载与脚本定位机制
  5. Avoid dependency hell for Claude SKILLs — Medium — 捆绑可执行脚本(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学通信知识星球海报-扫码加入