<!-- title: 有状态Skill开发：跨会话续跑 -->

> 本教程解决一个被普遍忽视的难题：**Skill 本身是无状态的**。你会学到如何用文件系统 + 状态脚本 + Hook，在无状态架构上"搭出"一个能跨会话续跑的有状态 Skill，并做出一个可运行的批量迁移追踪器。

---

## 一、概述

Claude Code 的 Skill 是扩展能力的核心载体，但它有一个被多数教程回避的本质特性：**Skill 在会话之间是无状态的**——每次启动都是一张白纸。本教程教你突破这一限制。

**学习目标**

- 理解 Skill 的"无状态"架构本质与三阶段渐进加载机制
- 掌握用文件系统作为唯一事实源、构建跨会话状态的设计模式
- 学会用 `SessionStart` / `Stop` 两类 Hook 实现自动续跑与断点恢复
- 完成一个可运行的"批量迁移追踪 Skill"，支持中断后续跑

**适用人群**：已能创建基础 `SKILL.md`、希望做长流程自动化（批量重构、框架升级、CI 迁移、文档生成）的中高级开发者。

---

## 二、前置条件

| 项 | 要求 |
|---|---|
| Claude Code | v2.1.0+，CLI 可正常启动 |
| 基础 | 会写最简 `SKILL.md`（frontmatter + 正文） |
| 运行时 | Node.js 18+（Hook 脚本用）、`jq`（状态读写用） |
| 心智模型 | 理解"上下文窗口""会话""Hook 事件"三个概念 |

> 不必精通 Hook，本教程会给出可直接复制的 `settings.json` 片段。

---

## 三、正文

### 步骤 1：先认清"无状态"这个底牌

很多 Skill 用着用着就"失忆"了：上一次迁移到一半，新开个会话它又从头开始。这不是 bug，而是架构设计。

官方文档与社区技术深度解析一致指出，Skill 具有以下本质特征：

- **会话间无持久化**：每个会话（或 API 请求）拿到的是隔离的干净环境，Skill 执行完即随环境销毁，不残留任何内存状态。
- **文件系统是唯一持久化通道**：`~/.claude/skills/`（个人级）、`.claude/skills/`（项目级）、插件目录三处文件会跨会话存在。
- **三阶段渐进加载**：会话启动只载入 `name` + `description`（约 30–50 token / Skill）；被判定相关时才载入 `SKILL.md` 正文（通常 < 5000 token）；引用文件与脚本按需读取，**脚本代码本身不进上下文，只有脚本输出消耗 token**。

这条最后的事实极其关键：**把状态读写的复杂逻辑放进脚本，是"免费的"**——因为脚本源码不占上下文，只有它打印的结果才占。这正是有状态 Skill 的工程支点。

**预期结果**：建立"Skill 无状态 → 用文件 + 脚本补状态"的心智模型。

**常见问题**

- *为什么不直接用 CLAUDE.md 记状态？* CLAUDE.md 是"原则/规则"层，每会话全量注入，把频繁变化的任务进度塞进去会污染上下文、抬升 token 成本。状态应单独成文件，按需读取。
- *能不能让 Skill 自己"记住"上次结果？* 不能靠 Skill 自身。必须显式落盘，下次显式读取。

### 步骤 2：设计状态文件与目录骨架

我们要做一个 **migration-tracker**：把某个目录下的旧 API 调用批量迁移到新实现（例如 `mysql_*` → PDO），42 个文件，可能跨多个会话才能干完。

状态文件用一个简单 JSON，字段语义清晰、可被 `jq` 操作：

```json
{
  "skill_version": "1.0.0",
  "created_at": "2026-06-15T10:00:00Z",
  "updated_at": "2026-06-15T11:30:00Z",
  "goal": "将 src/legacy 下所有 mysql_* 调用迁移到 PDO",
  "status": "in_progress",
  "total": 42,
  "done": 17,
  "queue": ["src/legacy/UserRepo.php", "src/legacy/OrderRepo.php"],
  "completed": ["src/legacy/Auth.php", "src/legacy/Session.php"],
  "current_file": "src/legacy/UserRepo.php",
  "current_line": 128,
  "lessons": ["mysql_query 第三参数是连接句柄，PDO 用 prepare+execute 替代"],
  "last_session_summary": "完成 Auth/Session，修复 3 处 SQL 注入"
}
```

