<!-- title: 造技术债扫描Skill：TODO注释秒出债清单 -->

## 概述

本教程带你从零打造一个**技术债扫描 Skill**：它能扫描代码库里所有 `TODO / FIXME / HACK / XXX` 注释，结合 `git blame` 算出每条债的**作者**和**债龄（沉淀了多少天）**，并按"严重度 + 陈旧度"排序，自动产出一份技术债体检报告。适合已经装好 Claude Code、想学会**自定义 Skill 这项核心扩展能力**的开发者。学完后你将掌握 Skill 的目录结构、frontmatter 字段、动态上下文注入（`!`command``）、支撑脚本、参数替换与触发调优——这套方法论可直接套用到任何重复性工作流上。

---

## 一、前置条件

| 项目 | 要求 |
|------|------|
| Claude Code | 已安装，版本建议 v2.1.145+（本教程用到 `!` 动态注入与 `${CLAUDE_SKILL_DIR}` 替换） |
| 终端环境 | macOS / Linux，或开启了 `CLAUDE_CODE_USE_POWERSHELL_TOOL` 的 Windows |
| 工具链 | `grep`、`git`、`awk`、`date`（系统自带） |
| 测试仓库 | 任意一个 git 仓库，有几个 TODO 注释最佳 |

核心认知（一句话）：**Skill 就是一个带 `SKILL.md` 的文件夹**。`SKILL.md` 顶部是 YAML frontmatter（告诉 Claude "我是什么、何时启用我"），下面是 Markdown 正文（Claude 执行时要遵循的指令）。文件夹名就是你敲的命令名。

---

## 二、先理解 Skill 的"渐进式披露"机制

很多人误以为 Skill 是把一大段提示词塞进每次对话——恰恰相反。Skill 的精髓是**按需加载**：

```mermaid
flowchart LR
    A[会话启动] --> B[只加载各 Skill 的<br/>description 摘要<br/>≈ 极低成本]
    B --> C{是否被触发?}
    C -- 用户敲 /命令 --> D[加载完整 SKILL.md 正文]
    C -- Claude 判断描述匹配 --> D
    C -- 不匹配 --> E[正文永不加载<br/>零成本]
    D --> F[执行动态注入 !命令]
    F --> G[把脚本输出 + 指令<br/>一起交给 Claude]
    G --> H[Claude 产出最终结果]
```

关键推论：**description 是常驻的"目录索引"，正文是触发后才花的钱**。所以 `description` 要写好（决定能否被正确触发），正文要写精（一旦加载就持续占 token）。这与 `CLAUDE.md` 的"事实常驻"形成互补——当一个 `CLAUDE.md` 段落长成了"操作流程"，就该抽成 Skill。

---

## 三、第 1 步：创建 Skill 目录结构

个人级 Skill 放在 `~/.claude/skills/` 下（所有项目可用），项目级放 `.claude/skills/`（仅当前项目）。我们做一个**个人级**的：

```bash
mkdir -p ~/.claude/skills/tech-debt-scan
cd ~/.claude/skills/tech-debt-scan
```

最终目录长这样：

```
~/.claude/skills/tech-debt-scan/
├── SKILL.md              # 必需：frontmatter + 指令
└── scan_tech_debt.sh     # 可选：支撑脚本（干脏活的）
```

> 文件夹名 `tech-debt-scan` 决定了命令名是 `/tech-debt-scan`。frontmatter 里的 `name` 字段只是**显示标签**，不改命令名。

---

## 四、第 2 步：写支撑脚本 `scan_tech_debt.sh`（干脏活）

Skill 的设计哲学：**让脚本做机械、确定的事（grep + blame），让 Claude 做需要判断的事（排序、归类、给建议）**。两者分工，既稳又省 token。

把下面内容存为 `~/.claude/skills/tech-debt-scan/scan_tech_debt.sh`：

