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.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/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 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 buildThis 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 imagein the startup line means the environment for this config and commit is already warm. post_startandpost_clonerun 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
setupfrom 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 --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 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 tocontents,issues,metadata, andpull_requests.- A map of GitHub App permission scopes — each scope you name set to the level you want it at (
readorwrite). 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.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.
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:
| Scope | Levels |
|---|---|
contents, issues, metadata, pull_requests | read, write |
actions, members, discussions | read, write |
secret_scanning_alerts, security_events, vulnerability_alerts | read, write |
administration, repository_projects | read, write, admin |
checks | write |
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. 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;
githubcontrols 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: How to teach every agent your team's conventions.
- Start sessions from CI and scripts: How to trigger agents from your own scripts and CI.
How to turn Sentry alerts into root-cause diagnoses
React to Sentry alerts with an agent that reads the stack trace, finds the code path, and posts a diagnosis or opens a fix.
How to teach every agent your team's conventions
Write your conventions once as a skill and have every agent across every repository load it when the task calls for it.