<!-- title: 让Skill调用外部脚本：scripts工程化 -->

# Claude Code Skills 进阶：让 Skill 调用外部脚本——scripts 目录工程化实战

## 概述

大多数 Skill 教程停在"写一个 `SKILL.md`、填好 `description`、等 Claude 自动触发"。但这只触及了 Skill 能力的表层。Skill 的真正工程化价值在于：它可以在目录里**捆绑可执行脚本**，把确定性的、可测试的、不占 token 的逻辑"下沉"到脚本里，让 Claude 只负责它最擅长的——理解、编排和判断。本教程聚焦 `scripts/` 子目录的工程化设计，覆盖跨工作目录定位、脚本 IO 契约、工具授权三大支柱，并以一个可运行的 `repo-vitals` 仓库体检 Skill 贯穿始终。适合已能写出基础 Skill、想让 Skill 从"纯指令"进化为"指令+可执行代码"混合体的开发者。

## 前置条件

- Claude Code v2.x（`/run`、`/verify` 等捆绑 Skill 需要 v2.1.145+，本教程核心机制在更早版本即可）
- 能手写一个带 frontmatter 的基础 `SKILL.md`
- 熟悉 Bash 基本语法与 Unix 管道
- 一个 git 仓库用来测试（实战案例会调用 `git` 命令）

## 一、核心认知：Skill 从来不只是 `SKILL.md`

很多人对 Skill 的心智模型是"一个 Markdown 文件"。这个模型来自 Slash Command 时代的肌肉记忆。但 Skill 标准的目录结构远比这丰富。Anthropic 官方文档给出的标准布局是：

```
my-skill/
├── SKILL.md           # 主指令（必需，入口）
├── reference.md       # 详细 API 文档（按需加载）
├── examples.md        # 用法示例（按需加载）
├── templates/
│   └── template.txt   # 模板文件
└── scripts/
    └── validate.sh    # Claude 可执行的脚本
```

注意最后一行：`scripts/` 下的脚本是 **"executed, not loaded"（被执行，而非被加载）**。这句话是整个工程化设计的纲领——`reference.md` 这类文件会被 Claude **读进上下文**当知识用，而 `scripts/` 下的文件会被 Claude **当作程序调用**，输出结果回流到对话。

逆向工程博主 Mikhail Shilkov 在抓取真实 Claude Code 会话后给出了更底层的解释：当 Skill 被触发时，Claude Code 会在注入的指令块里附带一行 **`Base Path: /Users/username/.claude/skills/pdf/`**。正是这个 base path 让 Claude 能定位并执行与 Skill 捆绑在一起的脚本，而脚本的相对路径就锚定在这个目录上。

所以一个成熟 Skill 的本质是**"声明（SKILL.md）+ 实现（scripts/）+ 参考（reference.md）"**的三层结构。本教程聚焦最容易被忽略、却最能提升确定性的中间层。

## 二、关键决策：三种"代码承载"方式的选择矩阵

在写 Skill 时，任何一段逻辑都有三种承载方式。选错会导致 Skill 又慢、又贵、又不可靠。先建立这张决策矩阵，后面所有实战才有判断依据。

| 承载方式 | 形态 | 谁执行 | 何时进上下文 | 适合的逻辑 |
|---|---|---|---|---|
| **① 纯指令** | SKILL.md 正文里的步骤描述 | Claude（调用其工具） | 触发即常驻整轮会话 | 判断、推理、编排、需要理解语义的步骤 |
| **② 内联注入** | `!`git diff HEAD`` 形式的命令 | Claude Code 宿主 | 触发前**先执行**，输出替换该行后 Claude 才看到 | 取一小段实时数据（diff、当前分支、时间戳） |
| **③ 外部脚本** | `scripts/foo.sh` + `${CLAUDE_SKILL_DIR}` 引用 | Claude 通过 Bash 工具调用 | **永不进上下文**，只回流 stdout | 多步骤、确定性、可测试、输出量大的统计/转换 |

第 ③ 种就是本教程的主角。判断一段逻辑该不该"下沉"到脚本，用这五条清单逐条对照：

