首页 › tutorials › 给Skill挂脚本:CLAUDE_SKILL_DIR实战

给 Skill 挂上可执行大脑:${CLAUDE_SKILL_DIR} 捆绑脚本实战

概述

本教程面向已经会写基础 SKILL.md、但想让 Skill 从"纯文本说明书"升级为"自带可执行工具箱"的开发者。你将理解 Claude Code Skills 的核心机制——scripts/ 目录与 ${CLAUDE_SKILL_DIR} 路径变量,掌握参数传递、权限收口(allowed-tools)、退出码处理,并亲手构建一个把"确定性统计交给 Python、把叙述性洞察交给 Claude"的真实 Skill:csv-pulse(CSV 健康体检)。适合需要把团队通用脚本封装成可复用能力的工程师与运维人员。

前置条件

如果第 2、4 条尚不满足,建议先读官方《Extend Claude with skills》文档的入门章节。


Step 1:为什么 Skill 需要捆绑脚本(动机)

很多新手做的第一个 Skill 长这样:在 SKILL.md 里用自然语言写"请检查文件格式、统计行数、找出空值",然后期待 Claude 每次都准确执行。问题在于:

  1. 自然语言会被"解释":同一句"统计空值",Claude 可能用 awk,也可能用 Python,不同实现结果甚至不同口径,团队难以复现。
  2. 关键校验必须是确定性的:金额汇总、行数、外键一致性这类"对就是对、错就是错"的检查,应交给代码而非大模型。
  3. 复杂逻辑塞进 SKILL.md 会爆上下文:几百行规则会撑大每次加载成本。

Claude Code Skills 的官方答案是把可执行代码打包进 Skill 自己的 scripts/ 子目录,再让 SKILL.md 指挥 Claude 调用它。代码负责"算得准",SKILL.md 负责"用得对"。这就是本文的主角——捆绑脚本(bundled scripts)。

官方原话点明了设计意图:脚本路径要用 ${CLAUDE_SKILL_DIR},这样"无论 Skill 装在 personal、project 还是 plugin 层级都能正确解析"。

Step 2:理解 ${CLAUDE_SKILL_DIR}(全文核心)

${CLAUDE_SKILL_DIR} 是 Claude Code 内置的字符串替换变量,在 Skill 内容被注入前,会展开为包含 SKILL.md 的那个目录的绝对路径。

它的三个关键特性决定了它就是捆绑脚本的"正确入口":

特性 含义 不用它会怎样
路径无关 Skill 装在哪都解析正确 写死 ~/.claude/skills/... 会在项目级/插件级安装时失效
绝对路径 展开后是完整路径,可直接拼接 相对路径依赖当前工作目录,CI/批处理里极易错位
注入时展开 替换发生在 Claude 读取 Skill 内容之前 你写的就是"占位符",Claude 看到的已是真实路径

配套变量还有 ${CLAUDE_SESSION_ID}(当前会话 ID,可用于日志/临时文件)和 ${CLAUDE_EFFORT}(当前努力级别 low/medium/high/xhigh/max,可据此让 Skill 在不同档位下行为不同)。但本教程聚焦 ${CLAUDE_SKILL_DIR},因为它是"让 Skill 变可执行"的唯一基石。

一个最小可用的调用长这样,写在 SKILL.md 正文里:

python3 ${CLAUDE_SKILL_DIR}/scripts/analyze.py "$ARGUMENTS"

Claude 读到这行时,${CLAUDE_SKILL_DIR} 已被替换成真实路径,它再用 Bash 工具执行——这就是"可执行大脑"的接线方式。

Step 3:目录结构、命名与权限收口

3.1 标准目录骨架

一个带脚本的 Skill 目录如下(以 csv-pulse 为例):

~/.claude/skills/csv-pulse/
├── SKILL.md                 # 入口:触发词 + 调度指令(精简)
├── scripts/
│   └── profile.py           # 确定性逻辑:解析、统计、输出 JSON
└── references/              # 可选:深度文档,按需读取
    └── output-contract.md   # 脚本输出字段的详细说明