```bash
#!/usr/bin/env bash
# scan_tech_debt.sh — 扫描技术债标记，附加 git blame 的作者与债龄
set -euo pipefail

ROOT="${1:-.}"          # $1 为空时默认扫当前目录
cd "$ROOT"

# 1) grep 出所有 TODO/FIXME/HACK/XXX，过滤常见无关目录与二进制后缀
grep -rnIE '\b(TODO|FIXME|HACK|XXX)\b[: ]' \
  --include='*.py' --include='*.js' --include='*.ts' --include='*.tsx' \
  --include='*.java' --include='*.go' --include='*.rs' \
  --exclude-dir=node_modules --exclude-dir=.git \
  --exclude-dir=dist --exclude-dir=build --exclude-dir=.next \
  . 2>/dev/null || true \
| while IFS=: read -r file lineno content; do
    [ -f "$file" ] || continue

    # 2) git blame 该行，取作者与提交时间戳
    blame=$(git blame --line-porcelain "$file" -L "$lineno,$lineno" 2>/dev/null || true)
    author=$(awk '/^author /{print $2" "$3; exit}' <<<"$blame")
    ts=$(awk '/^author-time /{print $2; exit}' <<<"$blame")

    # 3) 把时间戳换算成"债龄天数"
    days="-"
    if [[ "$ts" =~ ^[0-9]+$ ]]; then
      days=$(( ( $(date +%s) - ts ) / 86400 ))
    fi

    # 4) 严重度初判：FIXME/HACK = 高，其余 = 低（留给 Claude 再细化）
    sev="低"
    if grep -qE '\bFIXME\b|\bHACK\b' <<<"$content"; then sev="高"; fi

    content=$(printf '%s' "$content" | sed 's/^[[:space:]]*//; s/|/\\|/g')
    # 输出：严重度 | 文件:行号 | 债龄 | 作者 | 内容
    printf '%s|%s:%s|%s天|%s|%s\n' "$sev" "$file" "$lineno" "$days" "$author" "$content"
  done
```

赋可执行权限：

```bash
chmod +x ~/.claude/skills/tech-debt-scan/scan_tech_debt.sh
```

**预期结果**：在任一 git 仓库里手工跑 `~/.claude/skills/tech-debt-scan/scan_tech_debt.sh`，会吐出形如 `高|src/auth.py:42|128天|张三|FIXME 密码明文比对应换 bcrypt` 的管道分隔行。脚本能独立跑通，是 Skill 稳定的前提。

**常见问题**：
- *报 `grep: ... No such file`* → 去掉某个 `--include` 即可，或确认目录有对应后缀文件。
- *`days` 全是 `-`* → 该目录不是 git 仓库，`git blame` 失败；先 `git init` 或换到仓库根。
- *作者显示乱码* → 部分系统 `awk` 取 `$2 $3` 不够，可改用 `git log` 的 `%an`，但对大多数场景足够。

---

## 五、第 3 步：写 `SKILL.md`（frontmatter + 指令 + 动态注入）

这是核心文件。把下面内容存为 `~/.claude/skills/tech-debt-scan/SKILL.md`：

```markdown
---
name: 技术债扫描
description: 扫描代码库的 TODO/FIXME/HACK/XXX 注释，结合 git blame 算出债龄与作者，输出按严重度和陈旧度排序的技术债清单。在用户问"技术债""还有多少 TODO""哪些 FIXME 该先还""清理技术债"时使用。
argument-hint: [扫描根目录，默认当前目录]
allowed-tools: Bash Read
---

## 待分析的原始债清单

扫描脚本输出（格式：严重度 | 文件:行号 | 债龄 | 作者 | 内容）：

\`\`\`
!`${CLAUDE_SKILL_DIR}/scan_tech_debt.sh "$ARGUMENTS"`
\`\`\`

## 你的任务

把上面的原始清单整理成一份**技术债体检报告**：

1. **总览**：报总数，并按标记类型（TODO/FIXME/HACK/XXX）与严重度分桶统计。
2. **明细表**：输出 Markdown 表格，列 `严重度 | 债龄 | 文件:行号 | 作者 | 内容摘要`，**按"严重度降序、债龄降序"排序**——FIXME/HACK 且超过 90 天的排最前。
3. **Top 3 必还债**：逐条说明为什么危险（FIXME=已知缺陷，HACK=脆弱实现，XXX=待定危险区），并给处理方向（删除 / 重写 / 补测试 / 转工单）。
4. **空结果**：若脚本输出为空，明确回复"未发现技术债标记，工作树干净"。

`$ARGUMENTS` 是用户传入的扫描根目录；为空时脚本默认扫当前目录。
```

### 逐段拆解

**Frontmatter 字段**（全部可选，仅 `description` 强烈推荐）：

| 字段 | 作用 |
|------|------|
| `name` | 列表里的显示名（不影响命令名） |
| `description` | **最关键**。Claude 用它判断何时自动触发。`description` + `when_to_use` 合计在列表里被截断到 **1536 字符**，所以**关键用例放最前** |
| `argument-hint` | 敲命令时自动补全里显示的参数提示，如 `[扫描根目录]` |
| `allowed-tools` | 该 Skill 激活期间，Claude 用这些工具**免确认**。这里放 `Bash Read`，因为要跑脚本、可能读文件 |