目录骨架（标准分层，符合渐进披露）：

```
.claude/skills/migration-tracker/
├── SKILL.md                  # 轻量指令，编排工作流
├── scripts/
│   ├── state.sh             # 状态读写（jq 实现，代码不进上下文）
│   ├── session-brief.js     # SessionStart hook：注入续跑简报
│   └── checkpoint.js        # Stop hook：落盘摘要
└── references/
    └── state-schema.md      # 字段语义详解（按需读取）
```

> 状态文件本身不放 Skill 目录里（那是"能力"定义），而放业务位置 `.claude/migration-state/state.json`，与代码一起进版本控制，团队共享进度。

### 步骤 3：编写状态读写脚本（核心）

这是整个 Skill 的"状态引擎"。用 `jq` 保证原子性与可组合性：

```bash
#!/usr/bin/env bash
# scripts/state.sh — migration-tracker 状态引擎
# 用法: state.sh <init|show|next|done|checkpoint|summary>
set -euo pipefail

STATE_DIR="${MIGRATION_STATE_DIR:-.claude/migration-state}"
STATE_FILE="$STATE_DIR/state.json"
mkdir -p "$STATE_DIR"

stamp() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }

case "${1:-help}" in
  init)
    goal="${2:?usage: state.sh init <goal>}"
    printf '%s' "{\"skill_version\":\"1.0.0\",\"created_at\":\"$(stamp)\",\"goal\":\"$goal\",\"status\":\"in_progress\",\"total\":0,\"done\":0,\"queue\":[],\"completed\":[],\"current_file\":null,\"current_line\":0,\"lessons\":[],\"last_session_summary\":null}" > "$STATE_FILE"
    echo "✓ state initialized at $STATE_FILE"
    ;;
  show) cat "$STATE_FILE" ;;
  enqueue)
    # 从 stdin 接收文件列表，并入 queue 并去重
    tmp=$(mktemp); cat > "$tmp"
    jq --slurpfile q "$tmp" '.queue = ((.queue + $q[0]) | unique) | .total = (.queue|length)' "$STATE_FILE" > "$STATE_FILE.new"
    mv "$STATE_FILE.new" "$STATE_FILE"; rm -f "$tmp"
    echo "✓ enqueued, total=$(jq '.total' "$STATE_FILE")"
    ;;
  next)
    # 返回第一个"在 queue 但不在 completed"的文件
    jq -r '(.queue - .completed)[0] // "QUEUE_EMPTY"' "$STATE_FILE"
    ;;
  checkpoint)
    file="${2:?}"; line="${3:-0}"
    jq --arg f "$file" --argjson l "$line" --arg t "$(stamp)" \
      '.current_file=$f | .current_line=$l | .updated_at=$t' "$STATE_FILE" > "$STATE_FILE.new"
    mv "$STATE_FILE.new" "$STATE_FILE"
    echo "✓ checkpoint $file:$line"
    ;;
  done)
    file="${2:?}"
    jq --arg f "$file" --arg t "$(stamp)" \
      '.completed += [$f] | .done = (.completed|length) | .current_file = null | .updated_at=$t' \
      "$STATE_FILE" > "$STATE_FILE.new"
    mv "$STATE_FILE.new" "$STATE_FILE"
    echo "✓ completed $file, progress=$(jq -r '.done + "/" + (.total|tostring)' "$STATE_FILE")"
    ;;
  summary)
    text="${2:?usage: state.sh summary <text>}"
    jq --arg s "$text" --arg t "$(stamp)" \
      '.last_session_summary=$s | .updated_at=$t' "$STATE_FILE" > "$STATE_FILE.new"
    mv "$STATE_FILE.new" "$STATE_FILE"
    echo "✓ summary saved"
    ;;
  *) echo "commands: init|show|enqueue|next|checkpoint|done|summary"; exit 1 ;;
esac
```

**设计要点（幂等性）**

- `next` 用集合差 `queue - completed`，重复调用结果一致，天然幂等。
- 每次写都走 `*.new` 临时文件 + `mv` 原子替换，避免中断写坏 JSON。
- `checkpoint` 在处理一个文件**之前**落盘断点，保证最坏情况只丢"当前这一个文件的当前这一行之后"的进度。

**预期输出**