命名硬性规则(踩坑高发区):

3.2 用 allowed-tools 收口权限

官方最被低估的一行 frontmatter 是 allowed-tools。注意:它不是"允许列表",而是"限制 Claude 在本 Skill 内只能用的工具集"(即缩窄,而非授权)。给一个只想跑 Python 的 Skill 写上 allowed-tools: Bash(python3 *),意味着 Claude 在执行本 Skill 时只能发起匹配 python3 * 的 Bash 调用——既防止它顺手 rm,也让用户侧权限提示更干净。

---
name: csv-pulse
description: Profile a CSV file's health. Use when the user asks to "analyze csv",
  "check data quality", "profile a dataset", or "体检 CSV". Outputs a Markdown report.
when_to_use: CSV data quality profiling, missing-value detection, column-type guessing
allowed-tools: Bash(python3 *)
argument-hint: [path-to-csv]
---

要点:

Step 4:参数如何从用户传到脚本

${CLAUDE_SKILL_DIR} 解决"脚本在哪",参数则解决"对谁操作"。Claude Code 支持一组字符串替换:

变量 说明
$ARGUMENTS 调用时传入的全部参数原文
$ARGUMENTS[N] / $N 按位置取参数,$0 是第一个、$1 是第二个(0 基)
$name 在 frontmatter arguments: [file, format] 中声明的命名参数,$file→第1个、$format→第2个

声明命名参数后可读性更好:

arguments: [file, format]

于是 SKILL.md 正文里写 python3 ${CLAUDE_SKILL_DIR}/scripts/profile.py $file --out $format,调用 /csv-pulse data.csv md 时 $file 展开为 data.csv、$format 展开为 md。多词值要加引号,例如 /csv-pulse "my data.csv"。

设计取舍:把"原始路径"作为参数透传给脚本,而不让 Claude 自由发挥去拼接命令——这正是把口径锁死在代码里的关键。

Step 5:完整实战——构建 csv-pulse 体检 Skill

目标:用户说"帮我体检 sales.csv",Skill 自动调用捆绑 Python 脚本完成确定性统计(行数、列类型、缺失率、唯一值数),脚本输出 JSON 到 stdout,SKILL.md 指挥 Claude 解析 JSON 并写出一份可读的 Markdown 报告与改进建议。

5.1 创建目录

mkdir -p ~/.claude/skills/csv-pulse/scripts

5.2 写 SKILL.md(入口与调度)

---
name: csv-pulse
description: Profile a CSV file's health — row count, column types, missing-value
  rates, cardinality. Use when the user asks to "analyze csv", "check data quality",
  "profile a dataset", or "体检 CSV". Produces a Markdown report with findings.
when_to_use: CSV data-quality profiling, missing-value detection, column-type guessing
allowed-tools: Bash(python3 *)
argument-hint: [path-to-csv]
arguments: [file]
---

# CSV Pulse — 数据体检

## How to run
Run the bundled profiler and capture its **JSON** output to stdout:

```bash
python3 ${CLAUDE_SKILL_DIR}/scripts/profile.py "$file"

How to behave

  1. If the command exits non-zero, STOP and report the error verbatim to the user. Do NOT attempt to "fix" the file yourself.
  2. If exit code is 0, the last line of stdout is a JSON object. Parse it.
  3. Write a Markdown report with: - A summary table (rows, columns, total cells, missing cells, missing %) - A per-column table (type, missing count, missing %, unique values) - A "Findings" section: flag columns with missing % > 20, or unique-ratio > 0.95 (likely IDs), or columns with only 1 unique value (constant column).
  4. End with 3 prioritized recommendations.

For the exact JSON field contract, consult references/output-contract.md.

### 5.3 写脚本 `scripts/profile.py`(确定性大脑)

```python
#!/usr/bin/env python3
"""CSV health profiler. Prints findings to stderr, a single JSON line to stdout last."""
import csv, json, sys, os
from collections import Counter

