<!-- title: 造日志脱敏Skill：PII一键打码 -->

## 概述

本教程带你从零开发一个名为 `pii-masker` 的 Claude Code 自定义 Skill：它能扫描日志、配置文件中的手机号、邮箱、身份证、IP、银行卡、API 密钥等敏感信息，并自动打码，让你敢把生产日志贴出来排查、敢把配置文件提交进仓库。学完你将掌握 Skill 的目录结构、`SKILL.md` frontmatter 写法、描述触发机制、脚本挂载与渐进式披露（progressive disclosure）四大核心机制。

**适用人群**：已经能用 Claude Code 跑通基本任务、想把自己重复的"工作流"固化成模型可调用的能力、并对 Python/正则有基本了解的开发者与运维同学。

---

## 前置条件

- 已安装 Claude Code（`claude --version` 能输出版本号即可，本地或远端均可）
- 安装 Python 3.9+（脚本用 Python 实现，纯标准库，无需 pip 安装任何依赖）
- 对正则表达式有基本概念；不懂也没关系，文中给出可直接复制的现成模式
- 一个含测试日志的目录（下面会帮你造一份样例）

---

## 一、先想清楚：Skill 到底解决什么问题

很多人把 Skill 和"提示词模板""slash 命令""MCP 工具"混为一谈。它们的边界其实很清晰：

- **提示词模板**：你要手动复制粘贴，模型不会主动用。
- **Slash 命令**：你要显式敲 `/foo` 触发，本质是"你说一句，它做一件"。
- **MCP 工具**：给模型接外部系统的函数调用，偏"动作"和"数据源"。
- **Skill**：把一套"专家级工作流 + 判断准则 + 可调用脚本"打包成模型能**按需自动加载**的能力。模型读到你的任务描述，判断"这事儿我有现成技能"，就主动调用，不需要你显式喊名字。

Skill 最核心的设计哲学只有一句话：**描述（description）决定何时触发，正文（body）决定怎么做**。模型在每轮对话开始时只会扫描所有 Skill 的 `name` 和 `description`（成本极低），只有判断"相关"时才会把正文加载进上下文。这就是 Anthropic 反复强调的渐进式披露。

我们的 `pii-masker` 解决的真实痛点是：

1. 排查线上 Bug 时想把日志贴给 Claude，但日志里全是手机号和 token，贴出去就违规。
2. 提交配置文件进 Git 前，担心混入密钥。
3. 手动一个个找、手动改太慢，且容易漏。

---

## 二、Skill 目录结构总览

一个标准的 Skill 长这样：

```
pii-masker/                       # Skill 名就是目录名
├── SKILL.md                      # 唯一必需的入口文件
├── scripts/
│   └── mask.py                   # 挂载的可执行脚本
└── references/
    └── patterns.md               # 渐进式披露：详细正则表放这里
```

Skill 有两个作用域：

- **项目级**：`.claude/skills/pii-masker/`，跟着仓库走，团队成员 clone 下来即可共享。
- **用户级**：`~/.claude/skills/pii-masker/`，跨所有项目可用，适合放个人偏好类技能。

我们的脱敏器属于"团队都要用"的工程规范，放项目级更合适。下面动手。

---

## 三、第一步：建立目录骨架

在任意项目根目录执行：

```bash
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 —— description 是灵魂

`SKILL.md` 由两段组成：YAML frontmatter（元数据）+ Markdown 正文（指令）。

```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/` 目录和二进制文件。
- 对每条规则单独计数，输出汇总表。
```

**这段为什么这么写——三个关键点**：

1. **description 要写"做什么 + 何时用 + 带关键词"**。模型匹配靠的就是这里的语义和关键词。我们把 `sanitize`、`脱敏`、`PII`、`before sharing`、`before committing` 这些用户真实会说的词都塞进去了，命中率会显著提高。描述最好控制在 3–5 句之内，太长反而稀释信号。

2. **正文里给"何时使用 / 如何使用 / 行为准则"**。这三段是模型最常参考的，写清楚能避免它乱跑脚本。

3. **详细正则外移到 `references/`**。这就是渐进式披露：模型默认只读 SKILL.md 主体，遇到需要"调正则或定制规则"的深入需求时，才去读 `references/patterns.md`，节省上下文。

**常见问题：Skill 触发不稳定？** 多半是 description 太笼统（如只写"a tool to process files"）。改成"scan ... for PII/secrets and mask them"，并列举用户原话关键词，触发率立刻上来。

---

## 五、第三步：写打码脚本 scripts/mask.py

这是 Skill 的"执行手"。一个完整、纯标准库、可直接运行的实现：

