本教程带你从零开发一个名为 pii-masker 的 Claude Code 自定义 Skill:它能扫描日志、配置文件中的手机号、邮箱、身份证、IP、银行卡、API 密钥等敏感信息,并自动打码,让你敢把生产日志贴出来排查、敢把配置文件提交进仓库。学完你将掌握 Skill 的目录结构、SKILL.md frontmatter 写法、描述触发机制、脚本挂载与渐进式披露(progressive disclosure)四大核心机制。
适用人群:已经能用 Claude Code 跑通基本任务、想把自己重复的"工作流"固化成模型可调用的能力、并对 Python/正则有基本了解的开发者与运维同学。
claude --version 能输出版本号即可,本地或远端均可)很多人把 Skill 和"提示词模板""slash 命令""MCP 工具"混为一谈。它们的边界其实很清晰:
/foo 触发,本质是"你说一句,它做一件"。Skill 最核心的设计哲学只有一句话:描述(description)决定何时触发,正文(body)决定怎么做。模型在每轮对话开始时只会扫描所有 Skill 的 name 和 description(成本极低),只有判断"相关"时才会把正文加载进上下文。这就是 Anthropic 反复强调的渐进式披露。
我们的 pii-masker 解决的真实痛点是:
一个标准的 Skill 长这样:
pii-masker/ # Skill 名就是目录名
├── SKILL.md # 唯一必需的入口文件
├── scripts/
│ └── mask.py # 挂载的可执行脚本
└── references/
└── patterns.md # 渐进式披露:详细正则表放这里
Skill 有两个作用域:
.claude/skills/pii-masker/,跟着仓库走,团队成员 clone 下来即可共享。~/.claude/skills/pii-masker/,跨所有项目可用,适合放个人偏好类技能。我们的脱敏器属于"团队都要用"的工程规范,放项目级更合适。下面动手。
在任意项目根目录执行:
mkdir -p .claude/skills/pii-masker/scripts
mkdir -p .claude/skills/pii-masker/references
cd .claude/skills/pii-masker
预期结果:ls -R 能看到上面那棵树。Claude Code 启动时会自动扫描 .claude/skills/ 下每个含 SKILL.md 的目录。
常见问题:目录建好了但 Skill 不被识别?99% 的情况是 SKILL.md 还没创建,或 name 字段命名非法(只允许小写字母、数字、连字符,如 pii-masker,不能有空格和下划线)。
SKILL.md 由两段组成:YAML frontmatter(元数据)+ Markdown 正文(指令)。
---
name: pii-masker
description: Scans log files, config files, and pasted text for PII and secrets (Chinese phone numbers, emails, 18-digit ID cards, IPv4 addresses, bank card numbers, API keys/tokens) and masks them in place or to a new file. Use when the user wants to sanitize/脱敏 logs before sharing, remove sensitive data from configs before committing, or generate a PII report. Supports dry-run preview.
---
# PII Masker(敏感信息脱敏器)
## 何时使用
用户想"把日志发出去前先脱敏""配置文件提交前清掉密钥""给这份文件打码"时主动调用。
## 如何使用
1. 默认干跑(dry-run):只报告发现哪些敏感数据、各多少条,不改动文件。
2. 确认后,运行真正的打码,原地替换或输出到新文件。
## 调用脚本
对单个或一批文件执行脱敏:
- 干跑预览:`python3 scripts/mask.py --path <file-or-dir> --dry-run`
- 原地打码:`python3 scripts/mask.py --path <file-or-dir> --inplace`
- 输出到新文件:`python3 scripts/mask.py --path <file> --out <newfile>`
## 打码规则(概要)
- 手机号 → `138****1234`;邮箱 → `j***@example.com`;身份证 → `110101************`;
- IPv4 → `10.*.*.*`;银行卡 → `****-****-****-1234`;API 密钥 → `sk-************`。
- 完整正则与定制方法见 `references/patterns.md`。
## 行为准则
- 永远先 `--dry-run` 给用户看报告,得到确认再写盘。
- 不要去碰 `.git/` 目录和二进制文件。
- 对每条规则单独计数,输出汇总表。
这段为什么这么写——三个关键点:
description 要写"做什么 + 何时用 + 带关键词"。模型匹配靠的就是这里的语义和关键词。我们把 sanitize、脱敏、PII、before sharing、before committing 这些用户真实会说的词都塞进去了,命中率会显著提高。描述最好控制在 3–5 句之内,太长反而稀释信号。
正文里给"何时使用 / 如何使用 / 行为准则"。这三段是模型最常参考的,写清楚能避免它乱跑脚本。
详细正则外移到 references/。这就是渐进式披露:模型默认只读 SKILL.md 主体,遇到需要"调正则或定制规则"的深入需求时,才去读 references/patterns.md,节省上下文。
常见问题:Skill 触发不稳定? 多半是 description 太笼统(如只写"a tool to process files")。改成"scan ... for PII/secrets and mask them",并列举用户原话关键词,触发率立刻上来。
这是 Skill 的"执行手"。一个完整、纯标准库、可直接运行的实现:
#!/usr/bin/env python3
"""PII Masker —— 日志/配置敏感信息脱敏,纯标准库实现。"""
import argparse
import os
import re
import sys
# 每条规则:(名字, 编译好的正则, 打码函数)
RULES = [
("中国手机号", re.compile(r"(?<!\d)1[3-9]\d{9}(?!\d)"),
lambda m: m.group()[:3] + "****" + m.group()[-4:]),
("邮箱", re.compile(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}"),
lambda m: m.group()[0] + "***@" + m.group().split("@", 1)[1]),
("18位身份证号", re.compile(
r"(?<!\d)[1-9]\d{5}(?:19|20)\d{2}"
r"(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\d|3[01])\d{3}[\dXx](?!\d)"),
lambda m: m.group()[:6] + "************"),
("IPv4地址", re.compile(r"(?<!\d)(?:\d{1,3}\.){3}\d{1,3}(?!\d)"),
lambda m: m.group().split(".", 1)[0] + ".*.*.*"),
("银行卡号", re.compile(r"(?<!\d)(?:\d[ -]?){15,18}(?!\d)"),
lambda m: "****-****-****-" + re.sub(r"[^\d]", "", m.group())[-4:]),
("API密钥/Token", re.compile(
r"(?i)(?:api[_-]?key|access[_-]?token|secret|password|bearer|sk-)"
r"\s*[:=]\s*['\"]?([A-Za-z0-9_\-]{16,})['\"]?"),
lambda m: m.group(1)[0] + "-" + "*" * (len(m.group(1)) - 4)
+ m.group(1)[-4:] if m.lastindex else "********"),
]
def mask_text(text):
"""对一段文本执行全部规则,返回 (脱敏后文本, {规则名: 命中数})。"""
counts = {}
out = text
for name, pat, fn in RULES:
def replace(m, fn=fn, name=name):
counts[name] = counts.get(name, 0) + 1
return fn(m)
out = pat.sub(replace, out)
return out, counts
def iter_files(path):
if os.path.isfile(path):
yield path
return
for root, dirs, files in os.walk(path):
dirs[:] = [d for d in dirs if d != ".git"]
for f in files:
full = os.path.join(root, f)
if os.path.getsize(full) > 5 * 1024 * 1024: # 跳过 >5MB
continue
try:
with open(full, "rb") as fh:
fh.read(2048).decode("utf-8") # 二进制则跳过
yield full
except (UnicodeDecodeError, OSError):
continue
def main():
ap = argparse.ArgumentParser(description="PII 脱敏器")
ap.add_argument("--path", required=True, help="文件或目录")
g = ap.add_mutually_exclusive_group(required=True)
g.add_argument("--dry-run", action="store_true", help="只报告,不改写")
g.add_argument("--inplace", action="store_true", help="原地替换")
g.add_argument("--out", help="输出到新文件(仅单文件)")
args = ap.parse_args()
if args.out and os.path.isdir(args.path):
sys.exit("--out 仅支持单文件")
total = {}
for fp in iter_files(args.path):
with open(fp, encoding="utf-8") as fh:
text = fh.read()
new_text, counts = mask_text(text)
if not counts:
continue
for k, v in counts.items():
total[k] = total.get(k, 0) + v
if args.dry_run:
print(f"[预览] {fp}: {counts}")
elif args.inplace:
with open(fp, "w", encoding="utf-8") as fh:
fh.write(new_text)
print(f"[已脱敏] {fp}: {counts}")
elif args.out:
with open(args.out, "w", encoding="utf-8") as fh:
fh.write(new_text)
print(f"[输出] {args.out}: {counts}")
print("\n===== 汇总 =====")
if not total:
print("未发现敏感信息。")
for k, v in sorted(total.items(), key=lambda x: -x[1]):
print(f"{k:12s} {v}")
return 0 if total else 1
if __name__ == "__main__":
sys.exit(main())
预期结果:先在 Skill 目录外造一份带敏感信息的样例日志测试:
cat > /tmp/app.log <<'EOF'
2026-07-04 user=张三 phone=13912345678 login from 10.20.30.40
2026-07-04 [email protected] id=110101199001011234
2026-07-04 api_key = sk-abcdef0123456789abcdef0123456789
EOF
python3 .claude/skills/pii-masker/scripts/mask.py --path /tmp/app.log --dry-run
输出形如:
[预览] /tmp/app.log: {'中国手机号': 1, 'IPv4地址': 1, '邮箱': 1, '18位身份证号': 1, 'API密钥/Token': 1}
===== 汇总 =====
API密钥/Token 1
IPv4地址 1
18位身份证号 1
中国手机号 1
邮箱 1
去掉 --dry-run 改 --out /tmp/app.masked.log,再 cat 新文件,你会看到:
2026-07-04 user=张三 phone=139****5678 login from 10.*.*.*
2026-07-04 email=z***@corp.com id=110101************
2026-07-04 api_key = sk-************6789
常见问题:
(?<!\d) 和 (?!\d) 零宽断言确保手机号前后不是数字(脚本里已加),并在规则里要求"前后是空格/标点"等上下文。api_key = sk-... 还是 api-key:sk-...,脚本已兼容 :/=、空格与引号;若你用的是自定义 header 名,在 RULES 里加一条即可。在 references/patterns.md 写详细正则与定制指南:
# 脱敏规则正则与定制指南
| 类型 | 正则(简版) | 打码样式 |
|------|-------------|---------|
| 手机号 | `1[3-9]\d{9}` | 138****1234 |
| 邮箱 | `[A-Za-z0-9._%+-]+@...` | j***@domain.com |
| 身份证 | `[1-9]\d{5}(19\|20)\d{2}...` | 110101************ |
| IPv4 | `(?:\d{1,3}\.){3}\d{1,3}` | 10.*.*.* |
| 银行卡 | `(?:\d[ -]?){15,18}` | ****-****-****-1234 |
| API密钥 | `(api_key\|token\|secret)...` | sk-************ |
## 新增规则
在 scripts/mask.py 的 RULES 列表追加一个三元组 (名字, 编译正则, 打码函数) 即可。
## 行业差异
- 金融:银行卡按 PCI-DSS 保留前6后4。
- 医疗:身份证按 HIPAA 等价规则保留前6。
- 海外:手机号改用 E.164 格式 `+\d{6,15}`。
这样做的好处:模型默认不读它,节省上下文;当用户问"我想加一条欧盟手机号的规则"时,模型才会主动打开这份参考,照着"新增规则"小节改 mask.py。这就是渐进式披露的实战价值——正文教模型"怎么用",参考文件教模型"怎么改"。
.claude/skills/。/ 或直接问"你有哪些 skill",应能看到 pii-masker。/tmp/app.log 拖进来,说"帮我把这份日志里的敏感信息脱敏一下再给我"。模型若识别成功,会主动调用 mask.py --dry-run,给你报告,再问你要不要写盘。触发不灵的诊断清单:
| 现象 | 原因 | 解决 |
|---|---|---|
| 完全不被发现 | 目录/文件名错、无 SKILL.md | 确认路径与文件名 |
| name 报错 | 含空格/下划线/大写 | 改成 kebab-case |
| 被发现但不主动用 | description 关键词与用户原话不匹配 | 补充用户真实说法的关键词 |
| 用了但乱跑命令 | 正文没写清调用方式与边界 | 强化"行为准则"小节 |
假设团队规范要求:所有要外发的日志必须先脱敏。我们把整套流程串起来:
# 1) 拉一段线上日志
kubectl logs my-svc-pod > raw.log
# 2) 干跑看报告(让 Claude 自动调用 pii-masker)
# 在 Claude Code 里直接说:"扫描 raw.log 看有哪些敏感信息"
# → 模型运行 mask.py --path raw.log --dry-run
# 3) 确认后输出脱敏版
python3 .claude/skills/pii-masker/scripts/mask.py \
--path raw.log --out raw.masked.log
# 4) 安全外发
cat raw.masked.log
更进一步,你可以在项目的 CLAUDE.md 里写一行项目约定:
外发任何日志/配置前,必须先用
pii-masker技能干跑确认无 PII。
这样模型每次帮你在会话里处理日志时,都会自觉先脱敏,形成"人 + 模型 + 工具"的闭环防线。
flowchart TD
A[用户对话: 帮我脱敏这份日志] --> B{模型扫描所有 Skill 的 description}
B -->|关键词命中 脱敏/PII| C[加载 pii-masker 的 SKILL.md 正文]
B -->|未命中| Z[走普通对话流程]
C --> D[默认执行 mask.py --dry-run]
D --> E[输出命中报告: 手机号×3 邮箱×2 ...]
E --> F{用户确认写盘?}
F -->|是| G[执行 --inplace 或 --out]
F -->|需要改规则| H[加载 references/patterns.md]
H --> I[按指南修改 mask.py 的 RULES]
I --> D
G --> J[完成: 文件已脱敏可安全外发]
docs.claude.com/en/docs/agents-and-tools/agent-skills —— frontmatter 字段、命名规范、作用域的权威定义。skill-creator 技能能帮你半自动生成脚手架、跑 eval、优化 description 触发率,进阶时强烈推荐。RULES 替换成企业自己的数据分级规则即可作为合规自动化案例,建议增加一节"误报/漏报评估"环节。[!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 等场景全覆盖。