```bash
$ bash scripts/state.sh next
src/legacy/UserRepo.php
$ bash scripts/state.sh checkpoint src/legacy/UserRepo.php 128
✓ checkpoint src/legacy/UserRepo.php:128
```

**常见问题**

- *`jq` 报 parse error？* 多半是中途被 Ctrl-C 打断直接写了半截 JSON。原子替换（`.new` + `mv`）正是为此——要么旧值完整、要么新值完整，不会出现半截。
- *文件太多 `enqueue` 慢？* 没关系，脚本是子进程，不占你的上下文 token，慢只影响墙钟时间。

### 步骤 4：编写轻量 SKILL.md 指令

正文要"薄"——把复杂逻辑下沉到脚本，SKILL.md 只负责**编排**与**触发词**。`description` 是触发命中的唯一判据，必须写满场景与关键词：

```markdown
---
name: migration-tracker
description: 跟踪并执行跨文件的代码迁移、批量重构、框架升级、命名/ API 批量替换等长流程任务。当用户说"批量迁移""重构整个目录""把 X 全部换成 Y""升级到新 API""续跑迁移""继续上次的迁移"时使用。维护跨会话状态文件，支持从断点恢复，不会重复处理已完成文件。
allowed-tools: Read, Edit, Bash, Glob, Grep
---

# 迁移追踪 Skill

## 核心铁律：无状态架构上的状态工程
你跨会话无记忆。本 Skill 用 `.claude/migration-state/state.json` 作为唯一事实源。
**绝不在内存里维护进度——一律落盘，下次一律先读盘。**

## 工作流
1. **恢复或初始化**：`bash scripts/state.sh show`
   - 报错"不存在"→ 询问迁移目标 → `scripts/state.sh init "<目标>"`，
     再用 `Glob`/`Grep` 扫描受影响文件，管道喂给 `scripts/state.sh enqueue`。
2. **取下一项**：`next=$(bash scripts/state.sh next)`。
   若返回 `QUEUE_EMPTY` → 置 `status=done`，汇总报告，结束。
3. **先落断点**：`bash scripts/state.sh checkpoint "$next" 0`（处理前）。
4. **执行**：`Read` 该文件 → `Edit` 完成迁移。
5. **标记完成**：`bash scripts/state.sh done "$next"`；遇到非平凡坑写入 lessons。
6. **回到第 2 步**循环。

字段语义见 `references/state-schema.md`（仅当需要扩展字段时再读）。
```

**为什么这么薄？** 因为 Stage 2 加载正文要占 token，正文越薄，常驻成本越低；真正的复杂度都在脚本里（Stage 3 按需，且代码不入上下文）。

**常见问题**

- *Skill 装了却不触发？* 99% 是 `description` 太泛（如"分析代码"）。改成场景化长句（如上）命中率立刻上升。
- *`allowed-tools` 要不要给 `Write`？* 不给。状态写都走脚本，Skill 本体只需 `Read/Edit/Bash/Glob/Grep`，最小权限。

### 步骤 5：用 Hook 实现"开盖即续跑"

到这一步，Skill 已经能手动续跑了（新会话里说"继续迁移"即可）。但更优雅的是**自动**：新会话一开，自己把上次进度念出来。

Skill 不能自己注册 Hook——Hook 是 Harness 层的（`settings.json`），但 Skill 可以**自带脚本**，让用户一次性接线：

**`scripts/session-brief.js`**（SessionStart 事件，其 stdout 会作为上下文注入）：

```js
#!/usr/bin/env node
// SessionStart hook: 读取状态，输出续跑简报（stdout 注入到 Claude 上下文）
const fs = require('fs'); const path = require('path');
const f = path.join(process.cwd(), '.claude/migration-state/state.json');
if (!fs.existsSync(f)) process.exit(0);     // 无状态→静默，不污染新项目
const s = JSON.parse(fs.readFileSync(f, 'utf8'));
if (s.status === 'done') process.exit(0);   // 已完成→不打扰
const remain = (s.queue.length || s.total) - s.done;
console.log(`[migration-tracker 续跑简报] 目标: ${s.goal}`);
console.log(`进度: ${s.done}/${s.total}, 剩余 ${remain} 项, 断点: ${s.current_file || '队首'}`);
if (s.last_session_summary) console.log(`上次: ${s.last_session_summary}`);
console.log(`说"继续迁移"即可从断点恢复。`);
```