1. **确定性优先**——逻辑不能因为模型状态不同而给出不同结果（如"统计文件行数"必须永远精确）。
2. **token 成本高**——如果让 Claude 用 Read 工具逐文件读再数行数，会烧掉大量上下文。
3. **可独立测试**——脚本能脱离 Claude 单独跑、单独写单测。
4. **输出结构化**——脚本能吐出机器友好的文本（key:value、Markdown 表格、JSON），让 Claude 只做"解读"。
5. **跨模型稳定**——脚本逻辑不依赖任何模型的"聪明程度"，换 Haiku 也能跑对。

满足 2 条以上，就该下沉。`repo-vitals` 案例同时满足全部 5 条。

## 三、第一根支柱：`${CLAUDE_SKILL_DIR}` 跨工作目录定位

下沉到脚本后，第一个会踩的坑是路径。Claude 的工作目录（cwd）取决于你从哪里启动 `claude`，可能是仓库根，也可能是某个子目录。如果你的 SKILL.md 写死 `python scripts/foo.py`，而 cwd 不是 Skill 目录，调用就会 404。

官方给出的标准解法是 `${CLAUDE_SKILL_DIR}` 字符串替换变量——它永远展开为**当前 Skill 的 `SKILL.md` 所在目录**，与 cwd 完全无关。在动态注入命令里这样用：

````markdown
## 执行体检

!`bash "${CLAUDE_SKILL_DIR}/scripts/vitals.sh"`
````

无论你从哪个目录触发这个 Skill，`${CLAUDE_SKILL_DIR}` 都会被正确替换成 `~/.claude/skills/repo-vitals/`（个人级）或 `.claude/skills/repo-vitals/`（项目级）。这是连接"指令层"和"实现层"的桥梁。

> 配套的还有 `${CLAUDE_SESSION_ID}`（会话 ID，适合做日志文件名）和 `${CLAUDE_EFFORT}`（当前 effort 等级，可让脚本按 low/high 调整行为）。它们都通过同样的字符串替换机制工作。

## 四、第二根支柱：脚本的 IO 契约——stdin / stdout / 退出码

脚本是"黑盒程序"，Claude 只通过三个通道与它交互。把契约设计干净，Skill 才稳定。

**stdout = 数据通道**。Claude 读到的就是脚本 stdout 的全部内容。因此脚本应当只把"要给 Claude 解读的结构化结果"打到 stdout，所有噪音（进度、调试）一律走 stderr。一条简单的纪律：

```bash
echo "tracked: 142"        # stdout —— Claude 会看到
echo "debug: scanning..." >&2  # stderr —— 只在终端可见，不污染上下文
```

**退出码 = 健康信号**。`0` 表示正常完成，非 `0` 表示异常。Claude 会据此判断"脚本是否成功"。设计时让退出码有语义：

```bash
exit 0   # 正常，stdout 是有效数据
exit 2   # 依赖缺失（如没装 git）
exit 3   # 输入非法
```

**stdin = 可选输入**。需要处理大段输入时（如把文件内容喂给脚本），用 stdin 比命令行参数更安全（避免转义地狱）。SKILL.md 里可以引导 Claude：`echo "$content" | bash "${CLAUDE_SKILL_DIR}/scripts/parse.sh"`。

三条契约之外，再加一条工程纪律：**脚本开头永远写 `set -euo pipefail`**。未定义变量、管道中段失败时立即终止，避免把脏数据当正常结果回吐给 Claude。

## 五、第三根支柱：`allowed-tools` 让脚本免审批运行

Claude 调用你的脚本走的是 `Bash` 工具，默认每次都要弹权限审批。在一个会跑多次的 Skill 里，反复审批会打断节奏。`allowed-tools` frontmatter 字段解决这个问题——它声明 Skill 激活时预批准的工具，格式与 `settings.json` 的权限规则一致：

```yaml
allowed-tools: Bash(git *) Bash(bash *)
```

