Cloud Agents

Permissions

Each session runs with its own short-lived GitHub token, never a shared credential. Narrow what the token can do per agent in YAML, down to read-only.

Every agent runs with credentials scoped to its session: a GitHub token minted for that sandbox alone, narrowed in YAML to exactly what the job needs.

Scoped tokens

Each session's sandbox gets its own GitHub token (GH_TOKEN), minted from your Ellipsis GitHub App installation when the sandbox starts. It lives one hour, covers only the repositories in the sandbox, and dies with the sandbox. No session shares a token with another, and no personal credential is ever involved.

Everything in the sandbox that talks to GitHub (git, gh) authenticates with this one token, so the token's scope is the agent's blast radius. By default the token carries your installation's full permissions on the sandbox's repositories. Two fields under sandbox.github narrow it: permissions limits what the token can do, and repositories limits which repositories it can touch.

Model calls follow the same posture: sessions reach models through Ellipsis with a per-session key, and a real provider key never enters the sandbox. Sandboxes covers what else is in the box; Security covers the full trust boundary.

Permission maps

A map under sandbox.github.permissions requests an explicit level per GitHub App permission scope. This agent rewrites pull request descriptions; it has no reason to push code, so its token says so:

ellipsis:
  version: v1
  name: PR describer
  description: Keeps the pull request description current on every push

claude:
  system: |
    Read the full diff of this pull request, then rewrite the
    description to match what the branch does now. Preserve anything
    the author wrote by hand. Update the description with `gh pr edit`.

trigger:
  type: react
  pull_request:
    on: [pushed]
    repositories: [splitshift-api]

sandbox:
  repositories:
    - name: splitshift-api
  github:
    permissions:
      contents: read
      pull_requests: write

budget:
  session: 0.50
  day: 5.00

When priya-shah pushes to an open pull request, the agent reads the diff and gh pr edit lands the new description. If anything in that sandbox attempts git push, GitHub refuses with a 403: the token was minted with contents: read, so writing repository contents is impossible no matter what asked for it.

That last property is the point. The restriction is enforced by GitHub, not by Ellipsis: the token itself is minted with the reduced scope, so nothing running in the sandbox can exceed it, not a misbehaving tool and not a prompt injection hiding in a pull request description.

A map can only narrow. It may never exceed what your GitHub App installation already holds, and an unknown scope, or a level a scope does not take, fails config validation before the agent deploys.

Supported scopes: actions, administration, checks, contents, discussions, issues, members, metadata, pull_requests, repository_projects, secret_scanning_alerts, security_events, vulnerability_alerts. Each takes read or write, with two exceptions: checks is write-only, and administration and repository_projects also accept admin.

sandbox.github.repositories narrows the other axis: which repositories the token can touch, by name. A documentation agent that reads the API code but may only write to the docs repository:

sandbox:
  repositories:
    - name: splitshift-api
    - name: splitshift-docs
  github:
    repositories: [splitshift-docs]
    permissions:
      contents: write
      pull_requests: write

Both repositories are cloned and readable on disk; the token can push and open pull requests only against splitshift-docs. Narrowing never breaks the clone itself: repositories are checked out before the agent starts, with a separate credential that never enters the sandbox.

Read-only by construction

For reporting jobs (a standup writer, a weekly digest, a triage summary) the narrowest useful grant is a named posture:

sandbox:
  github:
    permissions: read_only

read_only mints the token with read access to contents, issues, metadata, and pull requests, and nothing else. The set is fixed, so the line means the same thing on every account. The agent can read anything in its repositories and write nothing: it physically cannot push a commit, open a pull request, or edit an issue, and no prompt can change that.

Because the grant is YAML in git, an agent's blast radius is reviewed like the rest of your code. How to automate your daily standup runs its whole reporting job on this one line.

Secrets

Credentials your build needs beyond GitHub (a registry token, a Doppler token, an internal API key) enter the sandbox as sandbox.variables. Store the value once (agent variable set NPM_TOKEN=..., or the dashboard), then name it in the config:

sandbox:
  variables:
    - name: NPM_TOKEN

The name list is the scope. Only agents that name a variable receive it; the rest of your stored variables never enter any sandbox. Stored values are write-only: once uploaded they are injected into sandboxes but never readable back through the dashboard, API, or CLI, which list names and timestamps only. Like the GitHub token, injected values die with the sandbox.

An inline value: hardcodes the variable in the config file. Use it for non-secret settings (LOG_LEVEL, an API base URL); a secret belongs in the store, never in a file in git.

Session logs record what your setup scripts print, so keep image.setup and hooks from echoing a value. The full workflow, from storing a credential to authenticating a CLI with it: Give agents the tools your build needs.