diff --git a/README.md b/README.md
index 6643e80..0baea63 100644
--- a/README.md
+++ b/README.md
@@ -143,6 +143,7 @@ Setup, configuration, limits, and troubleshooting live in the
| Command | What |
| ------------------------------------------------ | ----------------------------------------------------------------------- |
| `/createos-sandbox:offload
` | one-shot: stage → run → pull artifacts → destroy |
+| `/createos-sandbox:exec [args]` | run one untrusted/ad-hoc source file in a throwaway box |
| `/createos-sandbox:fanout [cmd2 …]` | run each command in its own throwaway box, in parallel |
| `/createos-sandbox:shell` | instant throwaway interactive Linux (destroyed on exit) |
| `/createos-sandbox:up` · `run` · `sync` · `down` | reusable per-repo box + file sync for live dev loops |
diff --git a/packages/claude-code-plugin/README.md b/packages/claude-code-plugin/README.md
index 48a236d..ae5130f 100644
--- a/packages/claude-code-plugin/README.md
+++ b/packages/claude-code-plugin/README.md
@@ -70,7 +70,7 @@ The plugin is a **thin Claude-facing surface** over the `createos` CLI. It ships
| Piece | Path | Role |
| ------------------ | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
-| **Slash commands** | `commands/*.md` | 20 commands (`offload`, `fanout`, `shell`, …), each a thin wrapper that calls `scripts/cos` |
+| **Slash commands** | `commands/*.md` | 22 commands (`offload`, `exec`, `fanout`, `shell`, …), each a thin wrapper that calls `scripts/cos` |
| **Skill** | `skills/using-createos-sandbox/SKILL.md` + `references/` | teaches Claude _when_ to reach for the sandbox on its own, with depth loaded on demand |
| **Hooks** | `hooks/hooks.json` + `scripts/` | `SessionStart` publishes the driver's absolute path; `PreToolUse(Bash)` nudges on heavy build/test commands |
| **Driver** | `scripts/cos` | the actual logic — staging, egress, keepalive, sync, networking, lifecycle, state |
@@ -125,6 +125,7 @@ claude --plugin-dir /path/to/createos-plugin/packages/claude-code-plugin
| Command | Summary |
| --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
| [`offload`](#offload--one-shot) `[flags] ` | one-shot: stage → run (keepalive) → pull → destroy |
+| [`exec`](#exec--remote-code-execution) `[-l lang] [-i stdin] [-t secs] [-N] [args]` | run one source file (untrusted/ad-hoc) in a throwaway box |
| [`fanout`](#fanout--parallel-boxes) `[-j N] [flags] [cmd2] …` | run each command in its own throwaway box, in parallel |
| [`agent`](#coding-agents) `[flags] ` | run claude/codex/opencode/pi/cursor on your code in a box |
| [`shell`](#shell--throwaway-linux) `[-s] [-r] [-e\|-p\|-E]` | instant throwaway interactive Linux (destroyed on exit) |
@@ -176,6 +177,16 @@ The core command. Stages a directory into a fresh box, runs a command, optionall
/createos-sandbox:offload -p python-uv -o dist . "uv sync --frozen && uv run python -m build"
```
+### Exec — remote code execution
+
+Run untrusted code or any ad-hoc script — anything you would rather not run locally — as one source file in a throwaway box. No directory to stage.
+
+```
+/createos-sandbox:exec [-l lang] [-i stdin-file] [-t secs] [-N] [-p preset] [-e dom] [args...]
+```
+
+Languages `py js mjs cjs ts go sh rb c cpp rs`, picked from the extension or `-l`. `-i` feeds stdin, `-t` caps wall-clock time (default 120 s, exit 124), `-N` denies all egress (default is unrestricted, like `offload`). stdout/stderr and the exit code are the program's own.
+
**Keepalive:** long or quiet compiles no longer die to exec-stream idle resets — the command runs detached with a heartbeat and re-attaches if the stream drops, so the build (and its cache) survives.
### Fanout — parallel boxes
diff --git a/packages/claude-code-plugin/commands/exec.md b/packages/claude-code-plugin/commands/exec.md
new file mode 100644
index 0000000..88b0126
--- /dev/null
+++ b/packages/claude-code-plugin/commands/exec.md
@@ -0,0 +1,11 @@
+---
+description: Remote code execution — run untrusted code or any ad-hoc script/snippet (py, js, mjs, cjs, ts, go, sh, rb, c, cpp, rs) in a throwaway CreateOS box instead of on this machine. Timeout, optional stdin, real exit code, auto-destroys.
+argument-hint: "[-l lang] [-i stdin-file] [-t secs] [-N] [-p preset] [-e dom] [args...]"
+allowed-tools: Bash
+---
+
+Run one source file in a disposable CreateOS Sandbox. Flags precede ``; everything after `` goes to the program untouched. Language comes from the extension (`-l` overrides). Egress is unrestricted by default; `-p`/`-e` allow just those hosts, `-N` blocks outbound connections. `-i` feeds a local file to stdin; `-t` is a wall-clock limit (default 120 s, exit 124 when hit).
+
+!`if test -n "$ARGUMENTS"; then "${CLAUDE_PLUGIN_ROOT}/scripts/cos" exec $ARGUMENTS; else "${CLAUDE_PLUGIN_ROOT}/scripts/cos" exec; fi`
+
+Report the program's stdout, stderr and the `cos: exit=N time=Ns` line above. Exit 124 means the timeout killed it.
diff --git a/packages/claude-code-plugin/scripts/cos b/packages/claude-code-plugin/scripts/cos
index d9d6a9a..d23c64e 100755
--- a/packages/claude-code-plugin/scripts/cos
+++ b/packages/claude-code-plugin/scripts/cos
@@ -462,6 +462,87 @@ example:
EOF
}
+# ─────────────────────────────────────────────────── run one untrusted source file
+# Egress stays open by default (snippets often call APIs). -N denies all: any rule flips
+# CreateOS to deny-by-default and an IP rule is enforced at once (hostname rules are not),
+# so one unroutable TEST-NET-1 address allows nothing.
+DENY_ALL_EGRESS=192.0.2.1/32
+cmd_exec(){ # no _norm: it would rewrite the program's own --flags after ; getopts stops at
+ local shape=s-1vcpu-1gb rootfs=devbox:1 lang="" stdin="" to=120 deny=0
+ COS_EGRESS=(); local -a _d; local OPTIND=1 o doms d
+ while getopts "s:r:l:i:t:e:p:v:Nh" o; do case $o in
+ s) shape=$OPTARG;; r) rootfs=$OPTARG;; l) lang=$OPTARG;; i) stdin=$OPTARG;; t) to=$OPTARG;;
+ v) add_env "$OPTARG";;
+ e) COS_EGRESS+=(--egress "$OPTARG");;
+ p) doms=$(egress_preset "$OPTARG") || die "unknown egress preset '$OPTARG' (have: $EGRESS_PRESETS)"
+ read -ra _d <<<"$doms"; for d in "${_d[@]}"; do COS_EGRESS+=(--egress "$d"); done;;
+ N) deny=1;;
+ h) exec_usage; exit 0;;
+ *) exec_usage >&2; exit 2;; esac; done
+ shift $((OPTIND-1))
+ if [ $# -lt 1 ]; then exec_usage; exit 0; fi
+ local file=$1; shift
+ numeric "$to" || die "-t wants whole seconds, got '$to'"
+ [ -z "$stdin" ] || [ -f "$stdin" ] || die "no such stdin file: $stdin"
+ local src=$file
+ if [ "$file" = - ]; then src=$(mktemp); cat >"$src"; else [ -f "$file" ] || die "no such file: $file"; fi
+ [ -n "$lang" ] || lang=${file##*.}
+ [ "$lang" != - ] && [ "$lang" != "$file" ] || die "cannot tell the language — pass -l py|js|mjs|cjs|ts|go|sh|rb|c|cpp|rs"
+
+ local main run
+ case "$lang" in
+ py|python) main=main.py; run="python3 main.py";;
+ js|node) main=main.js; run="node main.js";;
+ mjs) main=main.mjs; run="node main.mjs";;
+ cjs) main=main.cjs; run="node main.cjs";;
+ ts|typescript) main=main.ts; run="bun main.ts";;
+ go) main=main.go; run="go run main.go";;
+ sh|bash) main=main.sh; run="bash main.sh";;
+ rb|ruby) main=main.rb; run="ruby main.rb";;
+ c) main=main.c; run="gcc -O2 -o main main.c && ./main";;
+ cpp|cc|cxx) main=main.cpp; run="g++ -O2 -o main main.cpp && ./main";;
+ rs|rust) main=main.rs; run="rustc -O -o main main.rs && ./main";;
+ *) die "unsupported language '$lang' — py|js|mjs|cjs|ts|go|sh|rb|c|cpp|rs (anything else: cos offload)";;
+ esac
+ local a; for a in "$@"; do run="$run $(printf '%q' "$a")"; done
+
+ [ "$deny" = 1 ] && { COS_EGRESS=(--egress "$DENY_ALL_EGRESS"); echo "cos: egress DENIED (-N)" >&2; }
+
+ OFFLOAD_ID=$(create_box "cos-x-$$-${RANDOM}" "$shape" "$rootfs")
+ KEEP=0; trap on_offload_exit EXIT
+ wait_running "$OFFLOAD_ID" 30 || die "box $OFFLOAD_ID not running after 30s"
+ "$CLI" sandbox push "$OFFLOAD_ID" "$src" "/work/$main" >/dev/null 2>&1 || die "push failed"
+ [ "$file" = - ] && rm -f "$src"
+ local in=/dev/null
+ if [ -n "$stdin" ]; then
+ "$CLI" sandbox push "$OFFLOAD_ID" "$stdin" /work/.stdin >/dev/null 2>&1 || die "stdin push failed"; in=.stdin
+ fi
+
+ # ponytail: buffered exec, fine for snippets; a job that outlives the stream belongs in offload (keepalive)
+ local t0 rc=0; t0=$(date +%s)
+ "$CLI" sandbox exec "$OFFLOAD_ID" -- bash -lc "cd /work && timeout -k 5 $to bash -c $(printf '%q' "$run") <$in" || rc=$?
+ echo "cos: exit=$rc time=$(( $(date +%s) - t0 ))s$([ "$rc" = 124 ] && echo " (killed: -t ${to}s timeout)")" >&2
+ return "$rc"
+}
+exec_usage(){ cat <<'EOF'
+cos exec — run one source file (untrusted/ad-hoc code) in a throwaway box (auto-destroyed).
+ cos exec [flags] [args...] ('-' reads the code from stdin; needs -l)
+flags:
+ -l lang py | js | mjs | cjs | ts | go | sh | rb | c | cpp | rs (default: from the file extension)
+ -i file feed this local file to the program's stdin
+ -t secs wall-clock limit, default 120 (exit 124 on timeout)
+ -s shape -r rootfs -v KEY[=VAL]
+ -N deny ALL egress (box reaches nothing) — for code you suspect of exfiltration
+ -e / -p allow just these hosts (default: unrestricted egress)
+stdout/stderr pass through; exit code is the program's; 'cos: exit=N time=Ns' goes to stderr.
+example:
+ cos exec -i input.txt solution.py
+ cos exec -l py - <<'PY'
+ print(sum(range(10)))
+ PY
+EOF
+}
+
# ───────────────────────────────────────────────────────────── reusable project box
cmd_up(){
_norm "$@"; set -- ${NORMA[@]+"${NORMA[@]}"}
@@ -1339,6 +1420,7 @@ main_usage(){ cat <<'EOF'
cos — CreateOS sandbox as remote compute. (run `cos install` to put `cos` on PATH)
cos auth check sign-in (CREATEOS_API_KEY, or `createos login` in a real terminal)
cos offload [flags] one-shot: stage→run(keepalive)→pull→destroy (cos offload -h for flags)
+ cos exec [flags] [args] run one untrusted source file (py|js|ts|go|sh|rb|c|cpp|rs); -N denies egress (cos exec -h)
cos agent [flags] run claude|codex|opencode|pi|cursor on your code in a box
against OpenRouter / any OpenAI- or Anthropic-compatible provider (cos agent -h)
cos fanout [-j N][flags] ... run each in its own throwaway box, in parallel (cos fanout -h)
@@ -1377,6 +1459,7 @@ case "$sub" in
install) cmd_install "$@";;
auth) cmd_auth "$@";;
offload) cmd_offload "$@";;
+ exec) cmd_exec "$@";;
agent) cmd_agent "$@";;
fanout) cmd_fanout "$@";;
shell) cmd_shell "$@";;
diff --git a/packages/claude-code-plugin/skills/using-createos-sandbox/SKILL.md b/packages/claude-code-plugin/skills/using-createos-sandbox/SKILL.md
index bda825a..1996d82 100644
--- a/packages/claude-code-plugin/skills/using-createos-sandbox/SKILL.md
+++ b/packages/claude-code-plugin/skills/using-createos-sandbox/SKILL.md
@@ -1,6 +1,6 @@
---
name: using-createos-sandbox
-description: Use when you need to run code OFF the user's machine — heavy/long builds or test suites, untrusted or unknown code, a parallel test/config matrix across many boxes, an instant clean Linux to try a tool, a live dev-server/watcher you edit against, reaching a box-side service from localhost (port tunnel) or sharing it on the public web (HTTPS preview URL), a multi-machine cluster on one private network, a WireGuard VPN into that network, mounting an S3 bucket of data, handing a coding task to another agent (Claude Code, Codex, OpenCode, Pi, Cursor) running on OpenRouter or any OpenAI-/Anthropic-compatible provider, or work that needs a real screen — a graphical Linux desktop with a browser that you drive by screenshot/click/type and the user can watch over noVNC. Offloads to ephemeral CreateOS Sandboxes via the `cos` helper (stage → exec → pull → auto-destroy), plus fanout, a scratch shell, and an opt-in reusable box with sync, tunnel, expose, desktop/computer-use, cluster, disk, vpn, pause/resume, custom images, and snapshot/fork. Also use to answer any question about CreateOS Sandbox itself — its REST API, SDKs (TypeScript, Go, Python, Rust, C#, Java), CLI commands, limits, lifecycle, egress, networks, disks, templates, webhooks, or integrations — by fetching the relevant live docs page listed in references/docs.md.
+description: Use when you need to run code OFF the user's machine — ALWAYS for untrusted or unknown code, and for any ad-hoc script or snippet you would otherwise run locally (remote code execution: `cos exec `), heavy/long builds or test suites, a parallel test/config matrix across many boxes, an instant clean Linux to try a tool, a live dev-server/watcher you edit against, reaching a box-side service from localhost (port tunnel) or sharing it on the public web (HTTPS preview URL), a multi-machine cluster on one private network, a WireGuard VPN into that network, mounting an S3 bucket of data, handing a coding task to another agent (Claude Code, Codex, OpenCode, Pi, Cursor) running on OpenRouter or any OpenAI-/Anthropic-compatible provider, or work that needs a real screen — a graphical Linux desktop with a browser that you drive by screenshot/click/type and the user can watch over noVNC. Offloads to ephemeral CreateOS Sandboxes via the `cos` helper (stage → exec → pull → auto-destroy), plus fanout, a scratch shell, and an opt-in reusable box with sync, tunnel, expose, desktop/computer-use, cluster, disk, vpn, pause/resume, custom images, and snapshot/fork. Also use to answer any question about CreateOS Sandbox itself — its REST API, SDKs (TypeScript, Go, Python, Rust, C#, Java), CLI commands, limits, lifecycle, egress, networks, disks, templates, webhooks, or integrations — by fetching the relevant live docs page listed in references/docs.md.
---
# Using CreateOS Sandbox as remote compute
@@ -38,7 +38,8 @@ Every `cos` command except `install` and `auth` runs this check first, so an una
| Situation | Why offload |
| ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
-| **Untrusted / unknown code** — a snippet, a fresh npm/pip package, scraped code, a PoC exploit | Isolation. The blast radius is one disposable box, not the laptop. |
+| **Untrusted / unknown code** — a snippet, a fresh npm/pip package, scraped code, a PoC exploit | Isolation. The blast radius is one disposable box, not the laptop. One file → `exec`. |
+| **Any ad-hoc script** — a one-off Python/JS/shell/Go snippet to compute, parse, probe or try something | Keep the laptop clean; `exec` runs it remotely and returns stdout, stderr and the exit code. |
| **Heavy build or test suite** — big `make`, full test run, compile, benchmark | Keeps the laptop free; runs on a box sized for it. |
| **Parallel/matrix work** — same job across N configs, test shards, batch | `fanout` — each command in its own throwaway box, concurrently, results collected. |
| **Quick scratch Linux** — try a CLI/tool/snippet on a clean box | `shell` — instant keyless box, destroyed on exit (interactive; the user runs it). |
@@ -60,6 +61,7 @@ Do NOT offload trivial commands, anything needing the user's local secrets/SSH/c
Almost every task is one of two shapes, and picking the wrong one wastes a lot of motion:
- **"Run this and tell me the result"** — a test suite, a build, a script, anything with an end. → **`cos offload `.** One command. It creates the box, ships the directory, runs, and destroys the box. Nothing to clean up.
+- **"Run this one piece of code"** — a snippet you wrote or were handed, untrusted code, a solution to test against an input. → **`cos exec `** (or `cos exec -l py -` with the code on stdin). No directory to stage.
- **"Keep a box around while I work"** — a dev server you'll hit repeatedly, a watcher reacting to edits, a session spanning many commands. → **`cos up`**, then `run`/`sync`, then `pause` or `down`.
If you find yourself doing any of the following, you have picked the wrong shape and should stop and use `offload` instead:
@@ -97,6 +99,22 @@ Long, quiet builds survive a dropped connection: the command runs detached with
For the full flag table, the egress presets, the enforcement caveats, fanout, and the OOM/disk/bandwidth traps on heavy builds → **`references/offload-and-egress.md`**.
+### Exec — remote code execution for one file
+
+Code you would rather not run on the user's machine — untrusted, generated, or just not yours to run locally — goes to `exec`: it writes the file into a fresh box, runs it with the right toolchain, and destroys the box.
+
+```bash
+cos exec -i input.txt solution.py arg1 # stdin from a file, args after the file
+cos exec -t 10 -N suspect.js # 10 s limit, no network at all
+cos exec -l go - <<'GO'
+package main
+import "fmt"
+func main() { fmt.Println("hi") }
+GO
+```
+
+Languages: `py js mjs cjs ts go sh rb c cpp rs` (from the extension, or `-l`). Everything after `` reaches the program unchanged. stdout/stderr pass through, the exit code is the program's, exit 124 means the `-t` limit (default 120 s) killed it, and `cos: exit=N time=Ns` goes to stderr. Egress is unrestricted by default like `offload`; `-N` denies all of it (enforced by an IP rule, so it applies immediately). More than one file, or dependencies to install → `offload`.
+
### Fanout — same input, many boxes, in parallel
```bash
diff --git a/packages/codex-plugin/scripts/cos b/packages/codex-plugin/scripts/cos
index d9d6a9a..d23c64e 100755
--- a/packages/codex-plugin/scripts/cos
+++ b/packages/codex-plugin/scripts/cos
@@ -462,6 +462,87 @@ example:
EOF
}
+# ─────────────────────────────────────────────────── run one untrusted source file
+# Egress stays open by default (snippets often call APIs). -N denies all: any rule flips
+# CreateOS to deny-by-default and an IP rule is enforced at once (hostname rules are not),
+# so one unroutable TEST-NET-1 address allows nothing.
+DENY_ALL_EGRESS=192.0.2.1/32
+cmd_exec(){ # no _norm: it would rewrite the program's own --flags after ; getopts stops at
+ local shape=s-1vcpu-1gb rootfs=devbox:1 lang="" stdin="" to=120 deny=0
+ COS_EGRESS=(); local -a _d; local OPTIND=1 o doms d
+ while getopts "s:r:l:i:t:e:p:v:Nh" o; do case $o in
+ s) shape=$OPTARG;; r) rootfs=$OPTARG;; l) lang=$OPTARG;; i) stdin=$OPTARG;; t) to=$OPTARG;;
+ v) add_env "$OPTARG";;
+ e) COS_EGRESS+=(--egress "$OPTARG");;
+ p) doms=$(egress_preset "$OPTARG") || die "unknown egress preset '$OPTARG' (have: $EGRESS_PRESETS)"
+ read -ra _d <<<"$doms"; for d in "${_d[@]}"; do COS_EGRESS+=(--egress "$d"); done;;
+ N) deny=1;;
+ h) exec_usage; exit 0;;
+ *) exec_usage >&2; exit 2;; esac; done
+ shift $((OPTIND-1))
+ if [ $# -lt 1 ]; then exec_usage; exit 0; fi
+ local file=$1; shift
+ numeric "$to" || die "-t wants whole seconds, got '$to'"
+ [ -z "$stdin" ] || [ -f "$stdin" ] || die "no such stdin file: $stdin"
+ local src=$file
+ if [ "$file" = - ]; then src=$(mktemp); cat >"$src"; else [ -f "$file" ] || die "no such file: $file"; fi
+ [ -n "$lang" ] || lang=${file##*.}
+ [ "$lang" != - ] && [ "$lang" != "$file" ] || die "cannot tell the language — pass -l py|js|mjs|cjs|ts|go|sh|rb|c|cpp|rs"
+
+ local main run
+ case "$lang" in
+ py|python) main=main.py; run="python3 main.py";;
+ js|node) main=main.js; run="node main.js";;
+ mjs) main=main.mjs; run="node main.mjs";;
+ cjs) main=main.cjs; run="node main.cjs";;
+ ts|typescript) main=main.ts; run="bun main.ts";;
+ go) main=main.go; run="go run main.go";;
+ sh|bash) main=main.sh; run="bash main.sh";;
+ rb|ruby) main=main.rb; run="ruby main.rb";;
+ c) main=main.c; run="gcc -O2 -o main main.c && ./main";;
+ cpp|cc|cxx) main=main.cpp; run="g++ -O2 -o main main.cpp && ./main";;
+ rs|rust) main=main.rs; run="rustc -O -o main main.rs && ./main";;
+ *) die "unsupported language '$lang' — py|js|mjs|cjs|ts|go|sh|rb|c|cpp|rs (anything else: cos offload)";;
+ esac
+ local a; for a in "$@"; do run="$run $(printf '%q' "$a")"; done
+
+ [ "$deny" = 1 ] && { COS_EGRESS=(--egress "$DENY_ALL_EGRESS"); echo "cos: egress DENIED (-N)" >&2; }
+
+ OFFLOAD_ID=$(create_box "cos-x-$$-${RANDOM}" "$shape" "$rootfs")
+ KEEP=0; trap on_offload_exit EXIT
+ wait_running "$OFFLOAD_ID" 30 || die "box $OFFLOAD_ID not running after 30s"
+ "$CLI" sandbox push "$OFFLOAD_ID" "$src" "/work/$main" >/dev/null 2>&1 || die "push failed"
+ [ "$file" = - ] && rm -f "$src"
+ local in=/dev/null
+ if [ -n "$stdin" ]; then
+ "$CLI" sandbox push "$OFFLOAD_ID" "$stdin" /work/.stdin >/dev/null 2>&1 || die "stdin push failed"; in=.stdin
+ fi
+
+ # ponytail: buffered exec, fine for snippets; a job that outlives the stream belongs in offload (keepalive)
+ local t0 rc=0; t0=$(date +%s)
+ "$CLI" sandbox exec "$OFFLOAD_ID" -- bash -lc "cd /work && timeout -k 5 $to bash -c $(printf '%q' "$run") <$in" || rc=$?
+ echo "cos: exit=$rc time=$(( $(date +%s) - t0 ))s$([ "$rc" = 124 ] && echo " (killed: -t ${to}s timeout)")" >&2
+ return "$rc"
+}
+exec_usage(){ cat <<'EOF'
+cos exec — run one source file (untrusted/ad-hoc code) in a throwaway box (auto-destroyed).
+ cos exec [flags] [args...] ('-' reads the code from stdin; needs -l)
+flags:
+ -l lang py | js | mjs | cjs | ts | go | sh | rb | c | cpp | rs (default: from the file extension)
+ -i file feed this local file to the program's stdin
+ -t secs wall-clock limit, default 120 (exit 124 on timeout)
+ -s shape -r rootfs -v KEY[=VAL]
+ -N deny ALL egress (box reaches nothing) — for code you suspect of exfiltration
+ -e / -p allow just these hosts (default: unrestricted egress)
+stdout/stderr pass through; exit code is the program's; 'cos: exit=N time=Ns' goes to stderr.
+example:
+ cos exec -i input.txt solution.py
+ cos exec -l py - <<'PY'
+ print(sum(range(10)))
+ PY
+EOF
+}
+
# ───────────────────────────────────────────────────────────── reusable project box
cmd_up(){
_norm "$@"; set -- ${NORMA[@]+"${NORMA[@]}"}
@@ -1339,6 +1420,7 @@ main_usage(){ cat <<'EOF'
cos — CreateOS sandbox as remote compute. (run `cos install` to put `cos` on PATH)
cos auth check sign-in (CREATEOS_API_KEY, or `createos login` in a real terminal)
cos offload [flags] one-shot: stage→run(keepalive)→pull→destroy (cos offload -h for flags)
+ cos exec [flags] [args] run one untrusted source file (py|js|ts|go|sh|rb|c|cpp|rs); -N denies egress (cos exec -h)
cos agent [flags] run claude|codex|opencode|pi|cursor on your code in a box
against OpenRouter / any OpenAI- or Anthropic-compatible provider (cos agent -h)
cos fanout [-j N][flags] ... run each in its own throwaway box, in parallel (cos fanout -h)
@@ -1377,6 +1459,7 @@ case "$sub" in
install) cmd_install "$@";;
auth) cmd_auth "$@";;
offload) cmd_offload "$@";;
+ exec) cmd_exec "$@";;
agent) cmd_agent "$@";;
fanout) cmd_fanout "$@";;
shell) cmd_shell "$@";;
diff --git a/packages/codex-plugin/skills/using-createos-sandbox/SKILL.md b/packages/codex-plugin/skills/using-createos-sandbox/SKILL.md
index 3969c3d..1996d82 100644
--- a/packages/codex-plugin/skills/using-createos-sandbox/SKILL.md
+++ b/packages/codex-plugin/skills/using-createos-sandbox/SKILL.md
@@ -1,6 +1,6 @@
---
name: using-createos-sandbox
-description: Use when you need to run code OFF the user's machine — heavy/long builds or test suites, untrusted or unknown code, a parallel test/config matrix across many boxes, an instant clean Linux to try a tool, a live dev-server/watcher you edit against, reaching a box-side service from localhost (port tunnel) or sharing it on the public web (HTTPS preview URL), a multi-machine cluster on one private network, a WireGuard VPN into that network, mounting an S3 bucket of data, handing a coding task to another agent (Claude Code, Codex, OpenCode, Pi, Cursor) running on OpenRouter or any OpenAI-/Anthropic-compatible provider, or work that needs a real screen — a graphical Linux desktop with a browser that you drive by screenshot/click/type and the user can watch over noVNC. Offloads to ephemeral CreateOS Sandboxes via the `cos` helper (stage → exec → pull → auto-destroy), plus fanout, a scratch shell, and an opt-in reusable box with sync, tunnel, expose, desktop/computer-use, cluster, disk, vpn, pause/resume, custom images, and snapshot/fork.
+description: Use when you need to run code OFF the user's machine — ALWAYS for untrusted or unknown code, and for any ad-hoc script or snippet you would otherwise run locally (remote code execution: `cos exec `), heavy/long builds or test suites, a parallel test/config matrix across many boxes, an instant clean Linux to try a tool, a live dev-server/watcher you edit against, reaching a box-side service from localhost (port tunnel) or sharing it on the public web (HTTPS preview URL), a multi-machine cluster on one private network, a WireGuard VPN into that network, mounting an S3 bucket of data, handing a coding task to another agent (Claude Code, Codex, OpenCode, Pi, Cursor) running on OpenRouter or any OpenAI-/Anthropic-compatible provider, or work that needs a real screen — a graphical Linux desktop with a browser that you drive by screenshot/click/type and the user can watch over noVNC. Offloads to ephemeral CreateOS Sandboxes via the `cos` helper (stage → exec → pull → auto-destroy), plus fanout, a scratch shell, and an opt-in reusable box with sync, tunnel, expose, desktop/computer-use, cluster, disk, vpn, pause/resume, custom images, and snapshot/fork. Also use to answer any question about CreateOS Sandbox itself — its REST API, SDKs (TypeScript, Go, Python, Rust, C#, Java), CLI commands, limits, lifecycle, egress, networks, disks, templates, webhooks, or integrations — by fetching the relevant live docs page listed in references/docs.md.
---
# Using CreateOS Sandbox as remote compute
@@ -38,7 +38,8 @@ Every `cos` command except `install` and `auth` runs this check first, so an una
| Situation | Why offload |
| ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
-| **Untrusted / unknown code** — a snippet, a fresh npm/pip package, scraped code, a PoC exploit | Isolation. The blast radius is one disposable box, not the laptop. |
+| **Untrusted / unknown code** — a snippet, a fresh npm/pip package, scraped code, a PoC exploit | Isolation. The blast radius is one disposable box, not the laptop. One file → `exec`. |
+| **Any ad-hoc script** — a one-off Python/JS/shell/Go snippet to compute, parse, probe or try something | Keep the laptop clean; `exec` runs it remotely and returns stdout, stderr and the exit code. |
| **Heavy build or test suite** — big `make`, full test run, compile, benchmark | Keeps the laptop free; runs on a box sized for it. |
| **Parallel/matrix work** — same job across N configs, test shards, batch | `fanout` — each command in its own throwaway box, concurrently, results collected. |
| **Quick scratch Linux** — try a CLI/tool/snippet on a clean box | `shell` — instant keyless box, destroyed on exit (interactive; the user runs it). |
@@ -60,6 +61,7 @@ Do NOT offload trivial commands, anything needing the user's local secrets/SSH/c
Almost every task is one of two shapes, and picking the wrong one wastes a lot of motion:
- **"Run this and tell me the result"** — a test suite, a build, a script, anything with an end. → **`cos offload `.** One command. It creates the box, ships the directory, runs, and destroys the box. Nothing to clean up.
+- **"Run this one piece of code"** — a snippet you wrote or were handed, untrusted code, a solution to test against an input. → **`cos exec `** (or `cos exec -l py -` with the code on stdin). No directory to stage.
- **"Keep a box around while I work"** — a dev server you'll hit repeatedly, a watcher reacting to edits, a session spanning many commands. → **`cos up`**, then `run`/`sync`, then `pause` or `down`.
If you find yourself doing any of the following, you have picked the wrong shape and should stop and use `offload` instead:
@@ -97,6 +99,22 @@ Long, quiet builds survive a dropped connection: the command runs detached with
For the full flag table, the egress presets, the enforcement caveats, fanout, and the OOM/disk/bandwidth traps on heavy builds → **`references/offload-and-egress.md`**.
+### Exec — remote code execution for one file
+
+Code you would rather not run on the user's machine — untrusted, generated, or just not yours to run locally — goes to `exec`: it writes the file into a fresh box, runs it with the right toolchain, and destroys the box.
+
+```bash
+cos exec -i input.txt solution.py arg1 # stdin from a file, args after the file
+cos exec -t 10 -N suspect.js # 10 s limit, no network at all
+cos exec -l go - <<'GO'
+package main
+import "fmt"
+func main() { fmt.Println("hi") }
+GO
+```
+
+Languages: `py js mjs cjs ts go sh rb c cpp rs` (from the extension, or `-l`). Everything after `` reaches the program unchanged. stdout/stderr pass through, the exit code is the program's, exit 124 means the `-t` limit (default 120 s) killed it, and `cos: exit=N time=Ns` goes to stderr. Egress is unrestricted by default like `offload`; `-N` denies all of it (enforced by an IP rule, so it applies immediately). More than one file, or dependencies to install → `offload`.
+
### Fanout — same input, many boxes, in parallel
```bash
@@ -224,7 +242,7 @@ Disk data lives in the user's own S3 account and region. `--path-style` is neede
- **Concurrency is limited** — external keys have been observed to allow 2 boxes running at once, with a daily creation cap. This is observed behaviour rather than published policy, so budget `cluster` and `fanout` against it and expect excess jobs to queue rather than fail.
- If a shape is rejected, the error names the allowed list — pick from it, or run `createos sandbox shapes`.
- Pre-existing boxes the user already runs are **not** yours. `cos` only ever destroys boxes it created itself; a box adopted with `cos up -a` survives `cos down`.
-- CreateOS Sandbox is in alpha with no SLA. When a limit or a number matters to a decision, check it live rather than quoting it from here.
+- CreateOS Sandbox is in alpha with no SLA. When a limit or a number matters to a decision, check it live rather than quoting it from here — `createos sandbox shapes`, or the [Limits](https://createos.sh/docs/Sandbox/Limits.md) page.
## References
@@ -236,3 +254,4 @@ Load these when the task actually needs the depth — the summaries above are en
| `references/networking.md` | choosing between tunnel/expose/cluster/vpn, cluster DNS names, expose gotchas, WireGuard setup |
| `references/coding-agents.md` | the five agent CLIs in `devbox:1`, per-agent provider wiring (OpenRouter / OpenAI-compatible / Anthropic-compatible), which agents can't be repointed, egress around an agent box |
| `references/lifecycle-and-images.md` | pause/resume, auto-pause tuning, fork caveats, built-in rootfs vs custom templates, env vars, remote editor, self-terminating jobs, single-file transfer, measured timings |
+| `references/docs.md` | every page of the live CreateOS Sandbox docs as a fetchable `.md` URL — REST API, SDKs, CLI reference, limits, concepts, integrations. Fetch only the page you need |
diff --git a/packages/codex-plugin/skills/using-createos-sandbox/references/docs.md b/packages/codex-plugin/skills/using-createos-sandbox/references/docs.md
new file mode 100644
index 0000000..fad9d2a
--- /dev/null
+++ b/packages/codex-plugin/skills/using-createos-sandbox/references/docs.md
@@ -0,0 +1,114 @@
+# CreateOS Sandbox docs — live index
+
+Every CreateOS Sandbox docs page, as raw markdown. Use this when the question is
+about the **product** — REST endpoints, SDK methods, CLI flags, limits, lifecycle,
+egress semantics, integrations — rather than about driving `cos`.
+
+**How to use:** pick the one or two pages that answer the question and fetch them
+(`WebFetch`, or `curl -sL `). Every URL below is `.md` and returns
+`text/markdown` — no HTML scraping needed. Do not fetch the whole list.
+
+The docs are the source of truth and newer than this plugin. When a page
+disagrees with `SKILL.md` or another reference file on a number or an API shape,
+trust the page. Behaviour this plugin measured itself (egress enforcement, the
+concurrency cap, OOM traps) stays in the other reference files.
+
+If a URL 404s, the page moved: re-derive the list from
+ (lines under `/Sandbox/`) and append `.md`.
+
+## Start here
+
+- [Sandbox](https://createos.sh/docs/Sandbox.md) — landing page: what the product is for.
+- [Overview](https://createos.sh/docs/Sandbox/Overview.md) — disposable Linux microVMs for untrusted code, AI agents, CI jobs, previews, networking, persistence, forking.
+- [Quickstart](https://createos.sh/docs/Sandbox/Quickstart.md) — first sandbox with CLI, TypeScript SDK and REST side by side.
+- [Concepts](https://createos.sh/docs/Sandbox/Concepts.md) — vocabulary: shapes, root filesystems, lifecycle state machine, pause and fork, private networks, egress rules, disks, templates.
+- [Limits & defaults](https://createos.sh/docs/Sandbox/Limits.md) — shapes, disks, networks, timeouts, quotas.
+- [Bring your own storage](https://createos.sh/docs/Sandbox/Bring-Your-Own-Storage.md) — snapshots and disks in your own S3 / R2 / MinIO / Tigris.
+- [Run on your own infrastructure](https://createos.sh/docs/Sandbox/Self-Hosting.md) — self-hosted / on-prem data plane (alpha, enterprise).
+- [Claude Managed Agents](https://createos.sh/docs/Sandbox/Claude-Managed-Agents.md) — run Claude Managed Agents inside sandboxes: setup, credentials, egress allowlist, cleanup.
+
+## CLI
+
+- [CLI](https://createos.sh/docs/Sandbox/CLI.md) — install, sign in, manage sandboxes from a terminal or CI.
+- [Overview](https://createos.sh/docs/Sandbox/CLI/Overview.md) — create, exec, sync, tunnel, destroy.
+- [Command Reference](https://createos.sh/docs/Sandbox/CLI/Commands.md) — every `createos sandbox` command and flag.
+- [Sandboxes (CLI section)](https://createos.sh/docs/CLI/Sandbox.md) — `createos sandbox` / `sb` from the main CLI docs, including shell and tunnel.
+
+## REST API
+
+- [REST API](https://createos.sh/docs/Sandbox/REST-API.md) — index of resource groups.
+- [Overview](https://createos.sh/docs/Sandbox/REST-API/Overview.md) — base URL, `X-Api-Key` auth, JSend envelope, rate behaviour.
+- [Sandboxes](https://createos.sh/docs/Sandbox/REST-API/Sandboxes.md) — create / list / inspect / destroy: shapes, rootfs, create-time egress, auto-pause, envs, ingress, regions, status lifecycle.
+- [Execution & Files](https://createos.sh/docs/Sandbox/REST-API/Execution-And-Files.md) — run commands (buffered or streamed), upload / download files and directories.
+- [Managed Processes](https://createos.sh/docs/Sandbox/REST-API/Managed-Processes.md) — start, supervise, stop long-running processes.
+- [Pause, Resume & Fork](https://createos.sh/docs/Sandbox/REST-API/Pause-Resume-Fork.md) — snapshot, restore, clone.
+- [Egress](https://createos.sh/docs/Sandbox/REST-API/Egress.md) — open with no rules, deny-by-default with any rule, in-kernel enforcement, rule formats.
+- [Networks](https://createos.sh/docs/Sandbox/REST-API/Networks.md) — private overlay networks, DNS names, limits.
+- [Devices & VPN](https://createos.sh/docs/Sandbox/REST-API/Devices.md) — WireGuard devices into private networks.
+- [Shell & Tunnels](https://createos.sh/docs/Sandbox/REST-API/Connections.md) — interactive shells and port tunnels.
+- [Disks](https://createos.sh/docs/Sandbox/REST-API/Disks.md) — register S3-compatible disks, mount / detach on running sandboxes.
+- [Templates](https://createos.sh/docs/Sandbox/REST-API/Templates.md) — custom rootfs from a Dockerfile: build status, limits.
+- [Bandwidth & Resize](https://createos.sh/docs/Sandbox/REST-API/Bandwidth-And-Resize.md) — bandwidth budget check / recharge, shape resize.
+- [Catalog & Identity](https://createos.sh/docs/Sandbox/REST-API/Catalog-And-Identity.md) — list shapes and root filesystems, whoami.
+- [Sandbox access tokens](https://createos.sh/docs/Sandbox/REST-API/Access-Tokens.md) — delegated credential scoped to one sandbox: create, rotate, disable.
+- [Self-Signal (In-Sandbox)](https://createos.sh/docs/Sandbox/REST-API/Self-Signal.md) — endpoints a sandbox calls on itself.
+- [Metrics](https://createos.sh/docs/Sandbox/REST-API/Metrics.md) — per-sandbox CPU, memory, disk, network.
+- [Webhooks](https://createos.sh/docs/Sandbox/REST-API/Webhooks.md) — lifecycle events, payload, retries, signature verification.
+- [Computer](https://createos.sh/docs/Sandbox/REST-API/Computer.md) — screenshots, input events, desktop control.
+
+## SDK — getting started
+
+- [SDK](https://createos.sh/docs/Sandbox/SDK.md) — SDKs for TypeScript, Go, Python, Rust, C#, Java.
+- [SDKs overview](https://createos.sh/docs/Sandbox/SDK/Overview.md) — install / create / run / files / cleanup in each language.
+- [Quickstart](https://createos.sh/docs/Sandbox/SDK/Quickstart.md) — `@nodeops-createos/sandbox` in five minutes.
+- [Tutorial](https://createos.sh/docs/Sandbox/SDK/Tutorial.md) — build a code-execution service: restrict egress, run untrusted code, collect output.
+- [Examples](https://createos.sh/docs/Sandbox/SDK/Examples.md) — agent code execution, fork-based parallel rollouts, multi-sandbox networks.
+
+## SDK — how-to
+
+- [How-To Guides](https://createos.sh/docs/Sandbox/SDK/How-To.md) — index.
+- [Upload & Download Files](https://createos.sh/docs/Sandbox/SDK/How-To/Files.md)
+- [Pause, Fork & Auto-Pause](https://createos.sh/docs/Sandbox/SDK/How-To/Lifecycle.md)
+- [Expose a Service](https://createos.sh/docs/Sandbox/SDK/How-To/Expose-A-Service.md)
+- [Disks, Networks & Templates](https://createos.sh/docs/Sandbox/SDK/How-To/Disks-Networks-Templates.md)
+- [Stream Command Output](https://createos.sh/docs/Sandbox/SDK/How-To/Streaming.md)
+- [Error Handling](https://createos.sh/docs/Sandbox/SDK/How-To/Error-Handling.md)
+- [Observability](https://createos.sh/docs/Sandbox/SDK/How-To/Observability.md)
+- [Delegate access to one sandbox](https://createos.sh/docs/Sandbox/SDK/How-To/Sandbox-Access-Tokens.md) — sandbox access token for a worker.
+
+## SDK — API reference (TypeScript)
+
+- [API Reference](https://createos.sh/docs/Sandbox/SDK/Reference.md) — index.
+- [Overview](https://createos.sh/docs/Sandbox/SDK/Reference/Overview.md)
+- [Client](https://createos.sh/docs/Sandbox/SDK/Reference/Client.md)
+- [Sandbox](https://createos.sh/docs/Sandbox/SDK/Reference/Sandbox.md)
+- [Sandbox Files](https://createos.sh/docs/Sandbox/SDK/Reference/Sandbox-Files.md)
+- [Sub-APIs](https://createos.sh/docs/Sandbox/SDK/Reference/Sub-APIs.md) — disks, networks, templates.
+- [Managed Processes](https://createos.sh/docs/Sandbox/SDK/Reference/Managed-Processes.md)
+- [Computer](https://createos.sh/docs/Sandbox/SDK/Reference/Computer.md)
+- [Errors](https://createos.sh/docs/Sandbox/SDK/Reference/Errors.md)
+- [Helpers](https://createos.sh/docs/Sandbox/SDK/Reference/Helpers.md)
+- [Types](https://createos.sh/docs/Sandbox/SDK/Reference/Types.md)
+
+## SDK — explanation
+
+- [Concepts](https://createos.sh/docs/Sandbox/SDK/Explanation.md) — index.
+- [VM Sandboxes](https://createos.sh/docs/Sandbox/SDK/Explanation/VM-Sandboxes.md) — why a Firecracker microVM, not a container.
+- [The Handle Model](https://createos.sh/docs/Sandbox/SDK/Explanation/Handle-Model.md) — what the `Sandbox` handle caches and when it refreshes.
+- [Sandbox Lifecycle](https://createos.sh/docs/Sandbox/SDK/Explanation/Lifecycle.md) — creating → running → pausing → paused → resuming → forking → destroying.
+- [Reliability](https://createos.sh/docs/Sandbox/SDK/Explanation/Reliability.md) — retries, timeouts, partial state.
+
+## Integrations
+
+- [Integrations](https://createos.sh/docs/Sandbox/Integrations.md) — index.
+- [Overview](https://createos.sh/docs/Sandbox/Integrations/Overview.md)
+- [Claude Code](https://createos.sh/docs/Sandbox/Integrations/Claude-Code.md) — this plugin.
+- [Codex](https://createos.sh/docs/Sandbox/Integrations/Codex.md)
+- [OpenCode](https://createos.sh/docs/Sandbox/Integrations/OpenCode.md)
+- [Pi](https://createos.sh/docs/Sandbox/Integrations/Pi.md)
+- [Herdr](https://createos.sh/docs/Sandbox/Integrations/Herdr.md)
+- [DeepSeek Harness](https://createos.sh/docs/Sandbox/Integrations/DeepSeek-Harness.md)
+- [Orca](https://createos.sh/docs/Sandbox/Integrations/Orca.md)
+- [n8n](https://createos.sh/docs/Sandbox/Integrations/n8n.md)
+- [Langflow](https://createos.sh/docs/Sandbox/Integrations/Langflow.md)
+- [AgentBox](https://createos.sh/docs/Sandbox/Integrations/AgentBox.md)
diff --git a/packages/opencode-plugin/README.md b/packages/opencode-plugin/README.md
index daa60d8..dbc1827 100644
--- a/packages/opencode-plugin/README.md
+++ b/packages/opencode-plugin/README.md
@@ -74,6 +74,7 @@ Two shapes of work, two tools. Getting this wrong is the most common mistake:
| Work | Tool |
| ------------------------------------------------------------ | --------------------------------------------------------- |
+| Untrusted code or any ad-hoc script — one program's source | `sandbox_run_code` — stdout, stderr, exit code; box destroyed |
| Has a finish line — a build, a test suite, a script | `sandbox_offload` — one call, box destroyed afterwards |
| Several variants of that at once — shards, a config matrix | `sandbox_fanout` — one throwaway box per command |
| Outlives one command — a dev server, a watcher, a session | `sandbox_create` + `sandbox_exec`, then `sandbox_destroy` |
@@ -84,7 +85,11 @@ build actually needs, a keepalive so a dropped stream does not kill a long build
guaranteed destruction even when the command throws, and staging excludes that
keep `.git`, `node_modules`, `target` and large media off the wire.
-## Tool inventory (38 tools)
+For questions about CreateOS Sandbox itself, the agent is told to fetch the live docs:
+every page listed under `/Sandbox/` in is raw
+markdown at `https://createos.sh/docs.md`.
+
+## Tool inventory (39 tools)
### Offload engine
@@ -92,6 +97,7 @@ keep `.git`, `node_modules`, `target` and large media off the wire.
| -------------------- | ---------------------------------------------------------------------------- |
| `sandbox_offload` | Stage a directory, run a command, pull artifacts, destroy the box |
| `sandbox_fanout` | Run each of several commands in its own throwaway box, in parallel |
+| `sandbox_run_code` | Remote code execution: run one program (py/js/ts/go/sh/rb/c/cpp/rs) with stdin, args and a timeout; egress open unless `egress_deny_all`/presets |
### Desktop / computer use
diff --git a/packages/opencode-plugin/index.ts b/packages/opencode-plugin/index.ts
index d731a7b..8b011ef 100644
--- a/packages/opencode-plugin/index.ts
+++ b/packages/opencode-plugin/index.ts
@@ -125,9 +125,12 @@ export const CreateOSPlugin: Plugin = async ({ project, client, $, directory })
`ONE call: it creates the box, stages the dir, runs, and destroys the box. Do not hand-roll that out of ` +
`sandbox_create + sandbox_exec — that drops egress restriction, the keepalive, and the guaranteed destroy.\n` +
`- Several variants of that at once (shards, a config matrix) → sandbox_fanout\n` +
+ `- Untrusted code or any ad-hoc script/snippet you would otherwise run locally → sandbox_run_code (code + lang)\n` +
`- "mount/sync this dir" → sandbox_sync local_dir="${hostCwd}" remote_dir="/root/project"\n` +
`- Port access → sandbox_preview_url (public URL) > sandbox_tunnel (localhost) > device VPN (last resort)\n` +
- `- Multi-node → sandbox_network_create + sandbox_create with network + sandbox_exec on other sandboxes`,
+ `- Multi-node → sandbox_network_create + sandbox_create with network + sandbox_exec on other sandboxes\n` +
+ `- Questions about CreateOS Sandbox itself (REST API, SDKs, CLI, limits) → fetch the matching page listed in ` +
+ `https://createos.sh/docs/llms.txt (under /Sandbox/); every page is raw markdown at https://createos.sh/docs.md`,
);
},
diff --git a/packages/opencode-plugin/src/sandbox-engine.ts b/packages/opencode-plugin/src/sandbox-engine.ts
index b8dcd9d..6a6a81c 100644
--- a/packages/opencode-plugin/src/sandbox-engine.ts
+++ b/packages/opencode-plugin/src/sandbox-engine.ts
@@ -15,7 +15,7 @@
* file drops into any TypeScript plugin.
*/
-import { execSync } from "node:child_process";
+import { execSync, spawnSync } from "node:child_process";
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
@@ -191,9 +191,16 @@ export interface EgressOptions {
egressPresets?: string[];
/** Unrestricted egress — only for a trusted offload. */
egressAll?: boolean;
+ /** Deny ALL egress: any rule flips CreateOS to deny-by-default, and an IP rule is
+ * enforced at once (hostname rules are not), so one unroutable TEST-NET-1 address
+ * allows nothing. Mirrors `cos exec -N`. */
+ egressDenyAll?: boolean;
}
+export const DENY_ALL_EGRESS = "192.0.2.1/32";
+
export function egressArgs(opts: EgressOptions): { args: string[]; warning?: string } {
+ if (opts.egressDenyAll) return { args: ["--egress", DENY_ALL_EGRESS] };
if (opts.egressAll) return { args: [], warning: undefined };
const domains = [
...(opts.egress ?? []),
@@ -518,6 +525,111 @@ export function cleanupFailureNote(sandboxId: string, error?: string): string {
);
}
+// ---------------------------------------------------------------------------
+// Remote code execution — one source file in a throwaway box (`cos exec`)
+// ---------------------------------------------------------------------------
+
+/** Language → file name in /work and the command that runs it. `.js` stays CommonJS-capable. */
+export const RUN_CODE_LANGS: Record = {
+ py: { file: "main.py", run: "python3 main.py" },
+ js: { file: "main.js", run: "node main.js" },
+ mjs: { file: "main.mjs", run: "node main.mjs" },
+ cjs: { file: "main.cjs", run: "node main.cjs" },
+ ts: { file: "main.ts", run: "bun main.ts" },
+ go: { file: "main.go", run: "go run main.go" },
+ sh: { file: "main.sh", run: "bash main.sh" },
+ rb: { file: "main.rb", run: "ruby main.rb" },
+ c: { file: "main.c", run: "gcc -O2 -o main main.c && ./main" },
+ cpp: { file: "main.cpp", run: "g++ -O2 -o main main.cpp && ./main" },
+ rs: { file: "main.rs", run: "rustc -O -o main main.rs && ./main" },
+};
+
+export interface RunCodeOptions extends EgressOptions {
+ code: string;
+ lang: string;
+ /** Passed to the program untouched. */
+ args?: string[];
+ stdin?: string;
+ /** Wall-clock limit; the program is killed and exits 124 when hit. Default 120. */
+ timeoutSec?: number;
+ shape?: string;
+ rootfs?: string;
+}
+
+export interface RunCodeResult extends ExecResult {
+ timedOut: boolean;
+ durationMs: number;
+ warnings: string[];
+}
+
+/** The in-box command: timeout-wrapped, stdin from a pushed file or /dev/null. */
+export function runCodeCommand(
+ lang: string,
+ args: string[],
+ timeoutSec: number,
+ hasStdin: boolean,
+): string {
+ const spec = RUN_CODE_LANGS[lang];
+ if (!spec) {
+ throw new Error(
+ `Unsupported language '${lang}' — have: ${Object.keys(RUN_CODE_LANGS).join(", ")}`,
+ );
+ }
+ const run = [spec.run, ...args.map(shq)].join(" ");
+ return `cd /work && timeout -k 5 ${timeoutSec} bash -c ${shq(run)} <${hasStdin ? ".stdin" : "/dev/null"}`;
+}
+
+/**
+ * Run one piece of code off the user's machine: create → push → run → destroy.
+ * Buffered exec on purpose — a job that outlives one exec stream belongs in offload().
+ */
+export async function runCode(opts: RunCodeOptions): Promise {
+ assertAuth();
+ const timeoutSec = opts.timeoutSec ?? 120;
+ const cmd = runCodeCommand(opts.lang, opts.args ?? [], timeoutSec, opts.stdin !== undefined);
+ const { id, warning } = createBox({
+ ...opts,
+ name: `cos-x-${process.pid}-${Math.floor(Math.random() * 1e6)}`,
+ });
+ const warnings = warning ? [warning] : [];
+ try {
+ if (!(await waitRunning(id))) throw new Error(`Sandbox ${id} did not reach running within 30s`);
+ const push = (content: string, remote: string) => {
+ const res = spawnSync("createos", ["sandbox", "push", id, "-", remote], {
+ input: content,
+ encoding: "utf-8",
+ timeout: 120_000,
+ });
+ if (res.status !== 0)
+ throw new Error(
+ `Push of ${remote} failed: ${(res.stderr || res.stdout || String(res.error)).trim()}`,
+ );
+ };
+ push(opts.code, `/work/${RUN_CODE_LANGS[opts.lang].file}`);
+ if (opts.stdin !== undefined) push(opts.stdin, "/work/.stdin");
+ const t0 = Date.now();
+ // spawnSync, not execShell: execShell drops stderr when the exit code is 0,
+ // and a program's stderr is part of its answer.
+ const r = spawnSync("createos", ["sandbox", "exec", id, "--", "bash", "-lc", cmd], {
+ encoding: "utf-8",
+ timeout: (timeoutSec + 60) * 1000,
+ maxBuffer: 64 * 1024 * 1024,
+ });
+ const code = r.status ?? 1;
+ return {
+ code,
+ stdout: r.stdout ?? "",
+ stderr: r.stderr || (r.error ? String(r.error) : ""),
+ timedOut: code === 124,
+ durationMs: Date.now() - t0,
+ warnings,
+ };
+ } finally {
+ const d = destroyBox(id);
+ if (!d.ok) warnings.push(cleanupFailureNote(id, d.error));
+ }
+}
+
/**
* Stage → run (keepalive) → pull → destroy, in one call.
*
diff --git a/packages/opencode-plugin/src/tools.ts b/packages/opencode-plugin/src/tools.ts
index a4643a9..8de4070 100644
--- a/packages/opencode-plugin/src/tools.ts
+++ b/packages/opencode-plugin/src/tools.ts
@@ -701,6 +701,61 @@ export function createTools($: any, getActive: () => ToolSandbox | null) {
},
}),
+ sandbox_run_code: tool({
+ description:
+ "Remote code execution: run untrusted code or ANY ad-hoc script/snippet in a THROWAWAY " +
+ "sandbox instead of on this machine, then destroy the box. Pass the source as `code`. " +
+ "Returns stdout, stderr and the program's exit code (124 = timeout). Use it whenever you " +
+ "would otherwise run a one-off script locally. Several files or dependencies to install → sandbox_offload.",
+ args: {
+ code: tool.schema.string().describe("Full source of the program"),
+ lang: tool.schema
+ .string()
+ .describe(`Language: ${Object.keys(engine.RUN_CODE_LANGS).join(" | ")}`),
+ args: tool.schema
+ .array(tool.schema.string())
+ .optional()
+ .describe("Program arguments, passed through untouched"),
+ stdin: tool.schema.string().optional().describe("Text fed to the program's stdin"),
+ timeout_sec: tool.schema
+ .number()
+ .optional()
+ .describe("Wall-clock limit in seconds, default 120"),
+ egress_deny_all: tool.schema
+ .boolean()
+ .optional()
+ .describe("Block all outbound connections. Egress is unrestricted by default"),
+ egress_presets: tool.schema
+ .array(tool.schema.string())
+ .optional()
+ .describe("Allow only what these ecosystems need: python-uv | rust-cargo | npm | github"),
+ egress: tool.schema
+ .array(tool.schema.string())
+ .optional()
+ .describe("Allow only these hosts; composes with egress_presets"),
+ shape: tool.schema.string().optional().describe("VM size. Defaults to 's-1vcpu-1gb'"),
+ },
+ async execute(args) {
+ const res = await engine.runCode({
+ code: args.code,
+ lang: args.lang,
+ args: args.args,
+ stdin: args.stdin,
+ timeoutSec: args.timeout_sec,
+ egressDenyAll: args.egress_deny_all,
+ egressPresets: args.egress_presets,
+ egress: args.egress,
+ shape: args.shape,
+ });
+ const lines = [
+ `exit code ${res.code}${res.timedOut ? " (killed by timeout)" : ""} in ${(res.durationMs / 1000).toFixed(1)}s`,
+ ];
+ for (const w of res.warnings) lines.push(`warning: ${w}`);
+ lines.push("", "stdout:", res.stdout || "(empty)", "", "stderr:", res.stderr || "(empty)");
+ return lines.join("\n");
+ },
+ }),
+
sandbox_fanout: tool({
description:
"Run each command in its OWN throwaway sandbox, in parallel, from the same staged directory. " +
diff --git a/packages/pi-extension/README.md b/packages/pi-extension/README.md
index b9cecd2..71280cb 100644
--- a/packages/pi-extension/README.md
+++ b/packages/pi-extension/README.md
@@ -81,6 +81,15 @@ it actually needs), the keepalive, and the guaranteed destroy, so a "successful"
leave an unrestricted box billing. The upload already skips `.git`, `node_modules`,
`target`, virtualenvs and large media.
+### Run untrusted code or an ad-hoc script
+
+`sandbox_run_code` is remote code execution for one program: pass the source as `code` with a
+`lang` (py, js, mjs, cjs, ts, go, sh, rb, c, cpp, rs), plus optional `args`, `stdin` and
+`timeout_sec` (default 120, exit 124 when hit). It returns stdout, stderr and the exit code and
+destroys the box. Egress is open unless `egress_deny_all` or presets restrict it. Its prompt
+guidelines also point the agent at the live CreateOS Sandbox docs
+(`https://createos.sh/docs/llms.txt`, each page raw markdown at `.md`).
+
### Drive a graphical desktop
On a sandbox created with `rootfs: desktop:1`, `sandbox_desktop` mints a live noVNC link the
diff --git a/packages/pi-extension/src/sandbox-engine.ts b/packages/pi-extension/src/sandbox-engine.ts
index b8dcd9d..6a6a81c 100644
--- a/packages/pi-extension/src/sandbox-engine.ts
+++ b/packages/pi-extension/src/sandbox-engine.ts
@@ -15,7 +15,7 @@
* file drops into any TypeScript plugin.
*/
-import { execSync } from "node:child_process";
+import { execSync, spawnSync } from "node:child_process";
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
@@ -191,9 +191,16 @@ export interface EgressOptions {
egressPresets?: string[];
/** Unrestricted egress — only for a trusted offload. */
egressAll?: boolean;
+ /** Deny ALL egress: any rule flips CreateOS to deny-by-default, and an IP rule is
+ * enforced at once (hostname rules are not), so one unroutable TEST-NET-1 address
+ * allows nothing. Mirrors `cos exec -N`. */
+ egressDenyAll?: boolean;
}
+export const DENY_ALL_EGRESS = "192.0.2.1/32";
+
export function egressArgs(opts: EgressOptions): { args: string[]; warning?: string } {
+ if (opts.egressDenyAll) return { args: ["--egress", DENY_ALL_EGRESS] };
if (opts.egressAll) return { args: [], warning: undefined };
const domains = [
...(opts.egress ?? []),
@@ -518,6 +525,111 @@ export function cleanupFailureNote(sandboxId: string, error?: string): string {
);
}
+// ---------------------------------------------------------------------------
+// Remote code execution — one source file in a throwaway box (`cos exec`)
+// ---------------------------------------------------------------------------
+
+/** Language → file name in /work and the command that runs it. `.js` stays CommonJS-capable. */
+export const RUN_CODE_LANGS: Record = {
+ py: { file: "main.py", run: "python3 main.py" },
+ js: { file: "main.js", run: "node main.js" },
+ mjs: { file: "main.mjs", run: "node main.mjs" },
+ cjs: { file: "main.cjs", run: "node main.cjs" },
+ ts: { file: "main.ts", run: "bun main.ts" },
+ go: { file: "main.go", run: "go run main.go" },
+ sh: { file: "main.sh", run: "bash main.sh" },
+ rb: { file: "main.rb", run: "ruby main.rb" },
+ c: { file: "main.c", run: "gcc -O2 -o main main.c && ./main" },
+ cpp: { file: "main.cpp", run: "g++ -O2 -o main main.cpp && ./main" },
+ rs: { file: "main.rs", run: "rustc -O -o main main.rs && ./main" },
+};
+
+export interface RunCodeOptions extends EgressOptions {
+ code: string;
+ lang: string;
+ /** Passed to the program untouched. */
+ args?: string[];
+ stdin?: string;
+ /** Wall-clock limit; the program is killed and exits 124 when hit. Default 120. */
+ timeoutSec?: number;
+ shape?: string;
+ rootfs?: string;
+}
+
+export interface RunCodeResult extends ExecResult {
+ timedOut: boolean;
+ durationMs: number;
+ warnings: string[];
+}
+
+/** The in-box command: timeout-wrapped, stdin from a pushed file or /dev/null. */
+export function runCodeCommand(
+ lang: string,
+ args: string[],
+ timeoutSec: number,
+ hasStdin: boolean,
+): string {
+ const spec = RUN_CODE_LANGS[lang];
+ if (!spec) {
+ throw new Error(
+ `Unsupported language '${lang}' — have: ${Object.keys(RUN_CODE_LANGS).join(", ")}`,
+ );
+ }
+ const run = [spec.run, ...args.map(shq)].join(" ");
+ return `cd /work && timeout -k 5 ${timeoutSec} bash -c ${shq(run)} <${hasStdin ? ".stdin" : "/dev/null"}`;
+}
+
+/**
+ * Run one piece of code off the user's machine: create → push → run → destroy.
+ * Buffered exec on purpose — a job that outlives one exec stream belongs in offload().
+ */
+export async function runCode(opts: RunCodeOptions): Promise {
+ assertAuth();
+ const timeoutSec = opts.timeoutSec ?? 120;
+ const cmd = runCodeCommand(opts.lang, opts.args ?? [], timeoutSec, opts.stdin !== undefined);
+ const { id, warning } = createBox({
+ ...opts,
+ name: `cos-x-${process.pid}-${Math.floor(Math.random() * 1e6)}`,
+ });
+ const warnings = warning ? [warning] : [];
+ try {
+ if (!(await waitRunning(id))) throw new Error(`Sandbox ${id} did not reach running within 30s`);
+ const push = (content: string, remote: string) => {
+ const res = spawnSync("createos", ["sandbox", "push", id, "-", remote], {
+ input: content,
+ encoding: "utf-8",
+ timeout: 120_000,
+ });
+ if (res.status !== 0)
+ throw new Error(
+ `Push of ${remote} failed: ${(res.stderr || res.stdout || String(res.error)).trim()}`,
+ );
+ };
+ push(opts.code, `/work/${RUN_CODE_LANGS[opts.lang].file}`);
+ if (opts.stdin !== undefined) push(opts.stdin, "/work/.stdin");
+ const t0 = Date.now();
+ // spawnSync, not execShell: execShell drops stderr when the exit code is 0,
+ // and a program's stderr is part of its answer.
+ const r = spawnSync("createos", ["sandbox", "exec", id, "--", "bash", "-lc", cmd], {
+ encoding: "utf-8",
+ timeout: (timeoutSec + 60) * 1000,
+ maxBuffer: 64 * 1024 * 1024,
+ });
+ const code = r.status ?? 1;
+ return {
+ code,
+ stdout: r.stdout ?? "",
+ stderr: r.stderr || (r.error ? String(r.error) : ""),
+ timedOut: code === 124,
+ durationMs: Date.now() - t0,
+ warnings,
+ };
+ } finally {
+ const d = destroyBox(id);
+ if (!d.ok) warnings.push(cleanupFailureNote(id, d.error));
+ }
+}
+
/**
* Stage → run (keepalive) → pull → destroy, in one call.
*
diff --git a/packages/pi-extension/src/tools.ts b/packages/pi-extension/src/tools.ts
index 8ace9c5..b0efb38 100644
--- a/packages/pi-extension/src/tools.ts
+++ b/packages/pi-extension/src/tools.ts
@@ -332,6 +332,77 @@ export function registerTools(pi: ExtensionAPI, getActive: () => ToolSandbox | n
},
});
+ // --- Remote code execution ---
+
+ pi.registerTool({
+ name: "sandbox_run_code",
+ label: "Run Code In Throwaway Sandbox",
+ description:
+ "Remote code execution: run one program's source in a throwaway sandbox and destroy it. Returns stdout, " +
+ "stderr and the program's exit code (124 = killed by the timeout). Egress is unrestricted unless restricted.",
+ promptSnippet: "Run untrusted code or an ad-hoc script off this machine",
+ promptGuidelines: [
+ "Use sandbox_run_code for untrusted code and for ANY ad-hoc script or snippet you would otherwise run locally; pass the full source as code. Several files or dependencies to install → sandbox_offload.",
+ "For questions about CreateOS Sandbox itself (REST API, SDKs, CLI, limits), fetch the matching page listed in https://createos.sh/docs/llms.txt under /Sandbox/ — every page is raw markdown at https://createos.sh/docs.md.",
+ ],
+ parameters: Type.Object({
+ code: Type.String({ description: "Full source of the program" }),
+ lang: Type.String({
+ description: `Language: ${Object.keys(engine.RUN_CODE_LANGS).join(" | ")}`,
+ }),
+ args: Type.Optional(
+ Type.Array(Type.String(), { description: "Program arguments, passed through untouched" }),
+ ),
+ stdin: Type.Optional(Type.String({ description: "Text fed to the program's stdin" })),
+ timeout_sec: Type.Optional(
+ Type.Integer({ minimum: 1, description: "Wall-clock limit in seconds (default: 120)" }),
+ ),
+ egress_deny_all: Type.Optional(
+ Type.Boolean({ description: "Block all outbound connections" }),
+ ),
+ egress_presets: Type.Optional(
+ Type.Array(Type.String(), {
+ description:
+ "Allow only what these ecosystems need: python-uv | rust-cargo | npm | github",
+ }),
+ ),
+ egress: Type.Optional(
+ Type.Array(Type.String(), {
+ description: "Allow only these hosts; composes with egress_presets",
+ }),
+ ),
+ shape: Type.Optional(Type.String({ description: "Sandbox size (default: s-1vcpu-1gb)" })),
+ }),
+ async execute(_id, params) {
+ const result = await engine.runCode({
+ code: params.code,
+ lang: params.lang,
+ args: params.args,
+ stdin: params.stdin,
+ timeoutSec: params.timeout_sec,
+ egressDenyAll: params.egress_deny_all,
+ egressPresets: params.egress_presets,
+ egress: params.egress,
+ shape: params.shape,
+ });
+ const header = `exit code ${result.code}${result.timedOut ? " (killed by timeout)" : ""} in ${(
+ result.durationMs / 1000
+ ).toFixed(1)}s`;
+ const warnings = result.warnings.map((warning) => `warning: ${warning}`);
+ const text = [
+ header,
+ ...warnings,
+ "",
+ "stdout:",
+ result.stdout || "(empty)",
+ "",
+ "stderr:",
+ result.stderr || "(empty)",
+ ].join("\n");
+ return { content: [{ type: "text", text }], details: { result } };
+ },
+ });
+
// --- Desktop / computer use ---
function desktopTarget(params: { sandbox_id?: string; screen?: string }): {
diff --git a/packages/shared/sandbox-engine.test.ts b/packages/shared/sandbox-engine.test.ts
index 435b742..b90d008 100644
--- a/packages/shared/sandbox-engine.test.ts
+++ b/packages/shared/sandbox-engine.test.ts
@@ -14,6 +14,7 @@ import {
cleanupFailureNote,
egressArgs,
retentionReasons,
+ runCodeCommand,
} from "./sandbox-engine.ts";
test("a preset expands to its domains as repeated --egress flags", () => {
@@ -171,3 +172,23 @@ test("an artifact path may not escape /work", () => {
expect(() => assertSafeOutPath("/etc/passwd")).toThrow(/inside \/work/);
expect(() => assertSafeOutPath("../../etc")).toThrow(/inside \/work/);
});
+
+test("egressDenyAll allows only an unroutable IP, so nothing is reachable", () => {
+ expect(egressArgs({ egressDenyAll: true, egressPresets: ["npm"] }).args).toEqual([
+ "--egress",
+ "192.0.2.1/32",
+ ]);
+});
+
+test("runCode passes program args through untouched and bounds the run", () => {
+ const cmd = runCodeCommand("py", ["--name=alice", "a b", "it's"], 30, false);
+ expect(cmd).toBe(
+ `cd /work && timeout -k 5 30 bash -c 'python3 main.py '\\''--name=alice'\\'' '\\''a b'\\'' '\\''it'\\''\\'\\'''\\''s'\\''' {
+ expect(() => runCodeCommand("cobol", [], 5, false)).toThrow(/Unsupported language/);
+});
diff --git a/packages/shared/sandbox-engine.ts b/packages/shared/sandbox-engine.ts
index b8dcd9d..6a6a81c 100644
--- a/packages/shared/sandbox-engine.ts
+++ b/packages/shared/sandbox-engine.ts
@@ -15,7 +15,7 @@
* file drops into any TypeScript plugin.
*/
-import { execSync } from "node:child_process";
+import { execSync, spawnSync } from "node:child_process";
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
@@ -191,9 +191,16 @@ export interface EgressOptions {
egressPresets?: string[];
/** Unrestricted egress — only for a trusted offload. */
egressAll?: boolean;
+ /** Deny ALL egress: any rule flips CreateOS to deny-by-default, and an IP rule is
+ * enforced at once (hostname rules are not), so one unroutable TEST-NET-1 address
+ * allows nothing. Mirrors `cos exec -N`. */
+ egressDenyAll?: boolean;
}
+export const DENY_ALL_EGRESS = "192.0.2.1/32";
+
export function egressArgs(opts: EgressOptions): { args: string[]; warning?: string } {
+ if (opts.egressDenyAll) return { args: ["--egress", DENY_ALL_EGRESS] };
if (opts.egressAll) return { args: [], warning: undefined };
const domains = [
...(opts.egress ?? []),
@@ -518,6 +525,111 @@ export function cleanupFailureNote(sandboxId: string, error?: string): string {
);
}
+// ---------------------------------------------------------------------------
+// Remote code execution — one source file in a throwaway box (`cos exec`)
+// ---------------------------------------------------------------------------
+
+/** Language → file name in /work and the command that runs it. `.js` stays CommonJS-capable. */
+export const RUN_CODE_LANGS: Record = {
+ py: { file: "main.py", run: "python3 main.py" },
+ js: { file: "main.js", run: "node main.js" },
+ mjs: { file: "main.mjs", run: "node main.mjs" },
+ cjs: { file: "main.cjs", run: "node main.cjs" },
+ ts: { file: "main.ts", run: "bun main.ts" },
+ go: { file: "main.go", run: "go run main.go" },
+ sh: { file: "main.sh", run: "bash main.sh" },
+ rb: { file: "main.rb", run: "ruby main.rb" },
+ c: { file: "main.c", run: "gcc -O2 -o main main.c && ./main" },
+ cpp: { file: "main.cpp", run: "g++ -O2 -o main main.cpp && ./main" },
+ rs: { file: "main.rs", run: "rustc -O -o main main.rs && ./main" },
+};
+
+export interface RunCodeOptions extends EgressOptions {
+ code: string;
+ lang: string;
+ /** Passed to the program untouched. */
+ args?: string[];
+ stdin?: string;
+ /** Wall-clock limit; the program is killed and exits 124 when hit. Default 120. */
+ timeoutSec?: number;
+ shape?: string;
+ rootfs?: string;
+}
+
+export interface RunCodeResult extends ExecResult {
+ timedOut: boolean;
+ durationMs: number;
+ warnings: string[];
+}
+
+/** The in-box command: timeout-wrapped, stdin from a pushed file or /dev/null. */
+export function runCodeCommand(
+ lang: string,
+ args: string[],
+ timeoutSec: number,
+ hasStdin: boolean,
+): string {
+ const spec = RUN_CODE_LANGS[lang];
+ if (!spec) {
+ throw new Error(
+ `Unsupported language '${lang}' — have: ${Object.keys(RUN_CODE_LANGS).join(", ")}`,
+ );
+ }
+ const run = [spec.run, ...args.map(shq)].join(" ");
+ return `cd /work && timeout -k 5 ${timeoutSec} bash -c ${shq(run)} <${hasStdin ? ".stdin" : "/dev/null"}`;
+}
+
+/**
+ * Run one piece of code off the user's machine: create → push → run → destroy.
+ * Buffered exec on purpose — a job that outlives one exec stream belongs in offload().
+ */
+export async function runCode(opts: RunCodeOptions): Promise {
+ assertAuth();
+ const timeoutSec = opts.timeoutSec ?? 120;
+ const cmd = runCodeCommand(opts.lang, opts.args ?? [], timeoutSec, opts.stdin !== undefined);
+ const { id, warning } = createBox({
+ ...opts,
+ name: `cos-x-${process.pid}-${Math.floor(Math.random() * 1e6)}`,
+ });
+ const warnings = warning ? [warning] : [];
+ try {
+ if (!(await waitRunning(id))) throw new Error(`Sandbox ${id} did not reach running within 30s`);
+ const push = (content: string, remote: string) => {
+ const res = spawnSync("createos", ["sandbox", "push", id, "-", remote], {
+ input: content,
+ encoding: "utf-8",
+ timeout: 120_000,
+ });
+ if (res.status !== 0)
+ throw new Error(
+ `Push of ${remote} failed: ${(res.stderr || res.stdout || String(res.error)).trim()}`,
+ );
+ };
+ push(opts.code, `/work/${RUN_CODE_LANGS[opts.lang].file}`);
+ if (opts.stdin !== undefined) push(opts.stdin, "/work/.stdin");
+ const t0 = Date.now();
+ // spawnSync, not execShell: execShell drops stderr when the exit code is 0,
+ // and a program's stderr is part of its answer.
+ const r = spawnSync("createos", ["sandbox", "exec", id, "--", "bash", "-lc", cmd], {
+ encoding: "utf-8",
+ timeout: (timeoutSec + 60) * 1000,
+ maxBuffer: 64 * 1024 * 1024,
+ });
+ const code = r.status ?? 1;
+ return {
+ code,
+ stdout: r.stdout ?? "",
+ stderr: r.stderr || (r.error ? String(r.error) : ""),
+ timedOut: code === 124,
+ durationMs: Date.now() - t0,
+ warnings,
+ };
+ } finally {
+ const d = destroyBox(id);
+ if (!d.ok) warnings.push(cleanupFailureNote(id, d.error));
+ }
+}
+
/**
* Stage → run (keepalive) → pull → destroy, in one call.
*
diff --git a/scripts/sync-shared.sh b/scripts/sync-shared.sh
index c4bf305..df57a2a 100755
--- a/scripts/sync-shared.sh
+++ b/scripts/sync-shared.sh
@@ -21,6 +21,7 @@ PAIRS=(
"packages/claude-code-plugin/scripts/cos:packages/codex-plugin/scripts/cos"
"packages/claude-code-plugin/scripts/offload-hint.sh:packages/codex-plugin/scripts/offload-hint.sh"
"packages/claude-code-plugin/skills/using-createos-sandbox/SKILL.md:packages/codex-plugin/skills/using-createos-sandbox/SKILL.md"
+ "packages/claude-code-plugin/skills/using-createos-sandbox/references/docs.md:packages/codex-plugin/skills/using-createos-sandbox/references/docs.md"
"packages/claude-code-plugin/skills/using-createos-sandbox/references/coding-agents.md:packages/codex-plugin/skills/using-createos-sandbox/references/coding-agents.md"
"packages/claude-code-plugin/skills/using-createos-sandbox/references/lifecycle-and-images.md:packages/codex-plugin/skills/using-createos-sandbox/references/lifecycle-and-images.md"
"packages/claude-code-plugin/skills/using-createos-sandbox/references/networking.md:packages/codex-plugin/skills/using-createos-sandbox/references/networking.md"