但要理解一个关键区分（官方原文）：`allowed-tools` **授予的是"免审批"权限，不是"限制可用工具"**。也就是说，列出 `Bash(git *)` 只是让 git 命令免审批，但 Claude 仍可调用其他任何工具。如果你想反过来**禁止**某些工具，要用 `disallowed-tools`：

```yaml
disallowed-tools: AskUserQuestion   # 自动循环型 Skill，禁止它打断问人
```

安全提示：项目级 Skill（`.claude/skills/`）里的 `allowed-tools` 要在你**接受工作区信任对话框**后才生效——和 `.claude/settings.json` 里的权限规则同理。审阅陌生仓库时，先看它的 Skill 给自己批了什么权限，一个写 `allowed-tools: Bash`（无参数=全开）的 Skill 等于自授予完整 shell，值得警惕。

## 六、实战：构建 `repo-vitals` 仓库体检 Skill

现在把三根支柱拼起来，做一个可运行的 Skill。它的分工清晰地体现"下沉"哲学：**脚本负责确定性地数数（git/文件统计），Claude 负责判断这些数字是否健康**。让 LLM 去逐个 Read 文件数行数是又慢又贵又不可靠的；脚本一秒搞定且永远精确。

### 步骤 1：创建目录结构

```bash
mkdir -p ~/.claude/skills/repo-vitals/scripts
cd ~/.claude/skills/repo-vitals
```

### 步骤 2：编写 `scripts/vitals.sh`

这是核心实现层。它用 git 与标准 Unix 工具采集仓库指标，全部走 stdout 输出结构化 Markdown：

```bash
#!/usr/bin/env bash
# scripts/vitals.sh — 仓库健康度统计，确定性输出，仅供 Claude 解读
set -euo pipefail

# 依赖检查：没有 git 直接退出码 2，stderr 报错
if ! command -v git >/dev/null 2>&1; then
  echo "error: git not found" >&2
  exit 2
fi

# 非 git 仓库退出码 3
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
  echo "error: not a git repository" >&2
  exit 3
fi

echo "## Repository Vitals"
echo ""
echo "- **tracked files**: $(git ls-files | wc -l)"

# 代码行数（去空行），仅统计常见源码后缀
code_lines=$(git ls-files \
  | grep -iE '\.(py|js|ts|tsx|go|rs|java|c|cpp|h|rb|sh|sql)$' \
  | xargs -r grep -hcve '^\s*$' 2>/dev/null \
  | awk '{s+=$1} END {print s+0}')
echo "- **code lines (non-blank)**: ${code_lines}"

echo "- **TODO/FIXME occurrences**: $(git grep -hiE 'TODO|FIXME' 2>/dev/null | wc -l)"
echo "- **contributors**: $(git shortlog -sne HEAD 2>/dev/null | wc -l)"
echo "- **last commit**: $(git log -1 --format='%ci (%cr)' 2>/dev/null)"

echo ""
echo "### Top 5 largest tracked files (by line count)"
git ls-files 2>/dev/null \
  | xargs -r wc -l 2>/dev/null \
  | grep -v ' total$' \
  | sort -rn | head -n 5 \
  | awk '{printf "- `%s` — %s lines\n", $2, $1}'
```

给脚本加可执行权限：

```bash
chmod +x ~/.claude/skills/repo-vitals/scripts/vitals.sh
```

**预期结果**：在任意 git 仓库里单独跑 `bash ~/.claude/skills/repo-vitals/scripts/vitals.sh`，会输出一段干净的 Markdown，类似：

```
## Repository Vitals

- **tracked files**: 187
- **code lines (non-blank)**: 12453
- **TODO/FIXME occurrences**: 34
- **contributors**: 4
- **last commit**: 2026-06-22 14:31:00 +0800 (23 hours ago)

### Top 5 largest tracked files (by line count)
- `src/legacy/monolith.go` — 2104 lines
- `src/parser/engine.py` — 1587 lines
...
```

