Set up your agent's cloud development environment
Define the config, bake tools into a cached image, run setup hooks, inject credentials, size compute, scope the GitHub token, and verify it all with a sandbox build from the terminal.
Every agent runs in an isolated cloud sandbox, and the default covers most agents: Python 3.13, Node.js 22, git, gh, curl, and a C/C++ toolchain (see The sandbox). This guide sets up everything beyond that, in one terminal loop: define the environment in the agent's config, store the credentials it needs, build and verify the image without running the agent, then test a session and deploy. Two more knobs, compute sizing and GitHub token scope, close the guide.
Prerequisites: the CLI installed and authenticated (brew install ellipsis-dev/cli/agent, then agent login; see CLI), and the GitHub App installed on your account. Sessions and builds clone real repositories, so the App comes first. In CI, export an API key as ELLIPSIS_API_TOKEN instead of logging in.
Define the agent
Scaffold a config file, then edit it:
agent config init agents/nightly-migration-checker.yamlThe finished config. Three parts define the environment: sandbox.image is what gets baked into the cached image, sandbox.hooks runs per-session setup, and sandbox.variables names the secret the sandbox receives. The next three sections cover each in turn:
ellipsis:
version: v1
name: Nightly migration checker
description: Runs splitshift-api's migration checks against a staging schema
claude:
system: |
Run `make check-migrations` in splitshift-api. If it fails,
identify the migration at fault and open an issue with the
failing output and the likely fix.
trigger:
type: cron
schedule: "0 3 * * *"
sandbox:
repositories:
- name: splitshift-api
variables:
- name: DOPPLER_TOKEN
image:
dockerfile_append: |
RUN curl -Ls https://cli.doppler.com/install.sh | sh
setup: |
cd splitshift-api && pip install -r requirements.txt
hooks:
post_start: |
doppler configure set token "$DOPPLER_TOKEN"
budget:
session: 1.50Bake tools and dependencies into the image
Install once, at image build time, instead of on every session. Two fields under sandbox.image split the work:
dockerfile_appendappendsRUNinstructions onto the managed base image. It runs when the image is assembled, before any repository exists, so use it for toolchain installs: apt packages, a package-manager binary, a CLI.setupis a shell script that runs after your repositories are checked out, and everything it writes to disk is captured in the cached image. Use it for dependency installs, so agents start withnode_modules/or a virtualenv already in place.
sandbox:
image:
dockerfile_append: |
RUN npm install -g pnpm
setup: |
cd app && pnpm installOnly RUN is supported in dockerfile_append. Ellipsis owns the base image, so FROM, USER, WORKDIR, ENTRYPOINT, and COPY are rejected at validation rather than silently dropped.
setup is plain shell, not Dockerfile syntax. It runs as the sandbox user with your sandbox variables available, and it is capped at 10 minutes. Environment variables are not captured in the image, but files are: never write a secret to disk in setup. A non-zero exit fails the session (exit status lifecycle_hook_failed) and is not retried.
The image is cached per repository set, commit, and image definition (dockerfile_append plus setup):
- A session at a commit that already has an image starts with everything in place, at no install cost.
- A session at a new commit starts from the most recent image for the same repositories, checks out the new commit, and re-runs
setupon top of the previous build, so package managers install only what changed. - Changing
dockerfile_appendorsetuprebuilds the image on the next session; after that, sessions reuse the new cached image.
Run setup with hooks
Hooks are shell scripts that run on every session, before the agent starts, as the sandbox user, with the session's sandbox variables in the environment:
post_startruns after the container starts, before any repository work. Repo-independent setup: authenticate a CLI.post_cloneruns after all repositories are cloned and checked out. Per-session, repo-dependent setup: code generation, or anything that touches session-scoped credentials.
Hook output is never cached, and each hook is capped at 5 minutes. A dependency install whose result should be reused across sessions belongs in image.setup instead.
The migration checker above splits its setup exactly this way: the Doppler CLI and splitshift-api's dependencies bake into the image, and post_start authenticates Doppler fresh each session, so the session-scoped token never lands in a cached image.
A hook or setup script that exits non-zero fails the session with exit status lifecycle_hook_failed and is not retried: broken setup surfaces as a clear failure, not an agent working in a half-prepared sandbox.
Inject credentials with sandbox variables
Agents often need credentials: a Linear API key, an npm token, a Doppler service token like the one above. Sandbox variables get them into the session's environment without ever putting them in the config file. Two parts: store the value once on your account, then name it in the agent's config. Only agents that name a variable receive it.
Store the value from the terminal:
$ agent sandbox variable set DOPPLER_TOKEN=dp.st.staging.4kXq...
✓ stored 1 variable(s) (values hidden): DOPPLER_TOKEN
NAME CREATED UPDATED
DOPPLER_TOKEN 2026-07-17 09:14 2026-07-17 09:14Load several at once from a .env or flat-JSON file with --from-file, delete with agent sandbox variable rm <name>, or store from the dashboard or the API:
curl https://api.ellipsis.dev/v1/sandboxes/variables \
-H "Authorization: Bearer $ELLIPSIS_API_TOKEN" \
-H "Content-Type: application/json" \
-X PUT \
-d '{"variables": [{"name": "LINEAR_API_KEY", "value": "lin_api_..."}]}'Values are write-only: the API and agent sandbox variable list return names and timestamps, never values. An account holds up to 500 variables.
Then name the variable under sandbox.variables. An entry without a value resolves from your stored variables at session start; an inline value is a literal, for non-secret config:
sandbox:
variables:
- name: DOPPLER_TOKEN
- name: LOG_LEVEL
value: infoIn the sandbox both are ordinary environment variables: $DOPPLER_TOKEN in a shell command, os.environ["DOPPLER_TOKEN"] in a script the agent writes.
If an agent names a variable that has no inline value and no stored value, the session (or image build) fails immediately rather than starting with it unset. Define the variable first.
Code running in a sandbox can list which variable names exist but cannot create, update, or delete stored values: the in-sandbox API token is rejected with 403 on writes. Rotating a secret is one set from outside; the next session picks it up.
Build the sandbox image
A build runs the config's environment definition exactly as a session would: the managed base image plus your dockerfile_append layers, then the repository checkout, then image.setup, then a snapshot into the image cache. No agent starts and no tokens are spent; a build bills compute only.
$ agent sandbox build start --config-file agents/nightly-migration-checker.yaml --watch
✓ started build build_3fKq82Lm
[image] managed base + dockerfile_append (cache tier: full)
[clone] splitshift-api @ 4c19e7d
[setup] cd splitshift-api && pip install -r requirements.txt
[setup] Collecting fastapi==0.115.6
[setup] Successfully installed fastapi-0.115.6 sqlalchemy-2.0.36 alembic-1.14.0
[snapshot] image cached
✓ build succeeded in 3m41sThis is the iteration loop for dockerfile_append and setup: a broken install fails here with its full output, in seconds-to-minutes per cycle, instead of failing a real session as lifecycle_hook_failed with no output persisted. And a green build is not a throwaway artifact: the snapshot lands in the shared image cache, so the team's next session on this config starts from the warm image.
--watchstreams the build to completion and exits0or1; without it,startreturns the build record immediately. Follow a detached build withagent sandbox build logs <buildId> --watch.--config <configId>builds a deployed config instead of a local file, to re-verify a live agent.--hooksalso runspost_startandpost_clonein the built sandbox. Off by default: hooks are per-session scripts, allowed to have side effects like fetching credentials, and are never baked into the image.- A build reports its cache tier.
exactmeans the image for this config and commit is already warm and the build skips straight to done; that is a result, not a wasted run. - A build validates the environment, not the agent: the system prompt and skills are never exercised. Build output is recorded and readable by your account, so keep
setupfrom echoing secret values.
Inspect past builds with agent sandbox build list and agent sandbox build get <buildId> (status, phase timings, cache tier, cost).
Test a session, then deploy
The same local file drives a one-off session, so test the agent end to end before anything is committed:
agent session start --config-file agents/nightly-migration-checker.yaml --watchThe image build is already cached, so the session starts with the Doppler CLI and splitshift-api's dependencies in place. When the run looks right, deploy. agent config create opens a pull request adding the config file to your repository, the same way the dashboard does:
$ agent config create --repo splitshift-api --file agents/nightly-migration-checker.yaml
✓ opened a pull request adding the agent (agents/nightly-migration-checker.yaml)
https://github.com/splitshift-hq/splitshift-api/pull/512
Merge it to deploy the agent.Once merged, Ellipsis syncs the config from the default branch (any agents/, .agents/, ellipsis/, or .ellipsis/ directory; see Agents as code) and the cron trigger takes over. Confirm it is live and watch it run:
agent config list # the new config, with its source path@branch
agent session list --config agent_x9Kd3Fq2 # sessions the trigger has startedSize the compute
Every sandbox gets 1 vCPU, 4096 MiB of memory, and a one-hour session timeout. sandbox.compute changes any of them per agent:
sandbox:
compute:
cpu: 4 # 0.125 to 16 vCPU
memory: 16GB # 512MB to 64GB
timeout: 15m # 60s to 1hmemory and timeout each accept a shorthand string or an explicit-units object. memory is a size string (512MB, 16GB; units are binary, so 1GB is 1024MB) with the units MB and GB; timeout is a duration string (90s, 15m, 1h, or combined like 1h30m) with the units h, m, and s. cpu is a plain vCPU count. Omitted fields keep their defaults. A value outside the allowed range fails config validation; nothing is clamped.
The object form spells the units out as keys, which reads well when you would otherwise reach for a comment. Set any of the unit keys (gb/mb for memory; hours/minutes/seconds for timeout); values on the same field sum:
sandbox:
compute:
memory:
gb: 16
timeout:
minutes: 15timeout caps the session's wall clock: a session that reaches it has its sandbox killed and fails. One hour is both the default and the maximum (a sandbox's GitHub token lives one hour, and a session never outlives its credential), so the knob only shortens a session: set it low to bound quick, frequent agents. Compute is billed on the requested allocation over the sandbox's lifetime, so a bigger or longer sandbox costs proportionally more. The session's spend limits bound its token cost independently of the timeout.
Size up for workloads that need it (a monorepo build, a large test suite), not by default: the base allocation covers most agents, and unused headroom still bills.
Restrict the GitHub token
Every sandbox gets a GH_TOKEN: a GitHub installation token minted for that sandbox alone, which git and gh authenticate with. By default it carries your installation's full permissions, scoped to the sandbox's repositories. sandbox.github_token narrows it, and GitHub enforces the narrowing: the token itself is minted with the reduced scope, so nothing the agent runs can exceed it.
permissions: read_only grants read access to contents, issues, metadata, and pull_requests. An agent that only reports never needs more:
ellipsis:
version: v1
name: Recent work summary
description: Summarizes the week's merged work across splitshift-web and splitshift-api
claude:
system: |
Summarize the pull requests merged in the last 7 days across the
repositories in your sandbox: what shipped, what it changed, and
anything still open that reviewers flagged. Return the summary as
your final message.
trigger:
type: cron
schedule: "0 9 * * 1"
sandbox:
repositories:
- name: splitshift-web
- name: splitshift-api
github_token:
permissions: read_only
budget:
session: 1.00This agent reads code and pull request history normally. If a prompt injection (or a bug) makes it try to push a branch, open a pull request, or edit an issue, GitHub rejects the call: the token was never granted write.
For finer control, request explicit levels per GitHub App permission scope, and optionally pin the token to a subset of the sandbox's repositories:
sandbox:
repositories:
- name: splitshift-api
- name: splitshift-infra
github_token:
permissions:
contents: read
pull_requests: write
repositories: [splitshift-api]Here the agent can read both checkouts on disk, but its token only reaches splitshift-api, and there only to read contents and write pull requests.
Rules:
- Permissions can only reduce what your installation granted. Requesting an ungranted permission fails the session with exit status
missing_token_permissions. repositoriesentries must be repositories the installation can access; a name it cannot access fails the session with exit statusmissing_repo_access. Public repositories from other owners are cloned anonymously and are never in the token's scope.- Cloning is unaffected: Ellipsis checks out repositories with its own credential before the agent starts, so a read-only token never breaks checkout,
image.setup, or hooks that read the repo. - The token lives one hour, the maximum session length;
github_tokencontrols what it may do, not how long.
Next
- Every CLI command and flag: CLI.
- What the sandbox is and what runs inside it: The sandbox.
- Load Claude Code skills into the sandbox: Share skills across your team's agents and repositories.
- Start sessions from CI and scripts: Spawn an agent session from the API and CLI.