本教程手把手带你从零构建一个硬编码密钥扫描 Skill——它能扫描代码仓库里泄露的 AWS Key、GitHub Token、私钥、高熵字符串等敏感凭据,并产出可修复的报告。你将完整走通 Claude Code Skill 的"三段式"结构:YAML frontmatter、Markdown 指令正文、可执行的捆绑脚本,并学会用 Evals 思路验证精度、用 description 调优让 Skill 被精准触发。适合已会基本使用 Claude Code、想进入"自定义能力扩展"阶段的开发者与安全工程师。
claude --version 能出版本号)⚠️ 安全提醒:本 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 存放在固定目录,有两个层级:
<项目根>/.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,且 --- 顶格:
---
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 位即可,避免二次泄露。
- 扫描结果属于敏感信息,提醒用户不要把报告粘贴到公开渠道。
name:必填。kebab-case(小写+连字符),最长 64 字符,且必须与目录名一致。description:必填。Claude 判断"是否触发"的唯一依据。写法见第五节。disable-model-invocation(可选):设为 true 可禁止 Claude 自动触发,只允许 /hardcoded-secrets-scan 手动调用——适合不想被误触的 Skill。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` 可查)
description 写不好,Skill 要么该触发时不触发,要么不该触发时乱触发。三条原则:
audit、leaked secrets、credentials、before push。Claude 是靠语义匹配,把你预期用户会说的话写进去。验证触发是否准确,最简单的办法:开一个新会话,输入自然语言而非斜杠命令,看 Claude 是否自动激活本 Skill:
"帮我看看这个仓库有没有泄露什么密钥,提交前想确认一下"
若 Claude 主动运行了 scan_secrets.py,说明 description 生效了。若没触发,回去改 description。
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
.secretsallow 与 false-positive-guide.md 的迭代——这是体会"脚本定规则、Claude 做判断"分工的最佳方式。[!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 等场景全覆盖。
