skills 可以指示 agent 运行 shell 命令,也可以在 scripts/ 目录中打包可复用脚本。本指南会介绍一次性命令、自包含且自带依赖的脚本,以及如何为 agent 使用场景设计脚本接口。
一次性命令
如果现有包已经能满足你的需求,你可以直接在 SKILL.md 指令里引用它,而不一定要单独建立 scripts/ 目录。很多生态都提供了能在运行时自动解析依赖的工具。
uvx
pipx
npx
bunx
deno run
go run
uvx 会在隔离环境中运行 Python 包,并使用非常激进的缓存策略。它随 uv 一起提供。uvx ruff@0.8.0 check .
uvx black@24.10.0 .
- 不随 Python 一起提供,需要额外安装。
- 很快。缓存策略很积极,因此重复运行几乎是瞬时的。
pipx 会在隔离环境中运行 Python 包。可以通过操作系统包管理器安装(apt install pipx、brew install pipx)。pipx run 'black==24.10.0' .
pipx run 'ruff==0.8.0' check .
- 不随 Python 一起提供,需要额外安装。
- 它是
uvx 的成熟替代方案。虽然 uvx 现在已经成了更常见的推荐选择,但 pipx 依然可靠,而且在更多操作系统包管理器里可用。
npx 可以运行 npm 包,并按需下载它们。它随 npm 一起提供,而 npm 又随 Node.js 一起提供。npx eslint@9 --fix .
npx create-vite@6 my-app
- 随 Node.js 一起提供,不需要额外安装。
- 会下载包、执行它,并缓存下来供后续运行复用。
- 使用
npx package@version 固定版本,确保可复现。
bunx 是 Bun 对应的 npx。它随 Bun 一起提供。bunx eslint@9 --fix .
bunx create-vite@6 my-app
- 在基于 Bun 的环境中,它是
npx 的即插即用替代品。
- 只有当用户环境里装的是 Bun 而不是 Node.js 时才适合使用。
deno run 可以直接从 URL 或 specifier 运行脚本。它随 Deno 一起提供。deno run npm:create-vite@6 my-app
deno run --allow-read npm:eslint@9 -- --fix .
- 访问文件系统或网络时,需要显式声明权限标志(如
--allow-read)。
- 使用
-- 将 Deno 自己的标志与目标工具的标志分开。
go run 可以直接编译并运行 Go 包。它内置在 go 命令中。go run golang.org/x/tools/cmd/goimports@v0.28.0 .
go run github.com/golangci/golangci-lint/cmd/golangci-lint@v1.62.0 run
- 内置于 Go,不需要额外工具。
- 通过固定版本或使用
@latest,让命令意图更明确。
在 skills 中编写一次性命令时的建议:
- 固定版本。 例如
npx eslint@9.0.0,这样命令在不同时间运行时行为一致。
- 把前置条件写进
SKILL.md。 例如“Requires Node.js 18+”,不要默认 agent 所在环境已经具备这些条件。对于运行时级别的要求,请使用 compatibility frontmatter 字段。
- 复杂命令移入脚本。 当你只是带几个 flag 调用一个工具时,一次性命令很适合;但如果命令已经复杂到第一次就不容易写对,那么放进经过测试的
scripts/ 脚本会更可靠。
从 SKILL.md 引用脚本
引用打包文件时,请使用 相对于 skill 目录根的相对路径。agent 会自动解析这些路径,不需要绝对路径。
在 SKILL.md 中列出有哪些脚本可用,这样 agent 才知道它们存在:
## Available scripts
- **`scripts/validate.sh`** — Validates configuration files
- **`scripts/process.py`** — Processes input data
然后明确指示 agent 去运行它们:
## Workflow
1. Run the validation script:
```bash
bash scripts/validate.sh "$INPUT_FILE"
```
2. Process the results:
```bash
python3 scripts/process.py --input results.json
```
这种相对路径约定同样适用于 references/*.md 这类辅助文件。代码块中的脚本执行路径始终是相对于 skill 目录根 的,因为 agent 会从那里执行命令。
自包含脚本
如果你需要可复用逻辑,可以在 scripts/ 中打包一个脚本,并让它以内联方式声明自身依赖。这样 agent 只用一条命令就能运行脚本,不需要额外的 manifest 文件,也不用提前执行安装步骤。
有几种语言都支持内联依赖声明:
PEP 723 定义了脚本内联元数据的标准格式。你可以在 # /// 标记之间用一个 TOML 代码块声明依赖:# /// script
# dependencies = [
# "beautifulsoup4",
# ]
# ///
from bs4 import BeautifulSoup
html = '<html><body><h1>Welcome</h1><p class="info">This is a test.</p></body></html>'
print(BeautifulSoup(html, "html.parser").select_one("p.info").get_text())
推荐使用 uv 运行:uv run scripts/extract.py
uv run 会创建隔离环境、安装声明过的依赖,然后执行脚本。pipx(pipx run scripts/extract.py)也支持 PEP 723。
- 用 PEP 508 规范固定版本,例如:
"beautifulsoup4>=4.12,<5"。
- 使用
requires-python 约束 Python 版本。
- 使用
uv lock --script 生成 lockfile,以获得完整可复现性。
Deno 的 npm: 和 jsr: import specifier 让每个脚本默认就是自包含的:#!/usr/bin/env -S deno run
import * as cheerio from "npm:cheerio@1.0.0";
const html = `<html><body><h1>Welcome</h1><p class="info">This is a test.</p></body></html>`;
const $ = cheerio.load(html);
console.log($("p.info").text());
deno run scripts/extract.ts
- npm 包用
npm:,Deno 原生包用 jsr:。
- 版本说明遵循 semver:
@1.0.0 表示精确版本,@^1.0.0 表示兼容版本。
- 依赖会被全局缓存。使用
--reload 可强制重新拉取。
- 带有原生 addon(node-gyp)的包可能无法工作,通常更适合使用自带预编译二进制的包。
当目录树中不存在 node_modules 时,Bun 会在运行时自动安装缺失的包。你可以直接在 import 路径中固定版本:#!/usr/bin/env bun
import * as cheerio from "cheerio@1.0.0";
const html = `<html><body><h1>Welcome</h1><p class="info">This is a test.</p></body></html>`;
const $ = cheerio.load(html);
console.log($("p.info").text());
bun run scripts/extract.ts
- 不需要
package.json 或 node_modules。TypeScript 也能原生运行。
- 包会被全局缓存。第一次运行会下载,之后几乎是瞬时的。
- 如果目录树中的任意上级目录已经存在
node_modules,自动安装会失效,Bun 会退回标准的 Node.js 解析方式。
Ruby 从 2.6 起就随附 Bundler。你可以使用 bundler/inline 直接在脚本里声明 gem:require 'bundler/inline'
gemfile do
source 'https://rubygems.org'
gem 'nokogiri'
end
html = '<html><body><h1>Welcome</h1><p class="info">This is a test.</p></body></html>'
doc = Nokogiri::HTML(html)
puts doc.at_css('p.info').text
- 显式固定版本(例如
gem 'nokogiri', '~> 1.16'),因为这里没有 lockfile。
- 当前工作目录中的
Gemfile 或 BUNDLE_GEMFILE 环境变量可能会产生干扰。
为 agent 使用场景设计脚本
当 agent 运行你的脚本时,它会读取 stdout 和 stderr 来决定下一步怎么做。有几个设计选择会显著影响脚本对 agent 是否友好。
避免交互式提示
这是 agent 执行环境里的硬性要求。agent 工作在非交互式 shell 中,不能响应 TTY 提示、密码对话框或确认菜单。任何等待交互输入的脚本都会一直挂起。
应当通过命令行 flag、环境变量或 stdin 接收全部输入:
# Bad: hangs waiting for input
$ python scripts/deploy.py
Target environment: _
# Good: clear error with guidance
$ python scripts/deploy.py
Error: --env is required. Options: development, staging, production.
Usage: python scripts/deploy.py --env staging --tag v1.2.3
用 --help 记录用法
--help 输出是 agent 学习脚本接口的主要方式。请包含简短描述、可用 flag 和使用示例:
Usage: scripts/process.py [OPTIONS] INPUT_FILE
Process input data and produce a summary report.
Options:
--format FORMAT Output format: json, csv, table (default: json)
--output FILE Write output to FILE instead of stdout
--verbose Print progress to stderr
Examples:
scripts/process.py data.csv
scripts/process.py --format csv --output report.csv data.csv
保持简洁,因为这些输出会和 agent 正在处理的其他内容一起进入上下文窗口。
编写有帮助的错误消息
当 agent 拿到一个错误时,这条消息会直接影响它下一次尝试怎么改。一个含糊的 “Error: invalid input” 只会浪费一次轮次。更好的方式是:明确说出哪里错了、期望的是什么、下一步应该怎么试。
Error: --format must be one of: json, csv, table.
Received: "xml"
使用结构化输出
优先使用结构化格式,例如 JSON、CSV、TSV,而不是自由文本。结构化格式既便于 agent 消费,也便于标准工具(jq、cut、awk)处理,因此更容易组成流水线。
# Whitespace-aligned — hard to parse programmatically
NAME STATUS CREATED
my-service running 2025-01-15
# Delimited — unambiguous field boundaries
{"name": "my-service", "status": "running", "created": "2025-01-15"}
把数据和诊断信息分开: 结构化数据输出到 stdout,进度消息、警告和其他诊断信息输出到 stderr。这样 agent 既能拿到干净、可解析的数据输出,又能在需要时查看诊断信息。
更多考虑
- 幂等性。 agent 可能会重试命令。“如果不存在则创建”通常比“创建并在重复时报错”更安全。
- 输入约束。 遇到有歧义的输入时,不要猜,直接报一个清晰错误。尽量使用枚举值和封闭集合。
- Dry-run 支持。 对于破坏性或有状态的操作,
--dry-run 可以让 agent 先预览将要发生什么。
- 有意义的退出码。 为不同失败类型使用不同退出码(例如未找到、参数非法、鉴权失败),并在
--help 输出中记录这些退出码的含义,让 agent 知道该如何解释它们。
- 安全默认值。 需要破坏性操作时,要考虑是否应该要求显式确认 flag(例如
--confirm、--force)或其他与风险级别相匹配的保护措施。
- 可预测的输出规模。 许多 agent 运行环境会在工具输出超过阈值时自动截断(例如 10 到 30K 字符),这可能导致关键内容丢失。如果你的脚本可能输出很多内容,默认就应该返回摘要或合理限制,并支持像
--offset 这样的 flag,让 agent 可以按需请求更多内容。另一种做法是:如果输出很大且不适合分页,就要求 agent 显式传入 --output,指定输出文件,或传 - 来明确表示要输出到 stdout。