def main(path):
    if not os.path.isfile(path):
        print(f"ERROR: not a file: {path}", file=sys.stderr); sys.exit(2)
    try:
        with open(path, newline="", encoding="utf-8-sig") as f:
            reader = csv.reader(f)
            rows = list(reader)
    except Exception as e:
        print(f"ERROR: cannot read CSV: {e}", file=sys.stderr); sys.exit(2)
    if not rows:
        print("ERROR: empty file", file=sys.stderr); sys.exit(2)

    header = rows[0]
    body = rows[1:]
    n_cols = len(header)
    columns = {h: [] for h in header}
    for r in body:
        for i, h in enumerate(header):
            columns[h].append(r[i] if i < len(r) else "")

    col_stats = {}
    total_cells = missing_cells = 0
    for h, vals in columns.items():
        miss = sum(1 for v in vals if v.strip() == "")
        uniq = len(set(vals))
        # crude type guess
        sample = [v for v in vals if v.strip()][:50]
        kind = "numeric" if sample and all(_is_num(v) for v in sample) else "text"
        col_stats[h] = {
            "type": kind, "missing": miss, "missing_pct": round(miss/len(vals)*100, 2) if vals else 0,
            "unique": uniq, "unique_ratio": round(uniq/len(vals), 3) if vals else 0,
        }
        total_cells += len(vals); missing_cells += miss

    result = {
        "file": os.path.basename(path), "rows": len(body), "columns": n_cols,
        "total_cells": total_cells, "missing_cells": missing_cells,
        "missing_pct": round(missing_cells/total_cells*100, 2) if total_cells else 0,
        "columns_detail": col_stats,
    }
    json.dump(result, sys.stdout, ensure_ascii=False)  # JSON LAST, on stdout
    sys.exit(0)

def _is_num(v):
    try: float(v); return True
    except: return False

if __name__ == "__main__":
    main(sys.argv[1] if len(sys.argv) > 1 else "")

关键约定:JSON 必须是 stdout 的最后一行,且退出码 0=成功、非 0=失败。SKILL.md 据此决定是"解析并写报告"还是"原样报错"。这就是"代码做确定性、Claude 做叙述"的契约边界。

5.4 写参考契约 references/output-contract.md

# Output contract for profile.py

`columns_detail` is a map: column-name -> { type, missing, missing_pct, unique, unique_ratio }
- type ∈ {numeric, text}
- missing_pct: 0–100
- unique_ratio: 0–1 (1.0 = all distinct, likely an ID)

这是渐进式披露的第三层:平时不进上下文,只有需要核字段时才被读。

5.5 预期结果

重启会话或运行 /csv-pulse sales.csv,你会看到 Claude 先发一条 Bash(python3 ...) 调用,拿到 JSON,再产出类似:

## CSV 体检报告 — sales.csv
| 维度 | 值 |
|---|---|
| 行数 | 12048 |
| 列数 | 9 |
| 缺失率 | 3.7% |

## 逐列
| 列 | 类型 | 缺失% | 唯一值 |
|---|---|---|---|
| order_id | text | 0.0 | 12048 |
| region | text | 18.4 | 6 |
| amount | numeric | 0.2 | 9120 |

## 发现
- ⚠️ `region` 缺失 18.4%,接近阈值 20%,建议补全或标记
- `order_id` unique_ratio=1.0,疑似主键,不应作为特征

Step 6:调试与常见坑

