
一、先说个场景
你写了一个 Skill,想让 agent 帮你把 Markdown 转成 HTML。于是你在 SKILL.md 正文里详细写了每一段 sed/awk 命令。结果发现:换一台电脑用户名不一样,路径全错;Windows 上没有 sed;代码块太长,agent 看着看着就丢了步骤。
正确的做法是把复杂逻辑放进 scripts/ 目录的脚本文件里,SKILL.md 只负责告诉 agent “什么时候调用哪个脚本、传什么参数”。这样既安全又便于维护,也更符合 OpenClaw 官方的 best practices:SKILL.md 写”做什么”,脚本写”怎么做”。
二、为什么是这样
OpenClaw 的 agent 读的是 Markdown 指令。如果指令里塞满 shell 命令,会出现几个问题:
- 可移植性差:写死
/home/alice/...在别人机器上直接失效。 - 上下文浪费:长命令序列挤占模型上下文,容易出错。
- 安全风险:把参数直接拼进 shell 命令,容易被不可信输入注入。
- 难测试:在 Markdown 里调命令,不如单独跑脚本来得直接。
把脚本放到 scripts/ 目录,SKILL.md 用 {baseDir} 引用,就能解决前两个问题:路径随技能目录自适应,正文保持简洁。把参数校验、错误处理放进脚本,还能降低注入风险。
模型并不擅长”精确复现一长串命令”。它读 Markdown 时容易把多行命令里的某一行漏掉,或者在引号、转义上出错。但你让模型”去跑 {baseDir}/scripts/convert.sh 并传这两个参数”,它只需要做一件简单且明确的事,成功率陡增。scripts/ 是把”模糊的自然语言指令”翻译成”确定的程序执行”的桥梁——模型负责理解意图,脚本负责保证结果,各司其职,比让模型亲自拼命令稳得多。
三、跟着做:分步操作
1. 目录结构
~/.openclaw/workspace/skills/md-to-html/
├── SKILL.md
└── scripts/
└── convert.sh
这个布局里,SKILL.md 和 scripts/ 平级,是最简单也最推荐的形式。技能目录下还可以有 assets/、examples/、references/、templates/ 等规范目录(后面误区里会提到),但 scripts/ 是你真正放可执行逻辑的地方。只要 SKILL.md 在技能根目录、scripts/ 也在根目录,相对关系就永远成立,不会因为技能被装到不同机器而改变。
2. 写脚本
scripts/convert.sh:
#!/bin/bash
set -euo pipefail
INPUT="$1"
OUTPUT="$2"
if [[ ! -f "$INPUT" ]]; then
echo "Error: input file not found: $INPUT" >&2
exit 1
fi
pandoc "$INPUT" -o "$OUTPUT"
echo "Converted: $OUTPUT"
注意 set -euo pipefail 这三件套:它让脚本在出错、遇到未定义变量、管道失败时立刻停下,而不是带着错误状态继续往下跑。对 agent 调用的脚本尤其重要——你宁可它干脆报错退出,也不要它” silently 跑出错误结果”。if [[ ! -f ... ]] 那段则是参数校验的范本:先确认输入文件存在,再动手,避免把空参数传进 pandoc 造成诡异失败。
3. 写 SKILL.md
---
name: md-to-html
description: Convert a Markdown file to HTML using pandoc.
metadata: { "openclaw": { "requires": { "bins": ["pandoc"] } } }
---
# Markdown to HTML
When the user wants to convert a Markdown file to HTML, use the `exec` tool to run `{baseDir}/scripts/convert.sh <input.md> <output.html>`. Confirm the input path with the user before running.
注意 {baseDir} 会被 OpenClaw 解析成该技能目录的绝对路径。无论 OpenClaw 装在哪台机器、哪个用户下,脚本路径都是对的。
这里有个细节值得强调:requires: { bins: ["pandoc"] } 和 {baseDir} 是搭档关系。gating 保证”机器上有 pandoc 才加载这个技能”,{baseDir} 保证”加载后脚本路径一定找得到”。两者一起,才让这个技能真正做到即装即用、换机不慌。另外正文里那句”Confirm the input path with the user before running”也很关键:让 agent 在动真格之前先跟用户确认路径,能避免误改、误删文件,是给脚本类技能加的一道人工保险。
4. 验证
openclaw skills list
openclaw agent --message "把 notes/draft.md 转成 HTML"
验证分两步:先 openclaw skills list 确认技能确实被加载(gating 没把它挡掉),再用 openclaw agent --message 发一条真实指令,看 agent 是否正确地调用了脚本、产出预期文件。如果 list 里没有它,回头查 requires.bins 和 pandoc 是否真的在 PATH;如果 list 有但执行失败,多半是脚本权限或参数问题。两层验证分开做,定位问题会快很多。
四、可以复制的指令
# 创建技能目录
mkdir -p ~/.openclaw/workspace/skills/md-to-html/scripts
# 查看 {baseDir} 会被解析到哪
# 在 SKILL.md 里写:Run `{baseDir}/scripts/convert.sh ...`
---
name: pdf-to-text
description: Extract text from a PDF using pdftotext.
metadata: { "openclaw": { "requires": { "bins": ["pdftotext"] } } }
---
# PDF to Text
When the user asks to extract text from a PDF, run `{baseDir}/scripts/extract.sh <pdf-path>` and return the first 2000 characters.
#!/bin/bash
# scripts/extract.sh
set -euo pipefail
PDF="$1"
pdftotext "$PDF" - | head -c 2000
这一段给了第二个现成样板:pdf-to-text。它和 md-to-html 是同一套路——gating 检查 pdftotext 在不在,脚本用 head -c 2000 控制输出长度,避免把整本 PDF 灌进对话上下文。{baseDir} 再次出现,证明它是脚本类技能的统一寻址方式。你照这两个例子,几乎可以把任何”读入一个文件、产出一些内容”的需求,都套进”SKILL.md 写意图 + scripts 写实现”的模板里。
五、实战案例
你有一个 daily-report skill,需要:
- 读取
~/notes/下当天修改的文件。 - 调用 Python 脚本做摘要。
- 输出成三段式日报。
目录结构:
~/.openclaw/workspace/skills/daily-report/
├── SKILL.md
├── scripts/
│ ├── collect.sh
│ └── summarize.py
└── templates/
└── report.md.tpl
SKILL.md:
---
name: daily-report
description: Generate a daily standup report from today's notes.
metadata: { "openclaw": { "requires": { "bins": ["python3"] } } }
---
# Daily Report
When the user says "写日报" or "daily report", run these steps:
1. Use `exec` to run `{baseDir}/scripts/collect.sh` to get today's modified files.
2. Pass the result to `{baseDir}/scripts/summarize.py`.
3. Render the output using `{baseDir}/templates/report.md.tpl`.
4. Save the final report to `~/daily-reports/<today>.md`.
scripts/collect.sh:
#!/bin/bash
find "$HOME/notes" -type f -mtime -1 -print
scripts/summarize.py:
#!/usr/bin/env python3
import sys, re
files = sys.stdin.read().strip().splitlines()
for f in files:
with open(f) as fh:
text = fh.read()
print(f"## {f}\n{text[:300]}\n")
这样,SKILL.md 保持简短,真正的文件遍历、读取、格式化都交给脚本。你单独调试脚本也很方便,不用每次让 agent 跑一遍。
这个案例的目录结构里还有一个值得注意的点:templates/ 单独放模板,而不是把模板内容硬塞进 SKILL.md。模板是”数据”不是”逻辑”,和脚本分开才清晰,将来改排版只动 report.md.tpl 一个文件。这也呼应了误区 5 提到的 Skill Workshop 目录规范——把不同类型的文件归到各自该在的目录,你的技能会随着功能变多而仍然井井有条,而不是所有东西挤在一个根目录里。
六、常见误区
- 误区 1:把脚本内容写进 SKILL.md 正文。正文应该”告诉模型做什么”,复杂脚本放到
scripts/。 - 误区 2:在脚本里写死用户名路径。用
$HOME、{baseDir}等变量,保证可移植。 - 误区 3:不校验脚本参数。脚本应校验参数存在、路径合法,避免不可信输入导致注入。
- 误区 4:脚本没有可执行权限。某些场景下 agent 通过
exec调用脚本时,需要脚本有chmod +x。 - 误区 5:把模板、示例、参考资料乱放。OpenClaw 的 Skill Workshop 提案目录规范是
assets/、examples/、references/、scripts/、templates/,早养成习惯。
补充误区 3 的实际危害:当 agent 把用户提供的文件名直接拼进脚本参数,而你没有校验,恶意或手滑的输入(比如带空格、分号、反引号的路径)就可能变成额外命令被执行——这就是经典的命令注入。在脚本开头用 set -u 配合”参数个数检查 + 文件存在检查”,是成本极低却很有效的防御。至于误区 4,很多新手写完脚本忘了 chmod +x,结果 agent 一调用就报”permission denied”,排查半天才发现是权限问题,所以建脚本后顺手加执行权限应成为肌肉记忆。
七、进阶玩法
按平台分发不同脚本:
scripts/
├── run.sh
├── run.ps1
└── run.py
SKILL.md 正文里让 agent 根据平台选择脚本:
On macOS or Linux, run `{baseDir}/scripts/run.sh`. On Windows, run `{baseDir}/scripts/run.ps1`.
把脚本作为配置项:
{
skills: {
entries: {
"md-to-html": {
config: { converter: "pandoc" },
},
},
},
}
脚本里读取环境变量或 skills.entries.<name>.config 注入的配置,实现同一技能在不同环境下用不同后端。
按平台分发这个玩法,正好呼应了误区 2 说的”Windows 上没有 sed”——与其写一个跨平台兼容性糟糕的 shell 脚本,不如直接为不同系统准备 run.sh / run.ps1,让 SKILL.md 根据平台挑一个。而 config 注入则更灵活:比如上面的 converter: "pandoc",将来你想换成 wkhtmltopdf,只改 openclaw.json 里的 config,脚本读这个值决定调用谁,SKILL.md 和脚本本身都不用动。这种”行为由配置驱动”的思路,让同一个技能能平滑适配不同团队、不同机器的差异,是技能走向”可复用组件”的关键一步。
八、小结
{baseDir} 和 scripts/ 是把 Skill 从”演示玩具”变成”实用工具”的关键。SKILL.md 负责”何时调用”,脚本负责”如何执行”。路径用 {baseDir} 相对引用,脚本用 scripts/ 目录组织,配合 gating 确保依赖存在,技能就会变得稳定、可移植、易维护。下次写 Skill 时,只要逻辑超过三行命令,就值得拆成脚本。
