Sandboxed scripts
CodeBuddy runs user-authored scripts inside a QuickJS WASM sandbox — a third trust tier that lets you customize behavior without exposing Node capabilities to arbitrary code.
Trust model
Section titled “Trust model”| Tier | Runs where | Trust |
|---|---|---|
| Extension host code | Node.js, full API | Full — shipped by us |
| User/team scripts | QuickJS WASM sandbox | Partial — behavior only; no I/O except via curated host bindings |
| LLM output | Never executed directly | Untrusted — sanitized and wrapped |
Every user script runs in the middle tier: it can compute, transform, and read (via bindings), but cannot write files, spawn shells, or reach the network on its own.
What runs in the sandbox
Section titled “What runs in the sandbox”| Convention | Purpose | Bindings available |
|---|---|---|
skill.js next to skill.md | Skill scripts — extend a skill with programmatic logic | None (string I/O) |
.codebuddy/transforms/<tool>.js | Rewrite a tool’s raw output before the LLM sees it | None (string I/O) |
.codebuddyignore.js | Predicate fallback / complement to the glob-based ignore file | Read-only pack |
{{js: EXPRESSION}} in AGENTS.md / rules.md | Dynamic prompt fragments (e.g. {{js: host.gitBranch()}}) | Read-only pack |
.codebuddy/commands/*.js | Custom slash commands (/my-command) | Read-only pack |
All five ship today.
Host bindings — what a script can call
Section titled “Host bindings — what a script can call”Bindings are the only way a sandboxed script reaches CodeBuddy or the OS. Each is Zod-validated at the argument boundary and permission-tagged for audit.
Read-only pack (available in every non-string-only context):
| Binding | Returns |
|---|---|
host.readFile(path) | File contents (path validated against workspace root) |
host.grep(pattern, opts) | Ripgrep matches over the workspace |
host.glob(pattern) | Path list matching the glob |
host.gitBranch() | Current git branch |
host.workspaceInfo() | Workspace root, project type, framework detection |
Every path argument runs through WorkspaceIdentityService.validatePathWithinWorkspace() before hitting the filesystem — no escape to /etc/ or ~/.ssh/, TOCTOU-resistant.
What the sandbox will not do
Section titled “What the sandbox will not do”- File writes — every write flows through the normal tool path with diff-review approval. There is no
host.writeFile. - Network access — no
host.fetch, no XHR, no sockets. If you need HTTP, use an MCP server. - Arbitrary shell — no
spawn, noexec, no shell strings. Terminal work goes through the modal-approval tool path. - Long-running scripts — a per-execution budget (memory, wall-clock, instruction count) is enforced at the WASM level. Scripts that exceed it are terminated cleanly.
- Cross-eval persistence — fresh VM per invocation (call mode). State does not leak between calls.
Execution model
Section titled “Execution model”- Call mode (all shipped consumers today) — each invocation gets a fresh VM. Predictable, isolated, cheap.
- Interrupt handler — fires every ~10k instructions. Runaway scripts terminate; the calling code falls back to its non-sandboxed path.
- Fail-open on script errors — a broken user script never blocks the agent. Timeouts, memory errors, and guest exceptions surface as a warning in
Output > CodeBuddy; the caller uses its default behavior. The sandbox is added value, not a gate. - VM pool — up to 4 warm contexts. Cold start is one-time per session.
Writing a script — minimal example
Section titled “Writing a script — minimal example”Create .codebuddy/transforms/read_file.js in your workspace to filter every read_file tool result before the LLM sees it:
// input: the raw tool output as a string// return: the transformed string (return input unchanged to no-op)export default function (input) { // Strip lines starting with "DEBUG:" from any file read return input .split('\n') .filter((line) => !line.startsWith('DEBUG:')) .join('\n');}Save the file. Next time the tool runs, the transform picks up. No restart. No configuration.
Same shape for skill scripts (skill.js beside skill.md), slash commands (.codebuddy/commands/deploy.js), and predicates (.codebuddyignore.js) — each has its own contract; see the linked pages.
Security guarantees
Section titled “Security guarantees”- Every binding has a Zod-validated signature — no argument confusion via prototype pollution or type coercion.
- Every path argument goes through
validatePathWithinWorkspace()— symlink-resolved, workspace-scoped. - Adding a new binding requires a security-review checkbox on the PR — the audit surface only grows deliberately.
- WASM isolation — even a JIT bug inside QuickJS cannot reach the extension-host process directly. Memory and control flow are contained.
- No host binding may call write-side tools (
Terminal.executeAnyCommand,EditFileTool.execute,DeleteFileTool.execute). Enforced at review time and by the binding framework’s own contract.
Related
Section titled “Related”- Security model — how the sandbox fits the broader threat model
- Project rules — where
{{js: …}}fragments live - Skills — how
skill.jsextends a skill - MCP — the other integration path: external process vs sandboxed script