**动态上下文注入**（最该掌握的特性）：

```
!`命令`        # 行内注入：Claude Code 先执行命令，再用输出替换这一行
```!            # 代码围栏注入：整块执行并替换
```

我们的脚本通过 `${CLAUDE_SKILL_DIR}` 引用——这是 Claude Code 内置替换变量，**永远指向 SKILL.md 所在目录**，跟当前工作目录无关，保证脚本路径不会因你在哪个项目里而失效。

**参数替换**：`$ARGUMENTS` 代表用户敲命令时传的全部参数。例如敲 `/tech-debt-scan ./apps/web`，`$ARGUMENTS` 就是 `./apps/web`。还有 `$0`/`$1`（按位置取）、`$name`（命名参数，需配合 `arguments` 字段）等。

---

## 六、第 4 步：测试两种调用方式

进入任意 git 仓库（先故意留几个 TODO），启动 `claude`，然后：

**方式 A——显式调用**（最可靠）：

```
/tech-debt-scan
```

或带参数扫子目录：

```
/tech-debt-scan ./src
```

**方式 B——自然语言触发**（靠 description 匹配）：

```
帮我看看项目里欠了多少技术债，哪些 FIXME 最该先还？
```

Claude 会自动匹配到这个 Skill 并执行。两种方式都应输出类似下面的报告：

```
## 技术债体检报告

共发现 7 处技术债标记：TODO×4、FIXME×2、HACK×1；高严重度 3 条。

| 严重度 | 债龄 | 文件:行号 | 作者 | 内容摘要 |
|--------|------|-----------|------|----------|
| 高 | 128天 | src/auth.py:42 | 张三 | FIXME 密码明文比对应换 bcrypt |
| 高 | 96天  | utils/cache.ts:11 | 李四 | HACK 临时绕过空指针，上线前必须修 |
| 低 | 30天  | api/order.go:8 | 王五 | TODO 加幂等校验 |
| ... | | | | |

### Top 3 必还债
1. **auth.py:42 密码明文比对**（FIXME / 128天）——已知安全缺陷且超期 4 个月，建议本周改用 bcrypt，并补一组对照测试。
2. **cache.ts:11 临时绕过**（HACK / 96天）——脆弱实现，建议定位空指针根因后删除该分支。
3. ... 
```

> Claude Code 有**热加载**：你改了 `SKILL.md` 或脚本，无需重启，下一条消息即生效。

---

## 七、第 5 步：调优 description，避免"不触发"或"乱触发"

这是新手最容易踩的坑。官方给出三条排障口诀：

1. **Skill 不触发** → `description` 没覆盖用户真实说法。补上用户会怎么问的原话（"还有多少 TODO""清理技术债"），就像我们已经做的那样。
2. **Skill 触发太频繁** → 描述太宽泛（例如只写"分析代码"）。收紧到具体动作 + 触发场景。
3. **描述被截断** → 关键用例必须放最前，别铺垫背景。1536 字符是硬上限。

如果你希望这个 Skill **只手动调用、绝不自动触发**（比如它是个会改文件的重型流程），加一行：

```yaml
disable-model-invocation: true
```

反之，若想隐藏某个"背景知识型" Skill、不让它出现在 `/` 菜单，用 `user-invocable: false`。两者组合能精细控制"谁能触发它"。

---

## 八、实战案例：5 分钟跑通完整流程

新建一个最小演示仓库：

```bash
mkdir ~/debt-demo && cd ~/debt-demo
git init -q && git config user.email "demo@x.com" && git config user.name "demo"

cat > auth.py <<'EOF'
def login(pwd):
    # FIXME 明文比对密码，存在时序攻击风险，换 bcrypt
    return pwd == "admin"
    # TODO 记录失败次数，连续 5 次锁定账号
EOF

cat > cache.ts <<'EOF'
// HACK 这里直接吞掉了空值，先让 demo 跑起来，上线前必须修
export function get(k){ return store[k] ?? null }
// XXX 缓存过期策略待定
EOF

