Guides

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.yaml

The 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.50

Bake 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_append appends RUN instructions 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.
  • setup is 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 with node_modules/ or a virtualenv already in place.
sandbox:
  image:
    dockerfile_append: |
      RUN npm install -g pnpm
    setup: |
      cd app && pnpm install

Only 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 setup on top of the previous build, so package managers install only what changed.
  • Changing dockerfile_append or setup rebuilds 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_start runs after the container starts, before any repository work. Repo-independent setup: authenticate a CLI.
  • post_clone runs 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:14

Load 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: info

In 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 3m41s

This 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.

  • --watch streams the build to completion and exits 0 or 1; without it, start returns the build record immediately. Follow a detached build with agent sandbox build logs <buildId> --watch.
  • --config <configId> builds a deployed config instead of a local file, to re-verify a live agent.
  • --hooks also runs post_start and post_clone in 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. exact means 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 setup from 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 --watch

The 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 started

Size 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 1h

memory 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: 15

timeout 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.00

This 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.
  • repositories entries must be repositories the installation can access; a name it cannot access fails the session with exit status missing_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_token controls what it may do, not how long.

Next