Running ox in constrained environments
"Ephemeral" is not a mode. It's a label for a constellation of capabilities the sandbox provides or withholds. Claude Code Cloud, Devin, GitHub Actions, and Codespaces are not all the same shape — one has persistent disk and an 8-hour lifetime, one writes disk to a snapshot but can run a daemon, one is gone in five minutes. ox probes each capability once at startup and lets every subsystem ask for what it actually needs.
This page covers the capability model, what works and what doesn't, how to set ox up on each major platform, and the exact error strings runbooks should grep for.
The capability model
When ox starts, it probes the environment for four capabilities and a lifetime hint:
| Capability | Question | Set by |
|---|---|---|
PersistDisk | Will writes to ~/.sageox/ survive the next ox invocation? | filesystem probe + OX_PERSIST_DISK override |
LongLivedHelper | Can a background helper outlive a single CLI invocation? | lifetime hint + OX_NO_DAEMON override |
Browser | Can ox open an interactive URL for device-code auth? | TTY + DISPLAY/BROWSER + known-headless detection |
Network | Is egress open, allowlisted, proxied, or offline? | platform-derived; OX_NETWORK override |
Subsystems consult capabilities directly. The local CodeDB indexer only runs when PersistDisk && LongLivedHelper. ox login's browser flow only attempts to launch when Browser is true; otherwise it refuses with a clear "set SAGEOX_TOKEN" message. Session staging picks ~/.sageox/sessions/... when PersistDisk is true, otherwise $TMPDIR/ox-sessions/....
Capability matrix per platform
| Platform | PersistDisk | LongLivedHelper | Browser | Network | Lifetime |
|---|---|---|---|---|---|
| Local laptop | yes | yes | yes | open | persistent |
| GitHub Codespaces | yes | yes | no | open | hours |
| Claude Code Cloud | no | yes | no | allowlist | hours |
| Devin | no | yes | no | open | hours |
| GitHub Actions runner | no | no | no | open | minutes |
Codespaces is no longer treated as ephemeral. A Codespace has persistent disk and a multi-hour lifetime — it behaves like a slow laptop. The previous CODESPACES=true auto-detection forced the constrained path and threw away the persistent disk Codespaces paid for. First-run does the setup, second-run reuses it.
--ephemeral is deprecated and will be removed. It only sets the env var inside one ox process — POSIX prevents writing back to the calling shell, so subsequent ox commands lose the signal and silently degrade. The canonical pattern is to export OX_EPHEMERAL=1 into the session env (e.g. write it to $CLAUDE_ENV_FILE before running ox), which is what every recipe on this page already does. The flag is redundant at best and a footgun at worst. The CLI accepts it for one release with a stderr deprecation warning, then removes it.
Override knobs
You can override the probe at the capability level:
| Variable | Effect |
|---|---|
OX_PERSIST_DISK=0 | Force the in-memory / $TMPDIR paths even when ~/.sageox/ is writable |
OX_NO_DAEMON=1 | Disable any background helper; run everything inline in the CLI process |
OX_NETWORK=allowlist | Tell ox it is running behind an egress allowlist (skip probes that would spuriously fail) |
OX_EPHEMERAL=1 | Coarse shortcut: sets PersistDisk=false, LongLivedHelper=false |
Use the capability-level overrides when the auto-probe gets it wrong (e.g. a brand-new sandbox ox doesn't recognize). Use OX_EPHEMERAL=1 when you just want "treat me as fully constrained" without spelling out each axis.
Prerequisites
- A Personal Access Token exported as
SAGEOX_TOKEN. - A static
oxbinary onPATH. The install script athttps://install.sageox.ai/oxships a single self-contained Go binary with no runtime dependencies.
What works, what doesn't
Subcommand behavior in constrained environments. "Designed-for" subcommands are the supported entrypoints; "degraded" still works but with a noted trade-off; "refused" exits non-zero with a clear pointer.
| Subcommand | PersistDisk=false | LongLivedHelper=false | Browser=false |
|---|---|---|---|
ox agent prime | designed-for | designed-for | designed-for |
ox distill --since=<window> | designed-for | designed-for | designed-for |
ox status | designed-for | designed-for | designed-for |
ox doctor | designed-for | designed-for | designed-for |
ox query "..." | works (refetches each call) | works | designed-for |
ox code search | degraded (remote-only; slower) | degraded (remote-only) | designed-for |
ox import <video> | works (stages to $TMPDIR, syncs at exit) | works (sync upload at exit) | designed-for |
ox kb writes | degraded (warns; writes lost at exit) | works | designed-for |
ox login | works | works | refused — exit non-zero, pointer to PATs |
ox daemon | refused | refused | works |
Session uploads are idempotent on (agent_id, session_id). Retrying a failed upload is safe — the second call is a no-op if the first actually succeeded.
Error contract
These strings are part of the CLI's contract surface. Operator runbooks grep for them; they will not be reworded without a deprecation pass.
| String | Cause | Fix |
|---|---|---|
SAGEOX_TOKEN expired or invalid | PAT revoked, expired, or wrong environment | Rotate at Settings → Tokens; update secret store |
SAGEOX_TOKEN not set; ox login is unavailable in this environment | No PAT and no browser capability | Export SAGEOX_TOKEN (or run on a host with a browser) |
failed to clone team context: dial tcp: i/o timeout | Network allowlist gap | Add team-context git host to platform's allowed hosts |
ox: 'daemon' requires a long-lived helper and is unavailable here | ox daemon invoked in a sandbox without LongLivedHelper | Use the inline command path — daemon is not required |
ox: 'login' requires a browser and is unavailable here | ox login invoked without Browser capability | Authenticate with SAGEOX_TOKEN |
OX_EPHEMERAL=1 set but you appear to be on a persistent machine | Stray export from a previous CI experiment | Unset OX_EPHEMERAL in your shell |
Exit codes follow sysexits.h: usage errors are 64, auth failures are 77.
Claude Code Cloud
Anthropic's Managed Agents run Claude Code in an isolated sandbox defined by an environment YAML.
Environment file
# .claude/environment.yml
packages:
go: ["github.com/sageox/ox/cmd/ox@latest"]
secrets:
SAGEOX_TOKEN: oxp_... # paste your PAT here, or reference a secret
hooks:
SessionStart: |
ox agent primepackages.goinstallsoxfrom source into the sandbox image. Pin to a specific version (@v0.42.0) for reproducibility once you have one you like.secrets.SAGEOX_TOKENis injected at runtime — it does not appear in the image layer. Prefer Anthropic's Secrets dashboard over inlining the value.hooks.SessionStartruns at the start of every Claude session.oxauto-detects the constrained environment fromCLAUDE_CODE_REMOTEand configures itself accordingly — noOX_EPHEMERALexport needed.
Networking
Claude Code Cloud defaults to limited egress. Add the SageOx hosts to your allowed_hosts:
network:
mode: limited
allowed_hosts:
- api.sageox.ai
- install.sageox.ai
- <your-team-context-git-host> # gitlab.com, github.com, etc.If you forget the team-context git host, ox agent prime will fail with a clone error pointing at the missing domain. Add it and retry.
Verify
Start a Claude session and look for these lines in the SessionStart log:
ox agent prime: constrained environment (CLAUDE_CODE_REMOTE)
ox agent prime: authenticated as <your-email>
ox agent prime: team context loaded (<N> files)If you see SAGEOX_TOKEN expired or invalid, rotate the PAT — see Personal Access Tokens.
Devin
Cognition's Devin repo setup splits configuration into three sections: setup (bakes into the snapshot), startup (runs every session), and secrets.
Setup commands
One-time, runs when Devin builds the workspace snapshot:
curl -fsSL https://install.sageox.ai/ox | sh
ox --version # sanity check that install succeededStartup commands
Runs at the start of every Devin session, on top of the snapshot:
ox agent primeox auto-detects the Devin sandbox from DEVIN_TASK_ID and configures itself accordingly.
Secrets
In the Devin Secrets dashboard, add:
SAGEOX_TOKEN=oxp_...Devin injects this into the environment before startup commands run. Never paste the token into the startup-commands box — that value is logged.
Notes
- Devin keeps the snapshot warm across sessions, so
ox agent primeruns against a pre-installed binary. If you bumpox, re-run the setup commands or invalidate the snapshot.
GitHub Actions
For CI use cases — generating release notes, distilling sessions into team context, running scheduled queries:
# .github/workflows/ox-distill.yml
name: ox distill
on:
schedule:
- cron: "0 6 * * *" # daily at 06:00 UTC
workflow_dispatch:
jobs:
distill:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install ox
run: curl -fsSL https://install.sageox.ai/ox | sh
- name: Prime and distill
env:
SAGEOX_TOKEN: ${{ secrets.SAGEOX_TOKEN }}
run: |
ox agent prime
ox distill --since=24hFor self-hosted runners with a persistent filesystem, ox will detect the persistent disk and use its normal local-state mode — you'll get better performance from the local CodeDB index without any config change.
GitHub Codespaces
A Codespace has persistent disk and a multi-hour lifetime. Treat it as a slow laptop, not a sandbox:
# one-time workspace setup
curl -fsSL https://install.sageox.ai/ox | sh
ox login # one-time, opens browser via Codespaces port-forward
ox init # connects the workspace to your repoAfter the first-run setup, every subsequent terminal session in the Codespace has ox configured from local state — no PAT needed, no per-session priming.
If you'd rather skip the device-code flow, export SAGEOX_TOKEN from a Codespaces secret instead of ox login. Both work; the device flow gives you the same credential refresh story as your laptop.
Troubleshooting
SAGEOX_TOKEN expired or invalid
The PAT has expired, been revoked, or is malformed. Create a new one at Settings → Tokens and update the secret in your agent platform. See Personal Access Tokens.
401 Unauthorized against a custom endpoint
ox defaults to https://api.sageox.ai. If you set SAGEOX_ENDPOINT to a staging endpoint, confirm the PAT was issued against that same environment. PATs are bound to the endpoint they were issued for.
failed to clone team context: dial tcp: i/o timeout
Network allowlist gap. The sandbox blocked the git host. For Claude Code Cloud, add the host to network.allowed_hosts. For Devin, check the workspace network settings. For GitHub Actions, this usually means a private team-context repo requires an SSH key — use HTTPS with a Git PAT instead.
ox: command not found
The install script failed silently, often because curl was missing from a minimal base image. Use the platform's package manager to install curl first, then re-run the install script. For Devin, add the install step to the setup commands so it runs in a snapshot with a fuller toolchain.
OX_EPHEMERAL=1 set but you appear to be on a persistent machine
You exported OX_EPHEMERAL=1 in a previous shell session and forgot. Unset it to restore full functionality. ox doctor will flag this on every invocation until you do.
Auto-detection misfired
If ox runs in constrained mode when you expect full mode (or vice versa), use the capability-level overrides above (OX_PERSIST_DISK, OX_NO_DAEMON, OX_NETWORK) rather than the coarse OX_EPHEMERAL=1. Run ox doctor to see which detection rule fired and what the resulting capabilities are.
Related
- Personal Access Tokens — the credential you need first
- ox agent prime — what gets loaded at session start
- ox doctor — see capabilities for the current environment
- SageOx + Codex cookbook — recipes for OpenAI Codex