git add -A && git commit -qm "init demo with debt"
```

然后：

```bash
claude
# 进入交互后输入：
/tech-debt-scan
```

**预期**：报告会准确识别 4 条债（2 高 2 低），并指出 `auth.py` 的 FIXME 因为是已知缺陷应优先处理。注意：因为是当天提交，`债龄` 会显示 `0天`——这验证了 `git blame` 链路打通；真实老仓库里你会看到几十上百天的沉淀债。

---

## 九、进阶：让大型仓库扫描更稳、更省 token

代码库很大时，有三个增强方向：

**1. 跑在子代理里（隔离上下文）**。把扫描的大量原始输出关进独立上下文，只把报告回传主会话：

```yaml
context: fork
agent: Explore
```

`context: fork` 让 Skill 在 fork 出的子代理中运行；`agent: Explore` 用只读探索型子代理，安全且不污染主对话历史。

**2. 用 `paths` 限定生效范围**。只在编辑特定文件时才让 Claude 自动加载它：

```yaml
paths: "**/*.{py,ts,tsx,go,java,rs}"
```

**3. 拆支撑文件**。若脚本膨胀，可拆成 `scan_tech_debt.sh` + `format_report.py`，在 `SKILL.md` 里注明每个文件做什么、何时加载，正文保持精简。

记住一个成本铁律：**Skill 正文一旦加载，跨多轮都留在上下文里**。所以正文要"说要做什么"，而非"讲为什么、讲历史"，每多一行都是反复支出的 token。

---

## 十、整套工作流程总览

```text
flowchart TD
    U[用户: 帮我看看技术债] --> M{Claude 匹配 description?}
    M -->|匹配| L[加载 SKILL.md 正文]
    M -->|用户敲 /tech-debt-scan| L
    L --> S[替换变量: ${CLAUDE_SKILL_DIR}, $ARGUMENTS]
    S --> I[动态注入: 运行 scan_tech_debt.sh]
    I --> R[拿到原始管道分隔清单]
    R --> C[Claude 排序/归类/点名 Top3]
    C --> O[输出技术债体检报告]
    I -.失败.-> DBG[排查: chmod? git 仓库? 后缀?]
```

---

## 延伸阅读

- **官方 Skill 文档**（`code.claude.com/docs/en/skills`）——字段与替换变量的权威来源，建议通读"Frontmatter reference"和"Available string substitutions"两节。
- **`/code-review`、`/debug`、`/loop` 等内置 Skill**——直接看它们怎么写 description 和分工脚本，是最好的范本。
- **Agent Skills 开放标准**——Skill 不止 Claude Code 能用，跨多个 AI 工具通用；写成标准格式可移植性最好。
- **`skill-creator` 工具**——想系统化做 Skill 并跑 eval 评估触发准确率，用它一键生成骨架 + 度量效果。

---

## 训练营适用性评估

| 维度 | 评估 |
|------|------|
| 适合的训练营类型 | **AI 工程提效 / 研发效能 / 命令行工具开发**类训练营；也适合"AI + DevOps"主题作为实操单元 |
| 学员基础 | 会基本命令行（grep/git）+ 用过 Claude Code 即可；无需编程语言专精 |
| 教学时长 | **约 90–120 分钟**：30 分钟讲机制 + 45 分钟动手造 + 30 分钟调优与排障 |
| 难度定位 | ⭐⭐⭐（中偏低）——核心是"配置 + 脚本"，不涉及复杂编程，但 frontmatter 调优需反复试错 |
| 可拓展练习 | ① 改成扫描 `console.log`/`print` 遗留；② 加 `paths` 做多语言仓库；③ 接 `gh issue create` 把 Top 债直接转成 GitHub 工单 |
| 评估产出 | 学员能独立交付一个"扫描+归因+排序+建议"的可用 Skill，并演示自然语言触发 |

---

## 参考来源

1. [Extend Claude with skills — Claude Code 官方文档](https://code.claude.com/docs/en/skills) — Skill 目录结构、frontmatter 全字段、动态上下文注入与字符串替换变量的权威定义。
2. [Claude Code Skill Frontmatter: Every YAML Option Explained](https://allahabadi.dev/blogs/ai/claude-code-skills-frontmatter-complete-guide/) — 逐字段图解 15 个 frontmatter 选项，含触发控制的取舍建议。
3. [Chapter 2: Custom Skills — Claude Code Handbook](https://joeyyu23.github.io/claude-code-handbook/en/book2-advanced/02-custom-skills) — 从机制到实践的系统性进阶讲解，覆盖支撑文件与子代理执行。
4. [How to create a Claude Code skill (and the best practices nobody put in)](https://codemeetai.substack.com/p/how-to-create-a-claude-code-skill) — 社区总结的 description 撰写与触发调优实战经验。
5. [Creating custom skills — Claude.ai Documentation](https://claude.com/docs/skills/how-to) — 自定义 Skill 的官方 how-to 指南，含从零创建到分享的完整流程。

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