# Sandboxing in Copilot CLI

Run Copilot CLI inside the ticket-service devcontainer with a token from an environment variable, and keep its experimental local sandbox on for the commands it runs.

Source: https://ai-sw-factory.mellicci.dev/fundamentals/sandboxing/copilot-cli

Local sandboxing and cloud sandboxes are in public preview, and the local sandbox is experimental in the CLI.

## At a glance

| | Copilot CLI |
|---|---|
| Term | Local sandbox (experimental); cloud sandbox |
| Configured in | `/sandbox` dialog, `sandbox.*` keys in `~/.copilot/settings.json` (`COPILOT_HOME` moves it), `--sandbox` / `--no-sandbox` |
| Loads / runs | Off by default. Once enabled, it applies to every later session, interactive and `-p` |
| Scope & precedence | Your settings plus automatic grants; the more specific path wins; managed enterprise settings only tighten |

## Build the scenario

The devcontainer is the boundary. The local sandbox stays on inside it and narrows what the agent's commands can do.

| Step | What you write or run | Where |
|---|---|---|
| 1 | Install the CLI (Node.js 22 or later) | `.devcontainer/devcontainer.json` |
| 2 | Pass a fine-grained PAT with **Copilot Requests** as `COPILOT_GITHUB_TOKEN` | Container environment |
| 3 | Start the egress firewall | `.devcontainer/init-firewall.sh` |
| 4 | Enable the sandbox and mirror the hosts | `~/.copilot/settings.json` |
| 5 | Check the result | `/sandbox status`, `/sandbox policy` |

```json
{
  "name": "ticket-service",
  "dockerComposeFile": "docker-compose.yml",
  "service": "app",
  "workspaceFolder": "/workspaces/ticket-service",
  "postCreateCommand": "npm install -g @github/copilot && pnpm install",
  "postStartCommand": "sudo .devcontainer/init-firewall.sh",
  "remoteEnv": {
    "COPILOT_GITHUB_TOKEN": "${localEnv:TICKET_AGENT_TOKEN}"
  }
}
```

The compose file mounts only the repository and starts Postgres; it mounts no home directory and no Docker socket. Start the session with experimental features on, then enable the sandbox once:

```shell
copilot --experimental
/sandbox enable
/sandbox status
/sandbox policy
```

The choice is saved as `sandbox.enabled`. This `settings.json` also narrows the sandbox's own network rules to the same hosts the firewall allows:

```json
{
  "sandbox": {
    "enabled": true,
    "allowBypass": false,
    "userPolicy": {
      "network": {
        "allowLocalNetwork": true,
        "allowedHosts": ["registry.npmjs.org", "github.com", "*.github.com"]
      },
      "deniedPaths": ["/workspaces/ticket-service/.env"]
    }
  }
}
```

## Specifics

### Built-in sandbox

| Topic | Documented |
|---|---|
| Enable | `/sandbox enable`; `copilot --sandbox -p "PROMPT"` for one session (experimental mode only) |
| Default | Off. Without it, shell commands run with your full user access |
| Backends | macOS Seatbelt (macOS 15+); Linux bubblewrap 0.5.0+; Windows BaseContainer (Insiders build) |
| Linux with outbound network | Also `slirp4netns`, `unshare` and `nsenter`, `iptables` and `ip6tables`, and `/dev/net/tun` |
| Isolation level | OS-level restriction through MXC; not a VM or container |
| Covers | Shell commands, `grep`/`glob`, local MCP and LSP servers (default on). Remote MCP is never sandboxed |
| Built-in file tools | Run in-process; they check the policy in software, best effort |
| Unsupported host | Sandbox turns off with a notice; enterprise `failIfUnavailable` blocks instead |

### Filesystem and network rules

