> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cycls.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Sandbox and trust model

> What isolates the bash tool from your secrets, what that isolation covers, what it does not, and the settings that close the remaining gaps.

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

| Party                      | Trust     | Reasoning                                                          |
| -------------------------- | --------- | ------------------------------------------------------------------ |
| Agent developer            | trusted   | you configured the image, wrote the handlers and own the workspace |
| End user and their prompts | untrusted | prompt injection is the primary threat                             |

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

## The layers

```
host  →  Docker container (agent runtime)  →  bubblewrap sandbox (bash tool)
```

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.

| Attempt                                                   | Result                                   |
| --------------------------------------------------------- | ---------------------------------------- |
| Read the parent process environment through `/proc`       | denied                                   |
| Grep every `/proc/*/environ` for a planted key            | no match outside the shell's own process |
| Reach another tenant's files through `/proc/<pid>/root`   | denied                                   |
| List `/workspace`                                         | only the current tenant's directory      |
| Read `/app/.env`                                          | empty, masked by tmpfs                   |
| Resolve or fetch the metadata server with network enabled | connection refused                       |
| Any network call with `network=False`                     | fails, no egress                         |

## Network

```python theme={null}
llm = cycls.LLM().sandbox(network=True)    # default
llm = cycls.LLM().sandbox(network=False)   # no egress from bash
```

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:

| Risk        | Behavior                           |
| ----------- | ---------------------------------- |
| read        | always runs                        |
| write       | follows the composer's Auto switch |
| destructive | asks in both modes                 |

`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](/agents/connectors#approvals) for per-tool policy and
organization level switches.

## Recommended settings for untrusted users

```python theme={null}
llm = (
    cycls.LLM()
    .model("anthropic/claude-sonnet-4-6")
    .allowed_tools(["Bash", "Editor", "WebSearch", "Canvas"])
    .sandbox(network=False)
    .bash_timeout(120)
)
```

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](/agents/connectors) or a deployed function.
4. Use a separate organization for tenants that must not share a trust domain.

## Next

<Card title="Custom loops" icon="code-branch" href="/agents/custom-loop">
  Replace the default loop and keep the building blocks.
</Card>
