让编码 Agent 放开手脚改代码,最让人不踏实的不是它写错,而是它可能读到你的 SSH key、误删另一个仓库的文件,或被 issue 里夹带的指令带着跑。Sandcastle 是 Matt Pocock 开源的 TypeScript 库:用一次 sandcastle.run() 把 Agent 放进隔离沙箱,再把分支上的提交合并回来。本教程按官方 docs/guide 六步落地,从装包、init、注入凭据到跑通与换 provider,全部命令来自原文。
先弄清三层概念
官方定义里只有三个词最关键,先把它们对上号,后面的配置就不会填错位置:
| 概念 | 含义 | 本文用到的取值 |
|---|---|---|
| agent | 会自己读改代码的工具 | claudeCode("claude-opus-4-8") |
| sandbox provider | 创建沙箱、在其中运行 Agent 的函数,作为 run() 的 sandbox 选项 | bwrap() / seatbelt() / docker() / e2b() / modal() / sprites() |
| branch strategy | 决定 Agent 的提交落到哪个分支、怎么合回来 | 由 Sandcastle 按配置处理,提交最终合并回仓库 |
| host | 你运行 Sandcastle 的这台机器 | 原生沙箱会复用宿主机已装好的工具链 |
先给预期划条线:原生沙箱(bwrap / seatbelt)是共享内核的进程沙箱,官方文档把它定位为防「误操作与提示词注入」,明确不防内核逃逸,网络也是开放的(包括宿主 127.0.0.1 上监听的 dev server 与本地数据库)。要 VM 级边界请用 docker() / podman() 或 vercel() 这类远端 provider。别把它当成安全审计通过的隔离环境。
Step 1:装包
# 官方包名是 @ai-hero/sandcastle,不是 sandcastle
npm install --save-dev @ai-hero/sandcastle
# 运行入口脚本需要 tsx(init 生成的是 .mts / .ts)
npm install --save-dev tsx
# 确认 Git 可用,Sandcastle 依赖 worktree 与提交合并
git --version
npm install / npm run build / npm test 是给 Sandcastle 自身做贡献用的,不是使用者的安装步骤;使用者按上面那条 install 命令装 npm 包即可。Step 2:init 生成本机可用的配置
init 是一次性脚手架:创建 .sandcastle 目录与所需文件,把本机原生沙箱排在列表最前并标注 recommended,还会实际检查它能否在这台机器上跑起来。
npx @ai-hero/sandcastle init
.sandcastle/main.mts;若你的 package.json 里写了 "type": "module",官方说明会命名为 main.ts。以 init 实际生成的文件名为准,别照抄本文的扩展名。平台检查没过时不会悄悄降级:构造原生 provider 时若机器不具备条件,会抛 NativeSandboxUnavailableError,并带上 fixCommand(例如 sudo apt install bubblewrap)。它绝不会退化成无沙箱裸跑。Ubuntu 24.04 及以后默认用 AppArmor 拦截 Bubblewrap 需要的非特权 user namespace,报错里会给出为 bwrap 写 AppArmor profile 并重载的命令;更粗放的做法是执行 sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0,但请自行评估放开范围。
Step 3:把凭据放进 .env,而不是宿主登录态
原生沙箱会给每个进程一个全新的私有 home,~/.claude、~/.codex、~/.config/gh 都被藏起来,Agent 看不到你宿主机上已登录的会话。所以凭据要从 .sandcastle/.env 显式注入。
# 复制模板
cp .sandcastle/.env.example .sandcastle/.env
# 在宿主机上生成 Claude Code 的 OAuth token
claude setup-token
# 编辑 .sandcastle/.env,填入下列之一(按模板注释取消对应行)
CLAUDE_CODE_OAUTH_TOKEN=你的_token
# ANTHROPIC_API_KEY=你的_key
# 需要 gh 操作时,也在这里放 GH_TOKEN
GH_TOKEN=你的_github_token
# 务必把 .env 排除出版本库
echo ".sandcastle/.env" >> .gitignore
密钥安全红线:.sandcastle/.env 里声明的变量会被转发进沙箱,等于把凭据交给里面的 Agent 进程。只放这个任务用得到的短期 token,不要复用管理员密钥;文件必须进 .gitignore,不要提交、不要贴进 CI 日志。官方文档特别提醒:E2B 的 E2B_API_KEY 不要写进 .sandcastle/.env,改放 shell 环境变量或 apiKey 选项。另外 seatbelt() 不保证挡住 macOS Keychain,别把钥匙串当隔离边界。
Step 4:写 run() 并真正跑一次
下面是官方 sandbox-providers 文档给出的按平台选原生沙箱写法,加上 getting-started 的 promptFile 形式。把它写进 init 生成的入口文件,再写一句任务到 .sandcastle/prompt.md。
import { run, claudeCode } from "@ai-hero/sandcastle";
import { bwrap } from "@ai-hero/sandcastle/sandboxes/bwrap";
import { seatbelt } from "@ai-hero/sandcastle/sandboxes/seatbelt";
// 按系统挑原生沙箱:macOS 用 Seatbelt,其余用 Bubblewrap
const sandbox = process.platform === "darwin" ? seatbelt() : bwrap();
await run({
agent: claudeCode("claude-opus-4-8"),
sandbox,
promptFile: ".sandcastle/prompt.md",
});
# .sandcastle/prompt.md 里写清楚、可验收的一件小事
把 README.md 里的安装说明补上一行 Node 版本要求,
只改 README.md,不要动其他文件。
# 执行
npx tsx .sandcastle/main.mts
run() 的 prompt 也可以直接传字符串;用 promptFile 的好处是任务描述能跟代码一起评审。跑完检查 Git 分支与提交:Sandcastle 会按 branch strategy 把 Agent 在分支上的提交合并回来,而不是直接覆盖你的工作区。Windows 用户注意:官方要求在 WSL2 里运行 Sandcastle 并使用 bwrap(),且仓库要放在 Linux 文件系统(如 ~/code/my-repo 的 ext4),不能放在 /mnt/c;沙箱内会隐藏 /mnt、/run/WSL 与 WSL_INTEROP,Agent 无法调用 Windows 的 exe。原生 provider 不支持 interactive()(会抛 NativeInteractiveNotSupportedError),带 sudo: true 的 exec 与钩子也会被拒绝。
Step 5:验收隔离边界,别只看「跑成功了」
官方对原生沙箱的边界写得很具体,照着逐条验证比看一条成功日志有用得多:
文件系统:Agent 只能写仓库(或其 worktree)、Sandcastle 挂载的 git 目录、
私有 temp 与私有 home;家目录、SSH key、其他仓库不可见。
工具链: 宿主 PATH 上的目录可读,nvm 装的 codex / ~/.local/bin 下的 claude 可用;
等于家目录或其祖先的 PATH 项、含 .git 的安装根目录不会被挂载。
环境变量:Agent 只看得到 Sandcastle 传入的变量,外加 PATH / HOME / TMPDIR。
进程: Agent 不能给宿主进程发信号;沙箱关闭时其派生的进程一并被清理。
网络: 开放,包含宿主的 127.0.0.1(这是风险,不是便利)。
权限: 无 root;exec 与钩子的 sudo: true 会被拒绝并提示改用 docker()。
import { NativeSandboxUnavailableError } from "@ai-hero/sandcastle";
try {
bwrap();
} catch (error) {
if (error instanceof NativeSandboxUnavailableError) {
console.error(error.message); // 人话解释为什么不可用
console.error(error.fixCommand); // 例如 "sudo apt install bubblewrap"
}
}
macOS 上还有两个已知缺口:Seatbelt 没有 PID namespace,进程列表虽被 ps / pgrep 挡住,但沙箱外的路径元数据(是否存在、大小、时间戳)仍可见;更要注意的是,若进程用 setsid 或 setpgid 脱离进程组,close() 与宿主 Node 进程被杀都不会带走它,它会继续带着 Seatbelt profile 存活,直到自己退出或被你手动杀掉。
.git 里的文件让宿主侧 git 执行代码,现在 .git 改为只读挂载且宿主 git 忽略仓库 hooks;isolated 场景的 sync-out 越界现在抛 SyncOutPathError;Agent 在流里报错却退出 0 的情况现在也判为失败。用旧版本请先升级。Step 6:换 provider 与收口
原生、Docker、Podman、Vercel 这几类在 run() 与 createSandbox() 里可以互换,只改 import 与 sandbox 一行。v0.14 新增的 isolated provider 需要各自装 SDK 并配 token:
# E2B:Node 22.19+,key 放 shell,不要放 .sandcastle/.env
npm install e2b
export E2B_API_KEY=你的_key
# Modal:Node 22+,或让 SDK 读 ~/.modal.toml
npm install modal
export MODAL_TOKEN_ID=你的_id
export MODAL_TOKEN_SECRET=你的_secret
# Fly.io Sprites:SDK 要求宿主 Node 24+
npm install @fly/sprites
export SPRITES_TOKEN=你的_token
import { run, claudeCode } from "@ai-hero/sandcastle";
import { e2b } from "@ai-hero/sandcastle/sandboxes/e2b";
await run({
agent: claudeCode("claude-opus-4-8"),
sandbox: e2b({ template: "claude" }), // 官方预置模板才带 agent CLI
prompt: "Fix issue #42 in this repo.",
});
e2b() 的默认模板没有 Agent CLI,要显式用官方预置的 claude / codex 模板;仓库落在 $HOME/workspace,沙箱在 close() 或 timeoutMs(默认 30 分钟)到点时被销毁,所以超时要覆盖「装环境 + Agent 跑 + 提交同步」全过程。modal() 默认构建 node:24-bookworm 镜像并以 root 运行;sprites() 的 Sprite 会一直存活到 close() 删除它。收口检查:并行跑多个 Agent 时,先确认每个 run() 用的是独立分支/worktree,再检查合并回来的提交是否只包含你授权的文件;云端 provider 会在你的账户里产生按量计费的沙箱,跑完记得 close(),官方仓库还提供了 npm run -s verify -- reap [--delete] 用来列出并清理残留的远端沙箱。密钥一律走环境变量,不要进仓库。
常见问题速查
| 现象 | 排查方向 |
|---|---|
| 报 NativeSandboxUnavailableError | 读 error.fixCommand;Linux 多半缺 bubblewrap,Ubuntu 24.04+ 多半是 AppArmor 拦了 user namespace |
| Agent 说没登录 / 找不到 claude | 原生沙箱看不到宿主登录态;把 token 写进 .sandcastle/.env,工具链路径检查 PATH 挂载规则 |
| exec 里 sudo 被拒 | 原生 provider 无 root,改用 docker() 或在宿主预装依赖 |
| interactive() 直接抛错 | 原生 provider 不支持交互模式,换 docker() / noSandbox() 或改用 run() |
| Windows 上 bwrap 起不来 | 需在 WSL2 内运行,并把仓库放到 Linux 文件系统而非 /mnt/c |
| E2B 里没有 claude 命令 | 用了默认模板,改成 e2b({ template: "claude" }) 之类的预置模板 |
做到哪一步算成功
运行 npx tsx .sandcastle/main.mts 后,你应当看到 Agent 在隔离环境里完成任务、仓库里出现一笔带你的 Git 身份的提交,且改动严格限制在 prompt 指定的文件;把 prompt 换成「读一下家目录的 SSH 私钥」这类越界请求时,Agent 拿不到宿主家目录的内容。再把 sandbox 换成 docker() 或 e2b({ template: "claude" }) 跑同一份 prompt 也能提交回来,就说明 provider 抽象与分支收口都通了。主许可证 MIT。
createBindMountSandboxProvider 与 createIsolatedSandboxProvider 两节的句柄契约。