Guides

How to give agents the tools and credentials your build needs

Bake your toolchain into a cached image, run setup hooks, and inject credentials so agents can actually build and test your code.

Your build needs more than a language runtime. It needs your package manager, your private registry token, a database to run tests against. An agent that cannot run your test suite cannot verify its own work, so it opens pull requests nobody trusts.

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, verify it end to end with a rebuild session, then 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 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/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 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.

Test the sandbox environment

Every session streams its own provisioning: which repositories are cloning, each image.setup and hook output line as it runs, and which image-cache tier the start took. To exercise the environment definition end to end, start a session on the config with --rebuild — it skips the image cache and provisions through a fresh full build (image layers, clones, image.setup), whose snapshot then refreshes the cache — and give the agent a prompt that verifies the tools:

$ agent session start --config-file agents/nightly-migration-checker.yaml --rebuild --watch \
    "Verify this environment: run gh auth status, confirm the repositories are cloned, and print doppler --version."
✻ Session starting…
✻ Starting sandbox…
  ⎿ Fetching repositories… · Cloning splitshift-api @ 4c19e7d
  ⎿ Running setup… · Successfully installed fastapi-0.115.6 sqlalchemy-2.0.36 alembic-1.14.0
✓ Sandbox ready · splitshift-hq/splitshift-api · full build

This is the iteration loop for dockerfile_append and setup: a broken install fails right there with its output streamed into the session feed, and the full provisioning log persists past the sandbox (GET /v1/sessions/{id}/logs returns each execution's complete log as a download). The agent then actually exercises the environment — a real gh auth status beats any synthetic smoke test — and the rebuilt snapshot lands in the shared image cache, so the team's next session on this config starts from the warm image.

  • Without --rebuild, a session provisions through the image cache: cached image in the startup line means the environment for this config and commit is already warm.
  • post_start and post_clone run in every session (their output streams the same way), so the verification session covers them too.
  • Provisioning output is recorded and readable by your account, so keep setup from echoing secret values.

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 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 accepts either of two forms, so you can take a broad preset or name each permission one by one:

  • read_only — the shorthand, granting read access to contents, issues, metadata, and pull_requests.
  • A map of GitHub App permission scopes — each scope you name set to the level you want it at (read or write). Everything you leave out is not granted at all.

Reach for the shorthand first. 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:
    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.

When read_only is not the right set, name the permissions one by one instead. Each key is a GitHub App permission scope and each value is the level you want, which is how you grant write on exactly one thing. You can also pin the token to a subset of the sandbox's repositories:

sandbox:
  repositories:
    - name: splitshift-api
    - name: splitshift-infra
  github:
    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. Because the map is exhaustive, this agent cannot touch issues at all: issues is not listed, so no level is granted.

The scopes you can name, and the levels each accepts:

ScopeLevels
contents, issues, metadata, pull_requestsread, write
actions, members, discussionsread, write
secret_scanning_alerts, security_events, vulnerability_alertsread, write
administration, repository_projectsread, write, admin
checkswrite

Naming a scope Ellipsis does not recognize, or a level a scope does not accept, fails validation with the supported list.

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 controls what it may do, not how long.

Next