argv-only spawns
Everysbx invocation uses spawn(bin, args, { shell: false }). No command is
ever passed through a host shell:
- Sandbox names are validated by regex.
- Project paths and
!cmdare passed as single argv elements. - Unit tests include adversarial names and paths.
src/sbx.ts spawns processes; a test guards that against regressions.
Per-project microVM
Each project runs in its ownsbx sandbox with only its project directory
mounted. The host filesystem outside that directory is unreachable by the agent.
Path containment and denylist
- A project’s directory must resolve (realpath, case-insensitive) inside
PROJECTS_ROOT. Ancestors and descendants of denied paths are rejected too. - The denylist covers the bot repo,
DATA_DIR, sensitive profile subtrees (.ssh,.aws,.gnupg,.config,.docker,.kube,.azure,.npmrc,.netrc,.celly,AppData), and system directories. - Text attachments are sanitized (basename only, no
.., no reserved Windows device names, no control characters) and written under.celly/inboxwith a containment check after resolution. - A single helper owns path logic; call sites do not hand-roll it.
Loopback and password
The generated sandbox config enables password auth (OPENCODE_SERVER_PASSWORD), and the server is published only on loopback. The
per-project password is written to ~/.config/celly/opencode.env (mode 0600)
inside the sandbox and stored in the bot database. It rotates when a sandbox is
recreated.
Bot-enforced permission policy
The sandbox config sets a default-deny policy, and the bot re-asserts it at the API layer after every wake and health check so a project-levelopencode.json
cannot weaken it:
- The
bashdeny list also blocks common inspection utilities (awk,base64,cat,cp,grep,head,less,od,sed,strings,tail,xxd) from readingopencode.envor~/.config/celly/, plus catch-all patterns for both paths. external_directory: denykeeps tools inside the mounted project.question: denyprevents headless deadlocks; surfacing questions in Discord is deferred.- Any permission request that still surfaces is answered from the policy — there is no catch-all auto-allow.
Secrets
DISCORD_TOKENlives only in.env(gitignored).- Provider credentials live in
sbx secretand are injected by the proxy, never stored in the bot or the repository. - The per-project server password is never placed on a host command line, is
never logged, and is masked in
/project status. - Log redaction covers tokens, passwords, and Authorization headers.
Honest caveat
The deny list is defense-in-depth, not a hard isolation boundary. An agent allowed to run bash can still run arbitrary allowed commands. The blast radius is contained by the microVM:OPENCODE_SERVER_PASSWORDonly guards a loopback-published port reachable from inside the sandbox and from the host’s127.0.0.1.- Provider credentials are injected by the
sbxproxy rather than stored in the sandbox. - A mounted project is untrusted; never keep secrets in it.
Related
- Architecture — where these controls live.
- Configuration — the path denylist and
DATA_DIR.