**`scripts/checkpoint.js`**（Stop 事件，主线程结束时触发，可调度后台 agent 写摘要）：

```js
#!/usr/bin/env node
// Stop hook: 会话结束前提醒落盘摘要（这里仅做轻量提示，避免阻塞）
const fs = require('fs'); const path = require('path');
const f = path.join(process.cwd(), '.claude/migration-state/state.json');
if (!fs.existsSync(f)) process.exit(0);
const s = JSON.parse(fs.readFileSync(f, 'utf8'));
if (s.status !== 'in_progress') process.exit(0);
// stdout 在 Stop 事件会作为 system message 回传给模型，触发它执行 summary 落盘
console.log(`[migration-tracker] 本会话即将结束，进度 ${s.done}/${s.total}。请用 state.sh summary "<本次做了什么>" 落盘后再结束。`);
```

**在 `settings.json` 接线**（项目级，团队共享）：

```json
{
  "hooks": {
    "SessionStart": [
      { "matcher": "*", "hooks": [
        { "type": "command",
          "command": "node .claude/skills/migration-tracker/scripts/session-brief.js" }
      ]}
    ],
    "Stop": [
      { "matcher": "", "hooks": [
        { "type": "command",
          "command": "node .claude/skills/migration-tracker/scripts/checkpoint.js" }
      ]}
    ]
  }
}
```

> 接线后**重启 Claude Code**。Skill 文件与 Hook 的修改都不会热生效——发现扫描只在初始化时发生。

**常见问题**

- *Hook 没触发？* 优先查 `command` 的相对路径与可执行权限（`chmod +x`）。可用 `claude --debug` 看加载日志，无效 YAML / 报错的脚本会**静默失败**。
- *Stop hook 阻塞退出？* Stop hook 有超时；耗时操作（如生成完整报告）应调度**后台 agent** 异步做，主线程立即返回，与社区实践一致。

### 步骤 6：测试与验证续跑

端到端验证清单：

1. `init` → `enqueue` 喂 3 个测试文件 → `show` 确认 `total=3`。
2. `next` 得文件 A → `checkpoint` → 手动 `Edit` → `done A`。
3. **Ctrl-C 中断 / 关掉会话 / 重启**。
4. 新会话一开，应自动看到 SessionStart 简报（"进度 1/3，断点..."）。
5. 说"继续迁移"，Skill 从文件 B 续跑，**不会重做 A**。
6. 全部完成后 `status=done`，简报静默退出。

关键观察：**done 集合是去重的、next 是幂等的**——这就是续跑正确性的数学保证。

---

## 四、无状态 vs 有状态：一张表看清取舍

| 维度 | 原生 Skill（无状态） | 本教程方案（有状态） |
|---|---|---|
| 跨会话记忆 | ❌ 无 | ✅ 文件系统持久化 |
| 上下文成本 | 仅 metadata | + 状态文件按需读 |
| 续跑能力 | ❌ 从头开始 | ✅ 断点恢复 |
| 复杂度 | 低 | 中（脚本 + Hook） |
| 适用场景 | 单次、原子任务 | 长流程、批量、可中断 |
| 失败模式 | 无状态可丢 | 需防"半截 JSON" |

**判断准则**：任务能在单会话内原子完成 → 用原生 Skill；任务必然跨会话、需要进度可见可恢复 → 上有状态方案。

---

## 五、实战案例：可运行的 migration-tracker

把上面零碎片段组装成一个**完整可复现**的最小项目（约 5 分钟跑通）：

```bash
# 1. 建骨架
mkdir -p .claude/skills/migration-tracker/{scripts,references}
mkdir -p .claude/migration-state
mkdir -p src/legacy && echo '<?php mysql_query("SELECT 1");' > src/legacy/Auth.php

# 2. 落脚本（粘贴步骤3、步骤5 的三个文件）
chmod +x .claude/skills/migration-tracker/scripts/state.sh

# 3. 初始化状态 + 入队
bash .claude/skills/migration-tracker/scripts/state.sh init "mysql_* → PDO"
grep -rl 'mysql_' src/legacy | bash .claude/skills/migration-tracker/scripts/state.sh enqueue

# 4. 接 Hook（粘贴步骤5 的 settings.json），重启 Claude Code

# 5. 在 Claude Code 里说："用 migration-tracker 继续迁移"
#    → 观察它读状态、取队首、checkpoint、Edit、done、循环
```

