${CLAUDE_SKILL_DIR} 捆绑脚本实战本教程面向已经会写基础 SKILL.md、但想让 Skill 从"纯文本说明书"升级为"自带可执行工具箱"的开发者。你将理解 Claude Code Skills 的核心机制——scripts/ 目录与 ${CLAUDE_SKILL_DIR} 路径变量,掌握参数传递、权限收口(allowed-tools)、退出码处理,并亲手构建一个把"确定性统计交给 Python、把叙述性洞察交给 Claude"的真实 Skill:csv-pulse(CSV 健康体检)。适合需要把团队通用脚本封装成可复用能力的工程师与运维人员。
claude 命令)SKILL.md(name + description)并知道放在 ~/.claude/skills/ 或项目 .claude/skills/ 下python3(3.8+),会基本 Python 与 BashSKILL.md 主体精简,重逻辑下沉到脚本/参考文件如果第 2、4 条尚不满足,建议先读官方《Extend Claude with skills》文档的入门章节。
很多新手做的第一个 Skill 长这样:在 SKILL.md 里用自然语言写"请检查文件格式、统计行数、找出空值",然后期待 Claude 每次都准确执行。问题在于:
awk,也可能用 Python,不同实现结果甚至不同口径,团队难以复现。Claude Code Skills 的官方答案是把可执行代码打包进 Skill 自己的 scripts/ 子目录,再让 SKILL.md 指挥 Claude 调用它。代码负责"算得准",SKILL.md 负责"用得对"。这就是本文的主角——捆绑脚本(bundled scripts)。
官方原话点明了设计意图:脚本路径要用 ${CLAUDE_SKILL_DIR},这样"无论 Skill 装在 personal、project 还是 plugin 层级都能正确解析"。
${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 工具执行——这就是"可执行大脑"的接线方式。
一个带脚本的 Skill 目录如下(以 csv-pulse 为例):
~/.claude/skills/csv-pulse/
├── SKILL.md # 入口:触发词 + 调度指令(精简)
├── scripts/
│ └── profile.py # 确定性逻辑:解析、统计、输出 JSON
└── references/ # 可选:深度文档,按需读取
└── output-contract.md # 脚本输出字段的详细说明
命名硬性规则(踩坑高发区):
csv-pulse ✅,csv_pulse ❌,Csv Pulse ❌SKILL.md(大小写敏感),不要 skill.md、SKILL.MD,也不要放 README.mdname 字段建议与目录名一致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]
---
要点:
description 是触发判定的核心,必须同时写清做什么与何时触发,把关键触发短语前置;它与 when_to_use 合计在技能清单里被截断于 1,536 字符description 里写 < >(XML 尖括号被禁)argument-hint 只影响自动补全提示,如 [path-to-csv]${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 自由发挥去拼接命令——这正是把口径锁死在代码里的关键。
csv-pulse 体检 Skill目标:用户说"帮我体检 sales.csv",Skill 自动调用捆绑 Python 脚本完成确定性统计(行数、列类型、缺失率、唯一值数),脚本输出 JSON 到 stdout,SKILL.md 指挥 Claude 解析 JSON 并写出一份可读的 Markdown 报告与改进建议。
mkdir -p ~/.claude/skills/csv-pulse/scripts
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"
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 做叙述"的契约边界。
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)
这是渐进式披露的第三层:平时不进上下文,只有需要核字段时才被读。
重启会话或运行 /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,疑似主键,不应作为特征
| 现象 | 根因 | 解法 |
|---|---|---|
| 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-skills-cli(npx claude-skills-cli init)一键生成骨架并做渐进式披露校验,省去手搓目录。codebase-visualizer 的结构,先在终端独立跑通脚本,再写 SKILL.md——脚本本身要能在没有 Claude 的情况下正确工作。plugin.json 作为插件分发(plugin 级的 ${CLAUDE_SKILL_DIR} 指向插件内 skill 子目录,路径逻辑无需改动)。context: fork 让脚本在隔离子代理里跑,避免污染主会话。${CLAUDE_SKILL_DIR}、frontmatter 全字段、字符串替换表的权威来源codebase-visualizer——捆绑 Python 脚本 + HTML 输出的完整范本${CLAUDE_SKILL_DIR} 第 02 课,含子代理编排spences10/claude-skills-cli——带校验的 Skill 生成器| 维度 | 评估 |
|---|---|
| 适合训练营类型 | "Claude Code 工程化进阶""团队提效工具封装""AI Agent 开发实战"等中阶营 |
| 学员基础 | 需已能独立写出最小 Skill;有 Python 基础 |
| 教学时长 | 约 90–120 分钟(含 30 min 讲解 + 60 min 动手构建 csv-pulse + 答疑) |
| 可交付成果 | 每位学员产出一个可运行的带脚本 Skill,并理解权限收口与参数传递 |
| 易混淆点 | allowed-tools 是"限制"非"授权";${CLAUDE_SKILL_DIR} 与工作目录无关——务必在课堂演示中对比"写死路径"的失败案例加深印象 |
| 衔接下一课 | 本文是通往"子代理编排""插件分发""Hooks 自动化"的桥梁 |
${CLAUDE_SKILL_DIR} 字符串替换、frontmatter 全字段表、codebase-visualizer 捆绑脚本示例的权威来源${CLAUDE_SKILL_DIR} 捆绑脚本调用与子代理编排${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 等场景全覆盖。