**常见问题**：
- `xargs: unmatched` 或行数统计为 0 → 多半是没有匹配后缀的文件，`xargs -r` 加 `s+0` 兜底已处理，输出会是 `0` 而非报错。
- 中文路径文件导致 `wc` 列错位 → 实战中可改用 `git ls-files -z | xargs -0` 处理特殊字符，但会增加复杂度，本例保持简洁。

### 步骤 3：编写 `SKILL.md` 编排层

```yaml
---
name: repo-vitals
description: Runs a repository health checkup and interprets the metrics. Use when the user asks about code health, project vitals, TODO/FIXME debt, largest files, contributor count (bus factor), or wants a project "health report".
allowed-tools: Bash(git *) Bash(bash *) Bash(wc) Bash(grep) Bash(sort) Bash(awk)
shell: bash
---

## Current vitals

!`bash "${CLAUDE_SKILL_DIR}/scripts/vitals.sh"`

## Instructions

The metrics above were produced deterministically by a script — do not re-count files yourself. Interpret them and write a concise health report:

1. **Bus factor risk**: if contributors ≤ 2, flag it as a concentration risk.
2. **Technical-debt signal**: normalize TODO/FIXME count against code lines (per 1k lines). >10/k lines = high, >3 = moderate.
3. **File-size smell**: any single file > 1000 lines is a candidate for splitting; name the worst offenders from the Top 5 list.
4. **Staleness**: if last commit is older than 30 days, note the project may be inactive.

Keep the report under 200 words, lead with the single biggest risk, end with one concrete next action. If the script exited non-zero (output shows "error:"), report the error instead of guessing.
```

注意三个工程要点：第一，`!`...`` 动态注入会在 Claude 看到指令前**先执行脚本**，所以 Claude 拿到的 SKILL.md 里那一行已经是真实数据，不是待执行的命令——这是"实时数据落地"的标准手法。第二，`Instructions` 明确告诉 Claude "不要自己数文件"，防止它无视脚本结果去重复劳动。第三，`allowed-tools` 用 `Bash(git *)` `Bash(bash *)` 这种带通配的精确授权，而非裸 `Bash`，把免审批范围压到最小。

### 步骤 4：测试与验证

进入任意 git 仓库，启动 `claude`，两种方式触发：

- **手动**：`/repo-vitals`
- **自动**：问"我这个项目健康吗？"或"帮我看看仓库的 TODO 债务"——`description` 里的触发词会让 Claude 自动加载

Claude Code 会实时检测到新 Skill 目录（无需重启），输出一份基于真实数据的解读报告。你应当看到报告中**数字与脚本独立运行的输出完全一致**，而风险判断（如"bus factor 低""monolith.go 该拆分"）是 Claude 基于数字的推理——这正是"脚本数数、Claude 判断"分工的体现。

## 七、进阶：`scripts` 与 `reference` 的加载语义对比

容易混淆的一对概念：`scripts/helper.py` 和 `reference.md` 都是 Skill 目录下的支持文件，但加载语义截然相反。

- `reference.md` / `examples.md`：**按需加载（loaded when needed）**。Claude 用 Read 工具读取，内容进上下文当知识。适合放 API 细节、长篇规范。官方建议 `SKILL.md` 本身控制在 500 行以内，超出的细节挪到这类文件。
- `scripts/*.sh`：**执行不加载（executed, not loaded）**。Claude 用 Bash 工具调用，只有 stdout 回流，源码本身不进上下文。

这条区分决定了 token 经济性。一段 500 行的正则逻辑：写进 `reference.md` 会被 Claude 读完、占数千 token 且每次触发都占；写进 `scripts/` 则 Claude 只看到一行输出。**知识放 reference，过程放 scripts**——这条原则能让大型 Skill 的上下文成本骤降。

## 八、整体工作流程

```mermaid
flowchart TD
    A["用户提问 / /repo-vitals"] --> B["Claude Code 匹配 description"]
    B --> C["注入 SKILL.md + Base Path"]
    C --> D["渲染前先执行 !`命令`"]
    D --> E["调用 scripts/vitals.sh\n走 Bash 工具"]
    E --> F{"退出码"}
    F -->|0| G["stdout 结构化数据\n替换到指令行"]
    F -->|非0| H["stderr 报错\nClaude 转入错误处理"]
    G --> I["Claude 解读数据\n输出健康报告"]
    H --> I
    E -.->|"allowed-tools\n免审批"| J["权限层"]
    C -.->|"${CLAUDE_SKILL_DIR}\n定位脚本"| E
```