```python
#!/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 目录外造一份带敏感信息的样例日志测试：

```bash
cat > /tmp/app.log <<'EOF'
2026-07-04 user=张三 phone=13912345678 login from 10.20.30.40
2026-07-04 email=zhangsan@corp.com 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
```

**常见问题**：

- **手机号误伤**：某些订单号或时间戳恰好是 11 位数字会被当成手机号。解决：用 `(?<!\d)` 和 `(?!\d)` 零宽断言确保手机号前后不是数字（脚本里已加），并在规则里要求"前后是空格/标点"等上下文。
- **API 密钥正则没命中**：注意源文件里是 `api_key = sk-...` 还是 `api-key:sk-...`，脚本已兼容 `:`/`=`、空格与引号；若你用的是自定义 header 名，在 `RULES` 里加一条即可。
- **大文件卡顿**：脚本对 >5MB 的文件直接跳过，避免把日志灌爆。

---

## 六、第四步：渐进式披露——把"细节"放进 references/

在 `references/patterns.md` 写详细正则与定制指南：

```markdown
# 脱敏规则正则与定制指南

| 类型 | 正则（简版） | 打码样式 |
|------|-------------|---------|
| 手机号 | `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`。这就是渐进式披露的实战价值——**正文教模型"怎么用"，参考文件教模型"怎么改"**。

---

## 七、第五步：装载、触发与迭代

1. **重启会话或新开一个 Claude Code 窗口**，让它重新扫描 `.claude/skills/`。
2. **验证被发现**：在对话框输入 `/` 或直接问"你有哪些 skill"，应能看到 `pii-masker`。
3. **自然语言触发测试**：把那份 `/tmp/app.log` 拖进来，说"帮我把这份日志里的敏感信息脱敏一下再给我"。模型若识别成功，会主动调用 `mask.py --dry-run`，给你报告，再问你要不要写盘。

**触发不灵的诊断清单**：

| 现象 | 原因 | 解决 |
|------|------|------|
| 完全不被发现 | 目录/文件名错、无 SKILL.md | 确认路径与文件名 |
| name 报错 | 含空格/下划线/大写 | 改成 kebab-case |
| 被发现但不主动用 | description 关键词与用户原话不匹配 | 补充用户真实说法的关键词 |
| 用了但乱跑命令 | 正文没写清调用方式与边界 | 强化"行为准则"小节 |

---

## 八、实战案例：把"上线前脱敏"固化成一条命令

假设团队规范要求：所有要外发的日志必须先脱敏。我们把整套流程串起来：

```bash
# 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。

这样模型每次帮你在会话里处理日志时，都会自觉先脱敏，形成"人 + 模型 + 工具"的闭环防线。

---

## 九、整体工作流程图

```mermaid
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[完成: 文件已脱敏可安全外发]
```

---

## 十、延伸阅读

- **Anthropic 官方 Skill 文档**：`docs.claude.com/en/docs/agents-and-tools/agent-skills` —— frontmatter 字段、命名规范、作用域的权威定义。
- **MCP vs Skills vs Slash 命令选型**：当你拿不准"这个需求该做成 Skill 还是 MCP"时，官方有个决策矩阵值得一看。
- **skill-creator 技能**：本环境自带的 `skill-creator` 技能能帮你半自动生成脚手架、跑 eval、优化 description 触发率，进阶时强烈推荐。
- **正则安全实践**：脱敏规则的边界取决于行业合规要求（PCI-DSS、GDPR、个保法），落地前务必对照当地法规调整"保留前几位/后几位"的策略。

---

## 十一、训练营适用性评估

- **企业内部 LLM/AI 工程训练营（中级）**：★★★★★ 最契合。学员有 Claude Code 基础，本教程能在一节实操课内交付一个可工程化复用的合规工具，且天然带出"安全 + 效率"双重价值，便于向企业方汇报。
- **安全合规专项训练营**：★★★★☆ 把 `RULES` 替换成企业自己的数据分级规则即可作为合规自动化案例，建议增加一节"误报/漏报评估"环节。
- **零基础通识训练营**：★★☆☆☆ 偏难。正则与脚本对零基础学员门槛偏高，可作为"展示型"案例而非"动手型"作业。
- **预计教学时长**：动手约 90 分钟（含造样例、写 frontmatter、写脚本、跑测试、做触发率优化），加上 30 分钟讨论"Skill 与 MCP 的边界"，共约 2 课时。

---

## 参考来源

1. [Build a Claude Skill from Scratch: Step-by-Step Tutorial (digitalapplied.com)](https://www.digitalapplied.com/blog/build-claude-skill-from-scratch-tutorial-step-by-step-2026) — 从空目录到完整 Skill，涵盖 frontmatter、references、scripts、allowed-tools 与作用域。
2. [How to Build a Claude Code Skill from Scratch (DEV Community)](https://dev.to/raxxostudios/how-to-build-a-claude-code-skill-from-scratch-433b) — 目录结构、提示词设计、slash 命令与发布流程实战。
3. [How to create Skills for Claude (claude.com 官方博客)](https://claude.com/blog/how-to-create-skills-key-steps-limitations-and-examples) — 创建步骤、限制与示例的官方 5 步指南。
4. [How to Create Custom Claude Skills: Complete Tutorial 2026 (skillsmd.pro)](https://skillsmd.pro/blog/create-custom-claude-skills) — 渐进式披露、description 触发机制与最佳实践的完整教程。
5. [Anthropic 官方 Agent Skills 文档 (docs.claude.com)](https://docs.claude.com/en/docs/agents-and-tools/agent-skills) — Skill 结构、frontmatter 字段、项目/用户作用域的权威定义。

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