本教程以"正则灾难性回溯(ReDoS)风险扫描"为案例,带你从零构建一个完整的 Claude Code 自定义 Skill,覆盖目录规划、SKILL.md frontmatter 编写、渐进式披露三层结构、辅助脚本、参考子文档与调试验证全流程。学完后你能独立设计任意领域的 Skill,并掌握让 Skill"该出手时才出手"的触发词写作精髓。适合已会用 Claude Code 跑基本任务、想封装团队可复用工作流的开发者。
很多人把 Skill、自定义命令、子代理、MCP 服务器混为一谈。它们的本质区别在于"加载时机"与"承载形态"。
Skill 是一组放在文件系统里的指令包,核心是一份 SKILL.md。模型平时不读它,只在判断当前任务与某条 Skill 的描述匹配时,才把它加载进上下文。这正是"渐进式披露"的精髓——平时零成本,命中才付费。
| 形态 | 承载 | 触发 | 适合场景 |
|---|---|---|---|
| Skill | 文件系统指令包 | 模型按 description 自动判断 | 领域工作流、专家知识封装 |
| 自定义命令 | slash 命令文件 | 用户输入 /名字 |
用户主动调用的固定流程 |
| 子代理(subagent) | 独立上下文的代理 | 主代理委派 | 隔离上下文、并行拆分 |
| MCP 服务器 | 外部进程 + 工具协议 | 模型调用 tool | 接入外部系统、实时数据 |
一句话记忆:命令是"我要做",MCP 是"我需要数据",子代理是"分身去做",Skill 是"我会做,且你该知道我什么时候该做"。
正则表达式写不好,会埋下 ReDoS 这颗定时炸弹。一段看似无害的 ^(a+)+$ 在面对精心构造的输入时,匹配时间会随字符串长度指数级膨胀,几十字符就能让一个 Node 服务 CPU 打满、整条请求线卡死。历史上 Stack Overflow、Cloudflare 都因此发生过线上事故。
regex-guard 这个 Skill 的目标很简单:扫描代码库里的正则字面量,找出有回溯爆炸风险的写法,给出回溯路径分析与安全重写建议。 它足够小、足够具体,却能完整展示一个生产级 Skill 的全部结构。
Skill 可以放在两个位置:用户级 ~/.claude/skills/<name>/(所有项目可用)或项目级 .claude/skills/<name>/(随仓库共享给团队)。我们选项目级,方便版本管理与团队复用。
mkdir -p .claude/skills/regex-guard/{scripts,reference}
cd .claude/skills/regex-guard
touch SKILL.md scripts/detect_regex_risks.py reference/evil-patterns.md reference/rewrite-patterns.md
最终目录长这样:
.claude/skills/regex-guard/
├── SKILL.md # 主入口:frontmatter + 精简工作流
├── scripts/
│ └── detect_regex_risks.py # 启发式扫描脚本
└── reference/
├── evil-patterns.md # 危险模式详解(第三层,按需读)
└── rewrite-patterns.md # 安全重写对照表(第三层,按需读)
预期结果:Claude Code 启动时会扫描该目录,发现 SKILL.md 即注册为一个候选 Skill,但此时还不会加载正文。
常见问题:目录名必须与 frontmatter 的 name 一致,否则部分版本会出现"注册成功但不触发"的幽灵状态。
整个 Skill 最关键的字段不是正文,而是 frontmatter 里的 description。模型只看 description 来决定要不要加载你的 Skill,写得含糊,Skill 就永远不会被触发。
---
name: regex-guard
description: 扫描代码库中的正则表达式字面量,识别可能导致灾难性回溯(ReDoS)的性能风险模式(嵌套量词、重叠交替叠加量词、贪婪通配叠加等),输出风险位置、回溯路径分析与可落地的安全重写建议。当用户排查正则性能问题、审查用户输入校验逻辑、做安全审计、或抱怨"某个校验函数偶尔卡死"时使用。
---
写好 description 的四条铁律:
可选字段还包括 disable-model-invocation: true(禁止自动触发,仅 /skill 手动调用)、version、author 等。allowed-tools 可声明限制,但要注意它主要是意图声明,不要把它当成安全边界——真正要收紧权限应靠 settings.json 的权限配置。
常见问题:描述写成 "一个检查正则的工具" 这种废话,模型根本无从判断何时该用。务必具体到场景。
正文是第二层披露。原则是只放工作流骨架,细节下沉到 reference。因为正文每次触发都会进入上下文,写得越长,每个会话的 token 成本越高。
# 正则回溯风险扫描(regex-guard)
## 何时使用
用户提交了用户输入校验、遇到正则匹配卡顿、或要求做一次正则/安全审查时,主动扫描仓库。
## 工作流
1. 调用 `scripts/detect_regex_risks.py <目录>` 跑启发式初筛,拿到可疑清单。
2. 对每个可疑正则,结合上下文判断是否真有可控输入路径。
- 若需要判断精确的回溯路径与星级,读取 `reference/evil-patterns.md`。
- 若需要给出安全重写,读取 `reference/rewrite-patterns.md`。
3. 输出报告:文件:位置、风险等级(高/中/低)、一句话原因、重写建议。
## 输出格式
按风险等级排序的 Markdown 表格,高风险项附 2–3 行修复 diff 示例。
## 注意
脚本仅为启发式,会有误报。最终判断必须结合输入是否可控——只处理常量字符串的正则
即便模式危险也无需告警,请勿制造噪音。
注意最后那段"何时读取 reference"的指引——这是渐进式披露的胶水。正文告诉模型"危险模式详解在 reference/evil-patterns.md,需要时去读",模型只在实际需要深度分析时才加载那份文档,平时不占上下文。
脚本负责把"重复且确定"的活儿从模型身上剥离——模型擅长判断,脚本擅长穷举。下面是一份精简但可运行的启发式扫描器:
#!/usr/bin/env python3
"""Extract regex literals from source and heuristically flag ReDoS risks.
This is a heuristic first-pass; precise backtracking analysis is left to the model
after it reads reference/evil-patterns.md.
"""
import re, sys, pathlib
# Heuristic dangerous patterns
DANGEROUS = [
(r'\([^)]*[+*?][^)]*\)[+*?]', 'nested quantifier, e.g. (a+)+'),
(r'\((?:[^|)]+\|)+[^|)]+\)[+*?]', 'overlapping alternation + quantifier, e.g. (a|a)*'),
(r'(?:\.\*|\.\+|\.\?){2,}', 'stacked greedy wildcards, e.g. .*.*'),
]
# Match /pattern/flags and new RegExp("pattern")
LITERAL = re.compile(r'/(?:[^/\\\n]|\\.)+/[gimsuy]*')
NEWRE = re.compile(r'new\s+RegExp\(\s*[\'"]([^\'"]+)[\'"]')
def scan(text):
found = [(m.start(), m.group(0).strip('/').split('/')[0]) for m in LITERAL.finditer(text)]
found += [(m.start(), m.group(1)) for m in NEWRE.finditer(text)]
risks = []
for off, pat in found:
for rule, reason in DANGEROUS:
if re.search(rule, pat):
risks.append((off, pat, reason))
return risks
if __name__ == '__main__':
root = pathlib.Path(sys.argv[1])
exts = {'.js', '.ts', '.jsx', '.tsx', '.py', '.go', '.java'}
files = [p for p in root.rglob('*') if p.suffix in exts
and 'node_modules' not in p.parts and '.git' not in p.parts]
total = 0
for f in files:
text = f.read_text(encoding='utf-8', errors='ignore')
for off, pat, reason in scan(text):
line = text[:off].count('\n') + 1
print(f'{f}:{line} /{pat}/ -> {reason}')
total += 1
print(f'---- {total} suspicious risk(s) ----')
预期结果:在一个含 const email = /^(a+)+$/ 的测试文件上运行,会输出该行与原因。
chmod +x scripts/detect_regex_risks.py
python3 scripts/detect_regex_risks.py ./src
# src/validators.js:42 /^(a+)+$/ -> nested quantifier, e.g. (a+)+
# ---- 1 suspicious risk(s) ----
常见问题:脚本误报是常态。比如 (\d{4})+ 虽然形似嵌套量词,但 \d{4} 是定长,没有回溯。这正是要把"最终判断"留给模型、把"精确判据"放进 reference 的原因——脚本只负责把候选捞出来。
reference/evil-patterns.md 存放需要专业知识才能写好的判据。这部分平时完全不加载,只有模型在分析某个可疑正则时,按正文指引"按需"读取。它可以是详尽的、篇幅不受限的。
# ReDoS 危险模式判据
## 星级标准(基于回溯步数的理论分析)
- ⭐⭐⭐ 指数级:嵌套量词叠加,如 (a+)+、(a*)*,输入长度 n 时回溯 O(2^n)
- ⭐⭐ 多项式级:重叠交替叠加量词,如 (a|a)*,回溯 O(n^2)
- ⭐ 线性放大:贪婪通配叠加,如 .*.*,常数级恶化但通常可接受
## 关键判别:输入是否可控
模式的危险度 = 回溯复杂度 × 输入可控度。
- 若正则只匹配代码内常量 → 即便 ⭐⭐⭐ 也无需告警
- 若正则匹配 request.body / 用户上传文本 → ⭐ 也要修
## 常见误报
- (\d{4})+ :定长子项,无回溯,安全
- (?:abc)+ :定长子项,安全
- 字符类 [a-z]+ 后跟 + :字符类不重叠时安全
reference/rewrite-patterns.md 则是修复对照表,例如把 ^(a+)+$ 改写成原子组 ^(?>a+)$ 或换用 string-argv 这类线性匹配。这两份文档共同构成了 Skill 的"专家大脑"。
Skill 文件就位后,重启或新开一个 Claude Code 会话即可自动注册。验证触发是否正常,是最容易踩坑的一环。
# 1. 确认 Skill 被发现
claude # 进入会话后输入
> /skills # 列出所有已注册 Skill,应能看到 regex-guard
调试触发率的三板斧:
/regex-guard 可手动激活(前提是未设 disable-model-invocation)。ANTHROPIC_LOG=debug 环境变量,能看到 Skill 是否进入候选集、为何被选中或排除。常见问题:改了 SKILL.md 却不生效——Claude Code 对 Skill 有缓存,需新开会话或重载。脚本路径要用相对 Skill 根目录的写法,模型默认在 Skill 目录下执行。
以一个 Express 项目为例,模拟一次完整的扫描:
# 项目结构
my-api/
├── src/
│ ├── validators.js # 含 const phone = /^(\\d+)+$/
│ └── routes/user.js
└── .claude/skills/regex-guard/ # 我们的 Skill
在仓库根目录打开 Claude Code,自然语言提问:
帮我审查一下 src 目录里的输入校验,看看有没有正则方面的隐患。
模型应自动触发 regex-guard,调用脚本初筛、读取 reference 做精确判断,最终产出类似报告:
| 文件 | 行 | 正则 | 风险 | 等级 | 建议 |
|---|---|---|---|---|---|
| src/validators.js | 18 | /^(\d+)+$/ |
嵌套量词,指数级回溯 | ⭐⭐⭐ | 改写为 /^\d+$/ 或 ^(?>\d+)$ |
修复建议附带最小 diff:
- const phone = /^(\d+)+$/;
+ const phone = /^\d+$/;
至此一个"会自己判断、会调脚本、会查专家文档、会输出规范报告"的 Skill 就闭环跑通了。
flowchart TD
A[用户提问:正则有隐患吗?] --> B{模型扫描所有 Skill 的 description}
B -->|命中 regex-guard| C[加载 SKILL.md 正文 第二层]
B -->|未命中| Z[零成本,不加载]
C --> D[按正文工作流调用 scripts 扫描]
D --> E{需要精确分析?}
E -->|是| F[读取 reference/evil-patterns.md 第三层]
E -->|否| G[直接输出报告]
F --> H[读取 reference/rewrite-patterns.md]
H --> G
G --> I[Markdown 报告 + 修复 diff]
图中三条虚线对应渐进式披露的三个层级:平时只有 description 在场,命中后才加载正文,深入分析才读 reference。每一层都意味着上下文成本,所以"能下沉就下沉"是 Skill 设计的第一原则。
.claude/skills/ 并提交到 Git;用户级 ~/.claude/skills/ 只对本机生效。PreToolUse 事件。[!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 等场景全覆盖。
