Egress and sandbox
A coding agent runs shell commands and fetches URLs with the developer’s credentials. Halos lets you state two limits once, in the profile: what the agent’s shell may write and read (the sandbox) and which hosts it may reach (egress). Each adapter renders them into the harness’s own managed settings. What a harness cannot hold becomes a release warning, and halod and halo doctor check the device (ADR-0032).
Write the policy
Section titled “Write the policy”permissions: sandbox: workspace-write # or sandbox.mode; off | workspace-write | read-onlysandbox: writableRoots: [/opt/build-cache] # absolute or ~/, beyond the workspace protectedPaths: ["~/.ssh", "~/.aws", ".env*"]egress: allowedDomains: [ai.acme.example, otel.internal.acme.example] presets: [vcs, package-registries] deniedDomains: [pastebin.com, transfer.sh] # default: deny # implied: something is allowedHow a host is decided, everywhere (validation, halo egress test, every adapter):
- A
deniedDomainsmatch denies. Deny always wins. - An
allowedDomainsor preset match allows. - Otherwise
defaultdecides. Unset meansdenywhen anything is allowed, elseallow.
github.com matches only that host. *.github.com matches every subdomain, never github.com itself. Domains are lower case, with no scheme, port, path or bare * (use default: allow for that).
Presets
Section titled “Presets”| Preset | Hosts | Sources |
|---|---|---|
package-registries |
registry.npmjs.org, pypi.org, files.pythonhosted.org, proxy.golang.org, sum.golang.org, index.crates.io, static.crates.io, repo.maven.apache.org, rubygems.org |
Copilot allowlist reference, Go modules reference, Cargo registry index, PyPI integration guide |
vcs |
github.com, api.github.com, codeload.github.com, *.githubusercontent.com, gitlab.com, bitbucket.org |
Copilot allowlist reference, Cursor run modes |
model-providers |
api.anthropic.com, api.openai.com, generativelanguage.googleapis.com, api.githubcopilot.com, *.githubcopilot.com |
Claude Code network config, Codex managed configuration, Gemini API, Copilot managed settings |
Behind a Halos gateway you rarely need model-providers: the harnesses call the gateway host, which a restricted policy must allow anyway.
What validation refuses
Section titled “What validation refuses”These amount to a bypass (invariant 1) and are errors:
- a writable root of
/,~, a glob, a relative path, a home directory (/Users/alice,/home/bob,/root), a system directory or an ancestor of one (/etc,/usr,/var,/Users,/home,/System,/Library), anything under/etcor/usr, or one over or under a protected path (case-insensitive, and/Users/alice/.sshcounts as~/.ssh); any writable root with moderead-only; - sandbox mode
offtogether with egress rules, protected paths or writable roots; default: allowtogether withallowedDomainsorpresets;- an egress policy that denies the gateway host;
sandbox.modeandpermissions.sandboxset to different values.
Rings and units
Section titled “Rings and units”A ring uses its profile’s policy. For a ring that needs more (say ring0 may reach a staging registry), point it at a profile that extends the baseline and adds the host; halo validate warns that the child widens its parent. A unit overlay (ADR-0018) may add deniedDomains and protectedPaths and narrow the allowlist. Switching the default to allow, dropping a denied domain or protected path, or adding a writable root is a loosening and fails validation.
What each harness enforces
Section titled “What each harness enforces”Rendered from the policy above; halo release build lists the rest as warnings, and halo harnesses shows the sandbox and egress capabilities.
| Harness | Sandbox | Shell egress | Fetch tool | Protected paths | Notes |
|---|---|---|---|---|---|
| Claude Code | sandbox.enabled, no unsandboxed retry, filesystem.allowWrite |
sandbox.network.allowedDomains + allowManagedDomainsOnly, deniedDomains |
WebFetch(domain:...) deny rules; unlisted hosts still prompt |
filesystem.denyRead/denyWrite (absolute, ~/) and Read/Edit deny rules |
read-only renders as workspace-write. No sandbox on native Windows. |
| Codex | requirements.toml allowed_sandbox_modes; writable_roots (default) |
requirements.toml [experimental_network] (documented experimental) |
not covered | [permissions.filesystem] deny_read (reads only) |
network_access stays false under default deny; a user who turns it on is held to the managed domains. |
| Copilot CLI | managed sandbox with allowBypass: false, readwritePaths |
userPolicy.network.allowedHosts/blockedHosts, allowOutbound: false |
not documented | userPolicy.filesystem.deniedPaths (absolute) |
Needs CLI >= 1.0.93; an older pin renders nothing (it would lock users out). Windows relies on programs honoring the proxy. |
| Gemini CLI | tools.sandbox: true on macOS (Seatbelt) |
none | none | none | A default: --sandbox and GEMINI_SANDBOX override it. Linux and Windows need Docker or Podman; not rendered there. |
| OpenCode | none | none | permission.webfetch: deny when nothing is allowed, else ask |
read/edit/external_directory deny rules |
No sandbox; bash is unconfined. OPENCODE_PERMISSION overrides the managed file. |
| Cursor CLI | not rendered | not rendered | not rendered | not rendered | sandbox.json and cli-config.json are per user or per repo only; set the team sandbox policy in Cursor’s admin dashboard. |
Sources and the claims not yet observed with the real CLIs are in the ADR and in internal/harness/FACTS.md.
Ask about a host
Section titled “Ask about a host”$ halo egress test https://pastebin.com/raw/x --ring ring3-ga # claude-code and codex caveat lines trimmedDENY pastebin.com (ring ring3-ga, profile engineering)rule egress.deniedDomains pastebin.com claude-code enforced: sandbox.network allowedDomains/deniedDomains for Bash, WebFetch(domain:...) deny rules for the fetch tool codex enforced: requirements.toml [experimental_network] for sandboxed commands gemini-cli not enforced - gemini-cli: profile field "egress.deniedDomains" is not supported and was not rendered--unit applies a unit’s overlays; --os picks the platform the per-harness rendering assumes; --output json returns the decision, rule, resolved rules and per-harness detail. It exits 1 on deny, so a CI step can assert a host stays blocked.
Verify on the device
Section titled “Verify on the device”halod already restores every managed file it owns. What can still weaken the posture is the user’s own configuration, which halod reports and never rewrites. When the managed file turns the sandbox or egress on, these become drift in the device report, each as posture:<harness>: <problem>; fix: <fix>:
| Harness | Per-user setting reported |
|---|---|
| Codex | ~/.codex/config.toml sandbox_mode = "danger-full-access", top level or in any [profiles.<name>] |
| Gemini CLI | ~/.gemini/settings.json tools.sandbox = false |
| Cursor CLI | ~/.cursor/sandbox.json type = "insecure_none" (always checked) |
Because it is drift, a ring with posture: enforce (ADR-0011) refuses such a device at the gateway until it is fixed. halod reads these files as root, so it reads only regular files up to 1 MiB, opens them without blocking, and never logs their content. A project’s own .codex/config.toml and command-line flags (-s, -c, --profile) are beyond halod’s reach: there only requirements.toml allowed_sandbox_modes (codex >= 0.77.0) holds.
halo doctor prints the effective posture of every installed harness, plus environment overrides it can see (GEMINI_SANDBOX=false, OPENCODE_PERMISSION, CODEX_HOME):
$ halo doctorok posture/claude-code sandbox: on (managed); egress: default deny, 19 allowed, 2 denied (managed)ok posture/codex sandbox: modes [read-only workspace-write] (managed); egress: default deny, 19 allowed, 2 denied (managed)warn posture/codex ~/.codex/config.toml sets sandbox_mode = "danger-full-access" fix: remove sandbox_mode from ~/.codex/config.toml; requirements.toml refuses it on codex >= 0.77.0, older versions run unsandboxed