现象 根因 解法
Claude 报"找不到脚本/No such file" 没用 ${CLAUDE_SKILL_DIR},或用了相对路径 全部脚本调用改为 python3 ${CLAUDE_SKILL_DIR}/scripts/xxx.py
脚本被当成文档读而非执行 SKILL.md 没给可运行的 Bash 代码块,或写成"请统计" 用 ```bash 代码块给出明确命令
Claude 自作主张改了文件 没收口权限 frontmatter 加 allowed-tools: Bash(python3 *)
参数没传进去 用了 $1 却没声明 arguments,或用户没给路径 声明 arguments: [file],或用 $ARGUMENTS
同一会话反复重算 脚本每次都重跑 让脚本支持 --cache 或把结果写临时文件,${CLAUDE_SESSION_ID} 可做缓存名
输出被 Claude 当噪声 把人类可读日志打 stdout 日志打 stderr,只有 JSON 走 stdout 最后一行
项目级安装失效 路径写死 ~/.claude/... 全部改用 ${CLAUDE_SKILL_DIR},它对 personal/project/plugin 三层都正确

排查口诀:先看会话里 Claude 实际发出的 Bash 命令是什么(命令行会被展开),把那段命令手动复制到终端跑一遍,90% 的问题当场暴露。

整体工作流

flowchart TD
    U["用户:'帮我体检 sales.csv'"] --> S["Skill 发现与加载<br/>description 触发"]
    S --> FM["读取 SKILL.md 主体<br/>含 allowed-tools 收口"]
    FM --> ARG["参数替换<br/>$file ← sales.csv"]
    ARG --> VAR["路径替换<br/>CLAUDE_SKILL_DIR → 真实目录"]
    VAR --> BASH["Claude 调用 Bash 工具<br/>python3 .../scripts/profile.py sales.csv"]
    BASH --> CODE{"脚本执行<br/>确定性统计"}
    CODE -->|exit 0| JSON["stdout 末行 = JSON"]
    CODE -->|exit !=0| ERR["stderr 原样报错<br/>Claude 停手"]
    JSON --> NARR["Claude 解析 JSON<br/>生成 Markdown 报告"]
    NARR --> R["交付报告 + 建议"]
    ERR --> R
    style CODE fill:#e8f5e9
    style VAR fill:#fff3e0

图中橙色节点(路径替换)是本文主角,绿色节点(脚本执行)是"可执行大脑",二者合力让 Skill 既装哪都能跑又算得准。

工程化进阶(可选)

延伸阅读

训练营适用性评估

维度 评估
适合训练营类型 "Claude Code 工程化进阶""团队提效工具封装""AI Agent 开发实战"等中阶营
学员基础 需已能独立写出最小 Skill;有 Python 基础
教学时长 约 90–120 分钟(含 30 min 讲解 + 60 min 动手构建 csv-pulse + 答疑)
可交付成果 每位学员产出一个可运行的带脚本 Skill,并理解权限收口与参数传递
易混淆点 allowed-tools 是"限制"非"授权";${CLAUDE_SKILL_DIR} 与工作目录无关——务必在课堂演示中对比"写死路径"的失败案例加深印象
衔接下一课 本文是通往"子代理编排""插件分发""Hooks 自动化"的桥梁

参考来源

  1. Extend Claude with skills — Claude Code 官方文档 — ${CLAUDE_SKILL_DIR} 字符串替换、frontmatter 全字段表、codebase-visualizer 捆绑脚本示例的权威来源
  2. Advanced: Progressive Disclosure, Bundled Scripts, and Subagent Orchestration — Lesson 02 专讲 ${CLAUDE_SKILL_DIR} 捆绑脚本调用与子代理编排
  3. Claude Code Skills: How to Set Them Up and Use Them — DEV Community — 目录命名规则、三层渐进式披露、触发测试清单的实操讲解
  4. Progressive Disclosure Architecture — DeepWiki (spences10/claude-skills-cli) — 渐进式披露架构与 Skill-Creator 元技能的深度解析
  5. Plugins reference — Claude Code Docs — plugin 级 ${CLAUDE_SKILL_DIR} 指向 skill 子目录的说明,用于进阶分发

[!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学通信知识星球海报-扫码加入