| Rule | Default | Key or control |
|---|---|---|
| Working directory | Read/write, plus the repo's `.git`; rest of the repo read-only | **Include working directory** |
| Home, system, tool locations | Read-only; other paths blocked | **Allow dev tool access** |
| Extra paths, read-only or denied | None | **Filesystem** tab; `sandbox.userPolicy.deniedPaths`; absolute paths, no wildcards |
| Outbound internet | Allowed | **Allow outbound connections** |
| Local network | Allowed | `sandbox.userPolicy.network.allowLocalNetwork` |
| Host rules | Empty; a non-empty `allowedHosts` blocks everything else | `allowedHosts`, `blockedHosts` (block wins) |
| HTTP proxy | Unset | `sandbox.userPolicy.network.proxy` |

Host rules go through a local proxy that enforces them on every platform. The HTTP proxy setting is cooperative on macOS, and the concept and settings pages disagree on Linux and Windows. On Linux, local network access cannot be controlled independently for spawned processes.

### Running inside a container

| Topic | Documented |
|---|---|
| Install | `npm install -g @github/copilot` (Node.js 22 or later); also Homebrew, WinGet or an install script |
| Sign-in | Dev containers default to the device code flow; `COPILOT_GITHUB_TOKEN` avoids it |
| Container guide | The documentation doesn't specify a devcontainer setup or a reference image |
| Sandbox in a container | The documentation doesn't specify whether bubblewrap starts in an unprivileged container. Run `/sandbox status` in yours |

### Credentials and secrets

| Topic | Behavior |
|---|---|
| Token lookup | `COPILOT_GITHUB_TOKEN`, then `GH_TOKEN`, then `GITHUB_TOKEN`, then keychain, then `gh`; a variable silently overrides a stored login |
| PAT | Fine-grained, owned by a personal account, **Copilot Requests**; classic `ghp_` is not supported |
| Redaction | `--secret-env-vars=VAR` redacts variables from shell and MCP environments; the two Copilot token variables are redacted by default |
| Git and `gh` inside the sandbox | A GitHub token is injected for `git`, and `GH_TOKEN` is exported for `gh`; turn off with `sandbox.auth.git` and `sandbox.auth.gh` |
| Files | Deny `.env` with `deniedPaths`. Dev tool access also exposes package-manager config and tokens read-only |

The documentation doesn't specify whether the sandbox passes the parent environment to commands unchanged.

### Escape hatches and limits

| Mechanism | Effect |
|---|---|
| Permissions (`--allow-tool`, `--deny-tool`) | Separate layer: they decide what the model may call; the sandbox limits what a permitted command can reach |
| `--allow-all` / `--yolo` | Equals `--allow-all-tools --allow-all-paths --allow-all-urls`; the documentation doesn't say whether it affects the sandbox. Deny rules still win |
| `sandbox.allowBypass` | Default true: a blocked command prompts to re-run outside, or to disable the sandbox for the session. Detached commands cannot be sandboxed |
| `--no-sandbox` | One session; ignored when a managed policy requires sandboxing |
| Cloud sandbox | `copilot --cloud --experimental`: ephemeral GitHub-hosted Linux, interactive only (not with `-p` or `-i`), billed by usage; org policy off by default |
| Local vs cloud | Local is free, on your machine, commands only. Cloud runs the whole session remotely, can be stopped and resumed on another device |

## Gotchas

- The sandbox is off until you enable it, and the setting lives in the container user's `~/.copilot/settings.json`. Rebuilding the container resets it unless you bake the file in or set `COPILOT_HOME` to a persisted path.
- The firewall and the sandbox overlap but are separate. `allowedHosts` covers sandboxed commands only; the model API call and in-process tools are outside it, so keep the firewall.
- With `allowBypass` on, a prompt can move a command outside the sandbox. In an unattended run nobody answers; turn it off so blocked commands fail.
- The documentation doesn't specify the model API host, so work out the firewall entry from your own traffic. Whether the sandbox runs inside an unprivileged container is also unspecified.
- Read access spans the whole repository, so anything sensitive in it needs a `deniedPaths` entry.