**验收标准**

- [ ] 中断后重开，SessionStart 自动打印进度简报。
- [ ] 续跑不重复处理 `completed` 里的文件。
- [ ] `state.json` 始终是合法 JSON（原子替换保证）。
- [ ] 全部完成后 `status=done`，简报不再打扰。

---

## 六、整体工作流程图

```mermaid
flowchart TD
    A[新会话启动] --> B[SessionStart Hook<br/>读 state.json]
    B --> C{state 存在且未完成?}
    C -- 否 --> Z[静默, 不打扰]
    C -- 是 --> E[注入续跑简报到上下文]
    E --> F[Stage2 加载 SKILL.md 正文]
    F --> G[state.sh next 取队首]
    G --> H{QUEUE_EMPTY?}
    H -- 是 --> K[status=done 汇总结束]
    H -- 否 --> I[checkpoint 落断点]
    I --> J[Read + Edit 执行迁移]
    J --> L[state.sh done 标记完成]
    L --> M{用户中断?}
    M -- 是 --> N[Stop Hook 提醒落 summary]
    M -- 否 --> G
    N --> O[下个会话从 A 续跑]
```

---

## 七、三条进阶优化

1. **并发安全**：多人同时跑会撞状态文件。给 `state.sh` 加 `flock` 文件锁，或把 queue 拆成 per-user 分片。
2. **状态版本演进**：字段变更时，在脚本入口做 `skill_version` 检查与一次性迁移，避免旧状态把新 Skill 带崩。
3. **蒸馏链**：把 `lessons` 升级为 `journal → lesson → rules` 三级蒸馏（参考社区 `claude-project-skill` 的做法），让踩坑经验沉淀为可复用规则，而非一次性笔记。

---

## 八、延伸阅读

- 官方 Skill 文档（创建、管理、共享）：<https://code.claude.com/docs/en/skills>
- Skill frontmatter 全字段解析：<https://allahabadi.dev/blogs/ai/claude-code-skills-frontmatter-complete-guide/>
- Skill 内部机制（调用、描述、触发）：<https://mikhail.io/2025/10/claude-code-skills/>
- 社区有状态 Skill 范例（journal→lesson→rules 蒸馏链 + Hook 实践）：<https://github.com/ZOO-AiiiPM/claude-project-skill>

---

## 九、训练营适用性评估

| 维度 | 评估 |
|---|---|
| 适合训练营 | ✅ 中高级研发提效营、AI 工程化营、企业自动化营 |
| 前置模块 | 需先完成"Skill 基础创建""Hook 入门"两节 |
| 教学时长 | 讲解 45 min + 动手实操 90 min，合计约 3.5 课时 |
| 学员产出 | 一个可带回自己项目复用的有状态 Skill 骨架 |
| 难点 | Hook 接线调试、幂等性思维、原子写 |

> 建议作为"Skill 进阶"模块的压轴课——它把"无状态"这个反直觉真相讲透后，学员对整个 Skill 体系的理解会跨一个台阶。

---

## 参考来源

1. [Extend Claude with skills — Claude Code Docs](https://code.claude.com/docs/en/skills) — 官方 Skill 创建、管理与共享权威文档
2. [Claude Code Skills: Complete Technical Deep-Dive (gregpriday/claude-code-docs)](https://github.com/gregpriday/claude-code-docs/blob/main/claude-code-skills.md) — 拆解无状态本质、三阶段渐进加载、文件系统持久化与同名优先级（personal > project > plugin）
3. [ZOO-AiiiPM/claude-project-skill](https://github.com/ZOO-AiiiPM/claude-project-skill) — 真实有状态 Skill 范例：8 份规范 + SessionStart/Stop 双 Hook + journal→lesson→rules 蒸馏链
4. [Claude Code Skill Frontmatter: Every YAML Option Explained](https://allahabadi.dev/blogs/ai/claude-code-skills-frontmatter-complete-guide/) — name/description/allowed-tools 等字段语义与校验规则
5. [Inside Claude Code Skills: Structure, prompts, invocation](https://mikhail.io/2025/10/claude-code-skills/) — description 触发机制与语义匹配实践

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