数据从用户提问流向最终报告，脚本层在中间承担"确定性采集"，`allowed-tools` 和 `${CLAUDE_SKILL_DIR}` 作为两条横向支撑贯穿全流程。

## 九、工程权衡总结：下沉还是不下沉

把本文的决策逻辑浓缩成一张速查表：

| 你的逻辑特征 | 推荐承载 | 理由 |
|---|---|---|
| 需要语义理解、创造性输出 | ① 纯指令 | 这是 LLM 的主场 |
| 取一小段实时状态（diff、分支） | ② 内联注入 `!`cmd`` | 轻量、一次性 |
| 多步统计、格式转换、确定性计算 | ③ scripts/ 脚本 | 精确、省 token、可测试 |
| 长篇参考知识、API 文档 | reference.md | 按需加载，不每次占上下文 |

一个反面案例值得记住：如果你的"逻辑"本质上需要 Claude 的判断力（比如"这段代码有没有安全问题"），强行写成脚本反而是反模式——脚本无法替代语义理解，只会把不确定性藏进正则里。**下沉只针对确定性逻辑，判断永远留给 Claude。**

## 训练营适用性评估

- **适合类型**：面向有 1–3 年经验的后端/DevOps 工程师的"Claude Code 进阶"或"AI 工程化"训练营；也适合企业内部"AI 辅助研发提效"工作坊。不适合零基础营（需先掌握基础 Skill 编写）。
- **预计教学时长**：2–2.5 小时。其中决策矩阵讲解约 30 分钟，`repo-vitals` 实战约 90 分钟（含学员自行扩展指标），权衡讨论约 30 分钟。
- **可拓展练习**：让学员为 `vitals.sh` 增加"依赖文件检查""许可证扫描"指标；把输出从 Markdown 改为 JSON，让 Claude 做更结构化的解读；用 `${CLAUDE_SESSION_ID}` 把每次体检结果落盘成历史趋势文件。
- **前置依赖**：建议在"基础 Skill 创建"和"动态注入"两节课之后讲授，作为承接"指令层"到"实现层"的桥梁课。

## 参考来源

1. [Extend Claude with skills — Claude Code 官方文档](https://code.claude.com/docs/en/skills) — 权威规范，覆盖 frontmatter 全字段（`allowed-tools`/`disallowed-tools`/`shell`/`arguments`）、`${CLAUDE_SKILL_DIR}` 字符串替换、scripts 目录"executed not loaded"语义与工作区信任机制
2. [Inside Claude Code Skills: Structure, prompts, invocation — Mikhail Shilkov](https://mikhail.io/2025/10/claude-code-skills/) — 基于真实会话抓取的逆向分析，揭示 Skill 触发时注入的 `Base Path` 机制与目录结构约定
3. [bash-script-skill: Template for Bash-based Claude Skills — skills.rest](https://skills.rest/skill/bash-script-skill) — 社区维护的 Bash 脚本型 Skill 模板，展示 `scripts/bash/main.sh` 的标准组织模式与退出码文档约定
4. [my-claude-skills / CLAUDE_CODE_SKILL_TUTORIAL.md — StrayDragon (GitHub)](https://github.com/StrayDragon/my-claude-skills/blob/main/CLAUDE_CODE_SKILL_TUTORIAL.md) — 实操教程，对比 `scripts/`（utility，执行）与 `templates/`（模板）的目录分工
5. [The Complete .claude Directory Guide for Claude Code [2026] — ComputingForGeeks](https://computingforgeeks.com/claude-code-dot-claude-directory-guide/) — 2026 年更新的 `.claude` 目录全景指南，覆盖 skills/commands/agents/hooks 的层级与覆盖优先级

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