Skip to content

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.

TierRuns whereTrust
Extension host codeNode.js, full APIFull — shipped by us
User/team scriptsQuickJS WASM sandboxPartial — behavior only; no I/O except via curated host bindings
LLM outputNever executed directlyUntrusted — 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.

ConventionPurposeBindings available
skill.js next to skill.mdSkill scripts — extend a skill with programmatic logicNone (string I/O)
.codebuddy/transforms/<tool>.jsRewrite a tool’s raw output before the LLM sees itNone (string I/O)
.codebuddyignore.jsPredicate fallback / complement to the glob-based ignore fileRead-only pack
{{js: EXPRESSION}} in AGENTS.md / rules.mdDynamic prompt fragments (e.g. {{js: host.gitBranch()}})Read-only pack
.codebuddy/commands/*.jsCustom slash commands (/my-command)Read-only pack

All five ship today.

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):

BindingReturns
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.

  • 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, no exec, 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.
  • 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.

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.

  1. Every binding has a Zod-validated signature — no argument confusion via prototype pollution or type coercion.
  2. Every path argument goes through validatePathWithinWorkspace() — symlink-resolved, workspace-scoped.
  3. Adding a new binding requires a security-review checkbox on the PR — the audit surface only grows deliberately.
  4. WASM isolation — even a JIT bug inside QuickJS cannot reach the extension-host process directly. Memory and control flow are contained.
  5. 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.
  • Security model — how the sandbox fits the broader threat model
  • Project rules — where {{js: …}} fragments live
  • Skills — how skill.js extends a skill
  • MCP — the other integration path: external process vs sandboxed script