Skip to main content
This page is for developers deploying an agent that runs shell commands on behalf of untrusted users. It states the trust boundaries, what is verified, and what is explicitly not covered.

Trust inputs

Every control below is designed against prompt injection, not against a malicious developer.

The layers

The agent’s Python process holds secrets as environment variables: provider keys, your own .env contents, and any credential your handlers use. The purpose of the bubblewrap layer is to keep a prompt-injected shell command away from all of them. Inside the sandbox:
  • The root filesystem is read-only. The user’s workspace is the only writable path, bound at /workspace.
  • /app and /tmp are ephemeral tmpfs, which masks the provider dotenv file baked into the image.
  • .db/ is masked, so the shell cannot read the chat store. The read and edit tools reject those paths as well.
  • The environment is cleared and repopulated with PATH, HOME, TERM and LANG only.
  • A user namespace is unshared, which is what blocks reading another process’s /proc/<pid>/environ, memory or root.
  • The cloud metadata address range is blocked by an LD_PRELOAD shim that intercepts connect() for 169.254.0.0/16 and IPv6 link-local ranges.

Verified behavior

These are the checks the project runs against a live sandbox.

Network

Network is on by default because most agents need curl, pip and git. A prompt-injected command can then send anything it can read to an arbitrary host. Turn it off when the agent does not need it. With network=False the sandbox gets a fresh network namespace and no egress at all. Web search still works, because it runs outside the sandbox.

What the metadata block does not cover

The LD_PRELOAD shim stops every program that uses the C library, which covers what a model writes in practice. It does not stop:
  • statically linked binaries, since they never load the shim
  • direct syscall invocation, for example through ctypes
  • inline assembly
  • a child process launched after unset LD_PRELOAD
All four require deliberate intent, not an accidental injection. If your threat model includes users who read the SDK source and craft bypasses, the architectural answer is a separate deployment per tenant, where a stolen credential reaches only that tenant’s own data.

Deployment isolation

Deployments under one account share a trust domain: code in one can reach another’s workspace storage. For hard isolation between production and experiments, or between clients, deploy from a separate organization. Each organization is its own tenant with its own boundary.

Local development caveat

The sandbox binds the host root read-only. In production that is the runtime container’s filesystem, which holds no host secrets. When you run an agent directly on your machine without Docker, local files such as personal credential stores become readable from the sandbox. Use cycls run, which wraps the agent in Docker, when exercising an agent that accepts untrusted input with network-enabled bash. Or keep network=False, which removes the exfiltration path.

Approvals

Isolation bounds what a command can reach. Approvals decide whether it runs at all. Every tool call is classified before it runs: rm and rmdir are not destructive by default, because the sandbox moves deletions to a thirty day trash. Commands with nothing behind them, such as shred, mkfs, dd if=, drop table and git push --force, always ask. See Connectors and MCP for per-tool policy and organization level switches.
  1. Turn off sandbox network access unless the agent needs it.
  2. Keep deploy credentials out of the image by splitting .env and .providers.env.
  3. Put anything that must not be readable by a shell outside the workspace, for example behind a connector or a deployed function.
  4. Use a separate organization for tenants that must not share a trust domain.

Next

Custom loops

Replace the default loop and keep the building blocks.