# Introduction
> Deploy AI software engineers from YAML files in your repository.
Source: https://www.ellipsis.dev/docs
An Ellipsis agent is a YAML file in your repository. It runs Claude in an isolated cloud sandbox with your repositories cloned and your GitHub, Linear, and Slack integrations connected, on a schedule, in reaction to events, on `@ellipsis` mentions, or on demand.
Three commands to a running agent. Full walkthrough in the
[Quickstart](/docs/get-started/quickstart).
```bash
brew install ellipsis-dev/cli/agent
agent login
agent session start --template welcome-to-ellipsis --watch
```
## Get started
Install the CLI and run your first agent in three commands.
Write an agent config from scratch and deploy it from your repository.
## Guides
Cron triggers: weekly digests, daily triage, dependency checks.
Start sessions when pull requests open, push, merge, close, or get reviewed.
Route mentions on GitHub, Slack, and Linear to an agent.
Bake tools into the image, run setup hooks, and inject credentials.
Repo skills load automatically; config skills share one library org-wide.
Trigger and watch sessions from the API and CLI.
Bring laptop Claude Code sessions into one searchable history.
Assets: images that outlive the sandbox, linkable on pull requests.
## Concepts
The session lifecycle: statuses, steps, costs, and what each session records.
Per-session budgets, trailing spend limits, and per-developer limits.
The isolated environment agents run in, and how to detect it.
Why GitHub is the source of truth, and how sync works.
Engineering metrics across humans, bots, and AI agents.
## Reference
Every field, default, and constraint in agent config files.
The `agent` binary: install, authenticate, run.
Start sessions, read history, and stream output over HTTP.
---
# Agents as code
> GitHub is the source of truth; how sync, previews, and deletes work.
Source: https://www.ellipsis.dev/docs/concepts/agents-as-code
Agents are YAML files in your repository, and GitHub is the source of truth: the file on your default branch is the agent. This gives agents everything code has: review before deploy, history, rollback with `git revert`, and no drift between what is written and what runs.
## The sync model
Pushes to the default branch sync agent configs. The canonical directory is `agents/`; `.agents/`, `ellipsis/`, and `.ellipsis/` work too, at any depth. A file is a config when it declares a top-level `ellipsis:` mapping; other YAML in those directories is ignored.
| You push | Ellipsis does |
| --- | --- |
| A new `agents/name.yaml` | Parses, validates, and registers the agent; starts any cron schedules. |
| An edit to the file | Updates the agent. |
| A rename or move (content unchanged) | Keeps the same agent: identity, history, and schedules carry over. |
| A delete | Soft-deletes the agent and removes its schedules. |
| An invalid edit | Records the sync error and keeps the last good config running. |
## The default branch decides
Registered agents always come from the default branch. A pull request branch never registers, edits, or deletes an agent, and a session that already started keeps the config snapshot it began with.
On private repositories, Ellipsis previews config changes on the pull request that makes them: sessions triggered by that PR use the PR's version of the config, so you watch an edited agent behave before merging. The preview is never registered, changes no schedules, and applies only to same-repo PRs by authors with write access.
## Deleting and reviving
Deleting the file stops future sessions and removes the agent from the dashboard; past sessions and their config snapshots stay intact. Re-adding a file at the same path revives the same agent: its identity, session history, and schedules reconnect. Re-adding the config at a different path registers a new agent.
## The dashboard is a front-end to the same model
Creating an agent in the dashboard does not bypass GitHub: Ellipsis opens a pull request adding the YAML file under `agents/`, and the agent goes live when it merges. After that, the file is the agent; edit or delete it like any other code.
## Validation
The same validator runs everywhere: GitHub sync, the dashboard, and `POST /agents/configs/validate`. A config fails validation when the `ellipsis.version` is unsupported (only `v1` exists), `claude.system` is empty on a non-mention agent, a [file referenced from `system`](/docs/reference/agent-config#system-prompts-from-repository-files) is missing or over 64 KiB at the pushed commit, a [`skills` entry](/docs/guides/share-skills-across-your-teams-agents-and-repositories) in the config's repository has no `SKILL.md` at the pushed commit, a cron expression is invalid, or `branches` is set on a non-`push` react event. New files that fail validation are reported and not registered; existing agents keep their last good config.
---
# Analytics
> Team velocity from your GitHub data, measured for humans and bots on the same board.
Source: https://www.ellipsis.dev/docs/concepts/analytics
Analytics measures your team's velocity from GitHub data: what merged, how fast it merged, and who shipped it. Every contributor counts, human engineers and AI agents alike, not just @ellipsis.
## Why track team velocity
Once agents open pull requests, "are we shipping faster" stops being answerable by feel. Velocity analytics exist to answer three questions:
- **Is agent adoption working?** If agents help, throughput rises and time to merge falls after you turn them on. If neither moves, change how you use them, not how hard you squint at anecdotes.
- **Who ships what?** Humans and bots sit on the same leaderboard, so agent output is measured with the same yardstick as everyone else's instead of living in a separate tool's dashboard.
- **Is review keeping up?** More authored code means more review load. Watching review counts next to merge speed shows whether the bottleneck moved from writing code to reviewing it.
The mechanism for all three is the same: every chart can be split between humans and bots. An author counts as a bot when GitHub marks the account as a bot or its login ends in `[bot]`, like `ellipsis[bot]` or `claude[bot]`; everyone else is a human. Alongside that toggle, every chart honors a date range (the last 30 days by default) and repository and author filters.
## Throughput
The Dashboard opens on this chart. Pick a metric tile and every bar below redraws as that metric’s daily total, counting humans, bots, and agents together.
| Part of the chart | What it means |
| --- | --- |
| Daily bars | One bar per UTC day. Its height is the selected metric’s total, counting each merged pull request on the day it merged. |
| Metric tiles | Switch what the bars measure. The number on each tile is that metric’s total across the whole window; the Time to merge tile swaps the bars for a percentile distribution. |
| Scale | The y-axis rescales to the busiest day in the current window, so the shape stays readable as you change filters. |
| Window & filters | The date range (the last 30 days by default) plus the repository and author filters decide which pull requests are counted. |
| Humans vs bots | The "Humans only" and "Bots only" toggles redraw every bar for that group alone. An author is a bot when GitHub marks the account as a bot or its login ends in [bot]; comparing the two views shows what share of daily output comes from agents. |
Read it twice: once on **Humans only** for the team's baseline, once on **Bots only** for what agents add on top. If the bot view is empty, your agents are configured but not shipping; if the human view falls while the bot view grows, work is shifting, not disappearing.
## Speed
Selecting the Time to merge tile turns each day into a distribution instead of a single bar, so a few slow outliers never hide behind an average. The humans vs bots toggle applies here too: compare how fast agent pull requests merge against human ones.
| Part of the chart | What it means |
| --- | --- |
| Interquartile box | The p25–p75 span of how long that day’s merged pull requests took, from `created_at` to `merged_at`. The middle half of pull requests fall inside it. |
| Median tick | The p50 line. Half of that period’s pull requests merged faster, half slower. It is the line an average would hide. |
| Full spread | The thin whisker runs from p0 (fastest) to p100 (slowest). In the app, hovering a day surfaces all five percentiles in the tooltip. |
| Daily trend | One box per UTC day. Boxes drifting down and tightening over the window mean changes are getting attention and shipping faster. |
A distribution, because a few week-old pull requests hide behind any average. The humans vs bots toggle is the interesting comparison here: agent pull requests that merge markedly slower than human ones usually signal review hesitation, not agent slowness.
## Attribution
Every contributor to your repos, ranked. Humans and AI agents appear on the same board, ordered by pull requests merged.
| Part of the chart | What it means |
| --- | --- |
| Ranking bar | Authors are ordered by pull requests merged; each bar’s length is that author’s PR count relative to the leader. |
| Contributor | Anyone who merged code in the window: engineers and AI agents alike, detected from commit authorship, not just @ellipsis. |
| Lines | Total lines added + deleted across that author’s merged pull requests. |
| PRs | Pull requests that author merged in the window. |
The leaderboard is where the humans vs bots split becomes concrete: `ellipsis[bot]` and `claude[bot]` rank alongside your engineers on identical metrics. That is the number to bring to the "are the agents worth it" conversation.
## Beyond the dashboard
The same data backs two more views with the same filters. The **Pull requests** view lists every pull request in the window with its author, repository, lines changed, and status, and its over-time chart splits opened, merged, and closed without merging. The **Reviews** view tracks reviews, inline comments, and active reviewers per day, adds reviewer-type and review-status filters, and ends in a chronological feed of the feedback behind the counts. Under the throughput chart, a strip of derived ratios covers median time to first review, lines changed per pull request, reviews per pull request, and pull requests merged per author.
## Programmatic access
Everything above is also an API: the [`/v1/analytics` endpoints](/docs/reference/api#analytics) return the same aggregation with the same filters, and [`agent analytics reviewers|prs|reviews`](/docs/reference/cli#analytics) prints it in the terminal. Because they take any credential, an agent can query them mid-session, so "which apps review the most pull requests?" is one command: `agent analytics reviewers --account-type bot`.
---
# Budgets and spend
> Per-session budgets, trailing spend limits, per-developer limits, and what happens at the cap.
Source: https://www.ellipsis.dev/docs/concepts/budgets
Agents spend real money, so spend is capped three ways: a budget on every session, trailing limits on totals, and optional per-developer limits. All ship with defaults you can tighten in **Usage → Limits** or per agent in its config.
## How budgets work
Every session resolves a budget before it starts: the config's `limits.run` if set, otherwise your account default ($50, also the platform maximum per session). As the agent works, each model call debits the budget in real time. A session that reaches its cap stops immediately and records `budget_hit`; the session timeline shows which step spent what.
```yaml fragment
budget:
session: 5.00 # this session may spend at most $5
```
For a weekly-digest agent, $1 is usually plenty. For an agent that opens PRs, budget for a few test sessions too.
To give a single session more room without editing the config, pass `config_override_yaml` when [starting or replaying it](/docs/guides/spawn-an-agent-session-from-the-api-and-cli#steer-a-single-session).
## Trailing spend limits
Account-wide caps on total spend over rolling windows, checked before every session. A session that would exceed one is cancelled before a sandbox exists.
| Window | Default |
| --- | --- |
| 1 day | $250 |
| 7 days | $2,500 |
| 28 days | $10,000 |
Adjust them in **Usage → Limits**, up to a platform ceiling of $1,000 / $10,000 / $40,000.
## Per-agent trailing limits
A config caps its own trailing spend with `limits.day`, `limits.week`, and `limits.month` (the same 1/7/28-day windows, same ceilings). This is the difference between "the account may spend $250 today" and "this one agent may spend $2 this week":
```yaml fragment
budget:
session: 1.00
day: 2.00
week: 10.00
month: 30.00
```
Omit any value to inherit the default: the account default for `run`, the platform ceiling for the windows.
## Per-developer limits
Cap what each person can spend, so one enthusiastic teammate can't consume the account's daily limit. Off by default; turn on **Limit spend per developer** in **Usage → Limits**.
Once on, the default limits apply to **every** developer automatically: anyone who triggers sessions through a mention, the dashboard, the CLI, or an API key. Overrides are the exception layer; add one for a developer who needs different numbers, and any window you leave empty inherits the default. The same person's GitHub and CLI activity counts as one developer.
The windows are the same trailing 1/7/28 days, from $1 up to the same platform ceilings. Enabling seeds defaults of $25 / $100 / $250; adjust before saving.
Common setups, using the 1-day window:
| Goal | Setup |
| --- | --- |
| Everyone gets $50, `@priya` gets $75 | Default limit `50.00`, override for `priya` at `75.00` |
| Trial a new teammate at $10 | Keep the default, add an override at `10.00` |
| No practical cap for one lead | Override at the ceiling, `1000.00` |
A developer at their limit has new sessions cancelled before a sandbox exists, recorded as `budget_hit` with the reason ("Spend limit exceeded: developer trailing 1-day limit of $75.00 reached."). A blocked mention gets a reply with that reason and a link to the session. Everyone else keeps working.
Scheduled sessions are exempt: a cron agent belongs to the team, not the person who committed its config. Cap it with [per-agent trailing limits](#per-agent-trailing-limits) instead.
## Reading spend
Each session stores its cost broken down into token spend, sandbox CPU, sandbox memory, and Ellipsis fee (see [How a session executes](/docs/concepts/sessions#what-each-session-records)). `agent budget` and `GET /v1/budget` show account spend for the billing period, and **Usage → Limits** shows each developer's trailing spend against their personal limit.
---
# The sandbox
> The isolated cloud environment agents run in, and how to detect it.
Source: https://www.ellipsis.dev/docs/concepts/sandbox
Every agent runs in its own cloud sandbox: an isolated Linux container created for the session, with the session's repositories already cloned, and torn down when the session finishes. Nothing an agent installs or breaks can leak into the next session; the only thing that carries over is the cached image the sandbox boots from, which holds the repositories and anything the config [bakes in at image build time](/docs/guides/set-up-your-agents-cloud-development-environment#bake-tools-and-dependencies-into-the-image).
## What's inside
The base image builds on `python:3.13`: Python 3.13 with `pip`, Node.js 22 with `npm`, `git`, the GitHub CLI (`gh`), `curl`, and a C/C++ toolchain (`make`, `g++`). Repositories are cloned before the agent starts, and `gh` is already authenticated against them.
Skills load at startup: each repository's `.claude/skills/` at the session's checkout, plus any config-declared skills, installed at `~/.claude/skills/` before the agent begins. See [Share skills across your team's agents and repositories](/docs/guides/share-skills-across-your-teams-agents-and-repositories).
Each sandbox gets 1 vCPU, 4096 MiB of memory, and a one-hour session timeout. `sandbox.compute` sizes CPU and memory up to 16 vCPU and 64 GiB, and can shorten the timeout; one hour is the maximum, matching the one-hour lifetime of the sandbox's GitHub token (see [Size the compute](/docs/guides/set-up-your-agents-cloud-development-environment#size-the-compute)).
Beyond that, an agent can install tools at run time, or the config can [bake tools and dependencies into the image and run setup hooks](/docs/guides/set-up-your-agents-cloud-development-environment).
## Credentials are scoped and die with the sandbox
The sandbox holds no long-lived secrets. Its GitHub token (`GH_TOKEN`) is a one-hour installation token minted for that sandbox alone, scoped to the repositories in it; the config's `sandbox.github_token` can narrow it further, down to read-only or a chosen permission set (see [Restrict the GitHub token](/docs/guides/set-up-your-agents-cloud-development-environment#restrict-the-github-token)). Model calls route through the Ellipsis proxy with a synthetic key (a real Anthropic key never enters the sandbox), and the Ellipsis API token can read but not modify your stored [sandbox variables](/docs/guides/set-up-your-agents-cloud-development-environment#inject-credentials-with-sandbox-variables). The proxy and API tokens are revoked when the sandbox is torn down; the GitHub token expires on its own within the hour.
## Detecting the sandbox
Ellipsis sets `ELLIPSIS_SANDBOX_ID` in every sandbox, and nowhere else. If your agents need to know whether they are running on the Ellipsis cloud, have them check `ELLIPSIS_SANDBOX_ID`.
## Environment variables
Anything named under an agent's `sandbox.variables` is an ordinary environment variable in that session: `$NPM_TOKEN` in shell, `os.environ["NPM_TOKEN"]` in Python. Only the variables an agent names reach its sandbox. See [Inject credentials with sandbox variables](/docs/guides/set-up-your-agents-cloud-development-environment#inject-credentials-with-sandbox-variables).
---
# How a session executes
> The session lifecycle, durable conversations, statuses, steps, and what each session records.
Source: https://www.ellipsis.dev/docs/concepts/sessions
Every agent execution is a session. Understanding the lifecycle tells you what any status means, why a session ended, and where its money went.
## Which config a session runs
A triggered session (cron, react, mention) always runs the config that declares its trigger. An on-demand session (dashboard, API, CLI) runs the config you name: a saved config id, an inline config, or a template.
Name none of those and Ellipsis resolves your team's default agent, in order:
1. **Repository default.** If the request carries a repository (the CLI sends the repo you are standing in) and that repository has a default agent, it runs.
2. **Account default.** Otherwise the account-wide default agent runs, if one is set.
3. **Bare config.** With no defaults set, the session runs a plain ad-hoc config: an empty system prompt on the default model, and your prompt is the sole instruction.
Defaults are pointers to existing synced configs, set on the agents settings page, with [`agent config default set`](/docs/reference/cli#default-agents) in the CLI, or through the [API](/docs/reference/api#default-agents); the configs themselves stay [YAML in your repository](/docs/concepts/agents-as-code). Which rung resolved is recorded on the session, the start response carries it (`resolution_source`), and the CLI prints it: `using config "CI failure triager" (repo default)`. A default that points at a config that was deleted, disabled, or never merged fails the session start with an error naming the setting; it never silently runs a different agent.
The repository the client is standing in is also cloned into the sandbox at its default branch, merged with the config's own `sandbox.repositories`, even when you name a config explicitly. So `agent session start` inside a repo always has that repo on disk. For uncommitted work, use [handoff](/docs/guides/sync-laptop-claude-code-sessions-into-one-searchable-history#hand-a-session-off-to-the-cloud) instead.
## The lifecycle
A session moves through four phases:
1. **Checks.** Before anything is provisioned, Ellipsis verifies the agent is enabled, the account can start workflows, and no [spend limit](/docs/concepts/budgets) is exceeded. A failed check cancels the session here; nothing executes.
2. **Sandbox creation.** An isolated [sandbox](/docs/concepts/sandbox) is created with the session's repositories cloned: the ones the config declares plus any the trigger brings (a PR mention adds that PR's repository).
3. **Execution.** The agent works step by step. Every step is recorded as it happens: assistant output, tool calls, tool results, tokens, and cost. Spend updates live, and the session streams to the dashboard, CLI (`--watch`), and API in real time.
4. **Finalization.** The output is stored, costs are totaled, and the sandbox is torn down. Teardown always happens, success or failure.
## Durable conversations
On conversational surfaces, a trigger starts a durable conversation, not a single-shot session. Later events on the same surface are delivered into the conversation as messages, and the agent keeps everything it already said and did as context. Each agent holds its own conversation per surface: a react agent and a mention agent on the same pull request are two separate conversations.
- **Slack.** A thread is one conversation, in a channel or a DM: every later message in it, a reply or another mention, is delivered into it. A DM's top-level messages are a separate continuous conversation of their own, answered top-level like a person chatting. Replying in a thread under any DM message forks a new conversation scoped to that thread, so parallel workstreams run as parallel conversations in one DM.
- **GitHub pull requests.** A pull request is one conversation. The first event an agent reacts to on the PR starts it; every later PR event the config declares (pushes, comments, reviews) continues it. A merge or close is the conversation's final message: the agent takes one last turn, then the conversation ends for good.
- **GitHub issues.** The same shape: `issue_open` starts the conversation, `issue_comment` continues it, and `issue_close` is its final message.
- **Linear issues.** A Linear issue is one conversation. The first `@ellipsis` comment starts the mention conversation and later mentions continue it; an agent reacting to `linear_issue_open` holds its own conversation on the issue.
- **Sentry alerts.** An alert starts a conversation keyed to its recurring identity: the Sentry issue for issue alerts, the alert rule for metric alerts. A re-fire is delivered into the conversation ("this alert fired again") instead of starting a session with no memory. Re-fires of one Sentry issue reach agents at most once every 6 hours.
- **Mentions.** An `@ellipsis` mention on a pull request, issue, or Linear issue joins that surface's mention conversation, and repeat mentions continue it. On GitHub and Linear, a follow-up comment must mention `@ellipsis` again to be delivered; on Slack, bare replies continue the thread. See [Answer @ellipsis mentions](/docs/guides/spawn-an-agent-session-by-mentioning-ellipsis#conversations).
After a conversation ends (a merged or closed PR, a closed issue), the next event on that surface starts a fresh conversation with no memory of the old one, which is preserved as history: a comment on a merged PR is answered by a new conversation. Only those react events end conversations, and only for agents whose config declares them; mention conversations never end, they go idle. Triggers without a conversational surface (`cron`, `push`, `slack_channel_created`) start a fresh session per fire, as do sessions triggered by [config previews](/docs/concepts/agents-as-code) on an open PR.
What the agent does with each message differs by trigger. A mention agent replies to every message on the surface it was mentioned on. A react agent posts no reply per turn: it acts through the sandbox, in the commits it pushes, the PRs it opens, and the comments it chooses to make with `gh` or the integrations.
Every conversation, on every surface, behaves the same way:
- A message into an active conversation is delivered within about a second; the agent attends to it at its next natural pause, like typing to Claude Code while it works.
- After a turn, the conversation stays warm for 10 minutes. While warm, the sandbox keeps running and its compute bills like any other sandbox time.
- After the window, state is snapshotted. The next message resumes the conversation, working tree and memory intact, in a fresh sandbox within seconds.
- Stopping a session stops the current turn only; the next message re-engages the same conversation.
- `limits.run` caps each burst of activity. A resumed conversation runs on the budget remaining after prior spend; trailing [spend limits](/docs/concepts/budgets) apply as always.
A conversation renders as one session everywhere sessions appear. In the dashboard it shows its conversation state — **Active** while a turn is executing, **Idle** while waiting for the next message, **Closed** once its surface ends it — along with its turn count, and its detail page leads with a Conversation tab: each message the surface delivered, and the turn that answered it, with per-turn timing and cost. Over the API the same structure is `session_state` on the session object and [`GET /v1/sessions/{session_id}/turns`](/docs/reference/api#read-a-conversations-turns).
## Statuses
`status` is where the session is in that lifecycle:
| `status` | Meaning |
| --- | --- |
| `scheduled` | Accepted, waiting for a worker. |
| `creating_sandbox` | Provisioning the sandbox and cloning repositories. |
| `running` | The agent is working. |
| `retrying` | A transient infrastructure failure interrupted the session before the agent acted; it will re-execute from scratch. |
| `completed` | Finished its task. |
| `error` | Ended on an unrecoverable failure. |
| `cancelled` | Stopped during checks, before executing. |
| `stopped` | A user stopped it mid-flight. |
## Exit statuses
Terminal sessions also carry an `exit_status`: the reason they ended, so a budget cutoff reads differently from a crashed tool call.
| `exit_status` | The session ended because |
| --- | --- |
| `completed` | It finished normally. |
| `budget_hit` | It reached a per-session or trailing spend cap. |
| `payment_required` | The account has no credits or payment method. |
| `tool_call_failed` | An API, tool, or MCP call failed unrecoverably. |
| `lifecycle_hook_failed` | A `post_start` or `post_clone` hook exited non-zero. |
| `missing_repo_access` | The Ellipsis GitHub App lacks access to a repository the session needs. |
| `missing_token_permissions` | The config's `sandbox.github_token.permissions` requests more than the installation was granted. |
| `cancelled` | A pre-execution check failed. |
| `interrupted` | The worker died mid-session. |
| `error` | Any other unrecoverable error. |
| `stopped` | A user stopped it. |
## What each session records
A session permanently stores: the **config snapshot** it executed (editing the file later never changes what a past session shows), the **source** that started it (`cron`, `react`, `mention`, `manual`, `api`, `cli`, or `laptop`), attribution to the person or event behind it, its full **step timeline**, per-model **token usage**, and **cost** broken down into token spend, sandbox CPU, sandbox memory, and Ellipsis fee.
`laptop` sessions are the exception to the lifecycle above: they are [Claude Code sessions synced from a developer's machine](/docs/guides/sync-laptop-claude-code-sessions-into-one-searchable-history), ingested rather than executed, so no sandbox, budget, or exit status applies. They move between `running` and `completed` as syncs arrive.
The snapshot is also what [replay](/docs/guides/spawn-an-agent-session-from-the-api-and-cli#replay-a-session) re-executes: a replayed session reproduces the session as it was, not as the config is now.
## Following a session
- Dashboard: the session detail page streams steps live and keeps the full timeline after.
- CLI: `agent session get session_7Hq2mX4p --watch` follows until terminal, or stop it early with `agent session stop session_7Hq2mX4p`.
- API: `GET /v1/sessions/{session_id}` for state, the [WebSocket stream](/docs/reference/api#stream-a-session) for live output with resume.
---
# Quickstart
> Install the CLI, run the welcome agent, and read your first result.
Source: https://www.ellipsis.dev/docs/get-started/quickstart
Three commands to a running agent. The agent runs in the Ellipsis cloud, not on your machine.
## 1. Install the CLI
The Ellipsis CLI is a single binary named `agent`:
```bash
brew install ellipsis-dev/cli/agent
```
Install the Ellipsis [Agent Skill](https://agentskills.io) so Claude Code,
Cursor, or Codex can run these commands for you:
```bash
npx skills add ellipsis-dev/cli
```
See [Teach your coding agent about Ellipsis](/docs/reference/cli#teach-your-coding-agent-about-ellipsis).
## 2. Log in
`agent login` runs a device-code flow: it opens a verification URL in your browser and stores a user token under `~/.config/ellipsis/config.json`.
```bash
agent login
```
## 3. Run the welcome agent
```bash
agent session start --template welcome-to-ellipsis --watch
```
`--watch` streams the session's steps to your terminal. The welcome agent reads your account's recent GitHub activity with the GitHub tools, then replies with a two-part summary: what your team has been working on, and how agent configs work. A condensed result looks like:
```text
Your team merged 14 pull requests in the last two weeks, mostly in
splitshift-api: the shift-trade engine got idempotent retries, and
schedule conflict detection moved server-side.
An Ellipsis agent is a small YAML file: an ellipsis: block names it,
triggers say when it runs, and limits cap what it can spend. Commit
one to your repository and Ellipsis keeps it in sync. Try
`agent config init --template ` or the dashboard to create
your first one.
```
Prefer the browser? [app.ellipsis.dev](https://app.ellipsis.dev) walks you through the same first session.
## Next
Write and deploy an agent of your own: [Your first agent](/docs/get-started/your-first-agent).
---
# Your first agent
> Write an agent config from scratch and deploy it from your repository.
Source: https://www.ellipsis.dev/docs/get-started/your-first-agent
Build a working agent in five steps: a weekly summary of the code your team shipped. By the end you will have a config file in your repository, a live agent, and its first session.
The example org throughout these docs is Splitshift, a shift-scheduling product with repos like `splitshift-web` and `splitshift-api`. Substitute your own repository names.
## 1. Create the file
Agents live in your repository under `agents/`. Create `agents/recent-work-summary.yaml` with the agent's identity and instructions:
```yaml fragment
ellipsis:
version: v1
name: Recent work summary
description: A weekly summary of the code changes we shipped
claude:
model: claude-haiku-4-5-20251001
system: |
Use the GitHub tools to find the pull requests merged in the
last week and summarize what the team has been working on.
Keep it under 150 words and grounded in what the tools
returned. Never invent activity.
```
The `ellipsis:` block is what marks the file as an agent config. `claude.system` is the instruction the agent follows on every session; `claude.model` picks the model. GitHub tools are available automatically.
## 2. Choose when it runs
Add a trigger. This one fires every Monday at 09:00 UTC:
```yaml fragment
trigger:
type: cron
schedule: "0 9 * * 1"
```
An agent can also run when repository or Linear events happen (`react`) or when someone mentions `@ellipsis` (`mention`). A config with no triggers still runs on demand from the CLI, API, or dashboard.
## 3. Point it at your code
List the repositories cloned into the agent's sandbox:
```yaml fragment
sandbox:
repositories:
- name: splitshift-web
- name: splitshift-api
```
## 4. Cap what it can spend
Budgets are in US dollars. `run` caps a single session; `week` caps this agent's trailing 7-day spend:
```yaml fragment
budget:
session: 1.00
week: 2.00
```
The complete file:
```yaml
ellipsis:
version: v1
name: Recent work summary
description: A weekly summary of the code changes we shipped
claude:
model: claude-haiku-4-5-20251001
system: |
Use the GitHub tools to find the pull requests merged in the
last week and summarize what the team has been working on.
Keep it under 150 words and grounded in what the tools
returned. Never invent activity.
trigger:
type: cron
schedule: "0 9 * * 1"
sandbox:
repositories:
- name: splitshift-web
- name: splitshift-api
budget:
session: 1.00
week: 2.00
```
## 5. Deploy it
Commit the file to your default branch:
```bash
git add agents/recent-work-summary.yaml
git commit -m "Add weekly work summary agent"
git push
```
Ellipsis syncs the file on push, registers the agent, and starts the cron schedule. (The dashboard offers the same flow in reverse: it opens the pull request that adds this file for you.)
Run it once now instead of waiting for Monday:
```bash
agent config list # find the config's id
agent session start --config agent_x9Kd3Fq2 --watch
```
Every Monday after this, a session starts on its own. The session's summary reads like:
```text
Merged 9 PRs this week. splitshift-api: shift-trade requests now
expire after 48h (#412), fixed a double-booking race in schedule
assignment (#418). splitshift-web: managers can bulk-assign open
shifts (#87). One revert: #415 reverted #409 (timezone handling).
```
## Iterate
The file is the source of truth. Edit the YAML and push, and the agent updates on sync; delete the file and the agent is removed. See [Agents as code](/docs/concepts/agents-as-code) for the sync model and [Agent config](/docs/reference/agent-config) for every field.
---
# Attach agent screenshots to pull requests
> Persist images past the sandbox with assets, and paste org-gated links on pull requests.
Source: https://www.ellipsis.dev/docs/guides/attach-agent-screenshots-to-pull-requests
A sandbox is ephemeral: when the session ends, every file in it is gone. Assets are the exception, images an agent uploads to Ellipsis that outlive the sandbox, each with a link only members of your organization can open. The payoff is an agent that shows its work: a UI change lands as a pull request with a screenshot of the result, not just a diff.
## How assets work
An upload returns a link of the form `https://app.ellipsis.dev/assets/{asset_id}`. That link is gated: opening it requires being logged in as a member of the owning organization, and everyone else gets a 404, so it is safe to paste in a pull request on a private repository. The dashboard lists all of an account's assets, each linked to the session that produced it. Delete one you no longer want with `agent asset delete` or [`DELETE /v1/assets/{asset_id}`](/docs/reference/api#assets), and its link stops resolving.
Constraints, all from [`POST /v1/assets`](/docs/reference/api#assets): PNG images only, 10 MiB per upload, and 100 uploads or 100 MiB per session.
## Upload from a session
Every sandbox carries `ELLIPSIS_API_TOKEN`, so an agent uploads with plain `curl`:
```bash
curl -s https://api.ellipsis.dev/v1/assets \
-H "Authorization: Bearer $ELLIPSIS_API_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"filename\": \"schedule-grid.png\",
\"content_type\": \"image/png\",
\"data_b64\": \"$(base64 -i schedule-grid.png)\"
}"
```
```json
{
"asset": {
"id": "9f2c4b7d1e8a4c02b6f3a1d5e7c90842",
"filename": "schedule-grid.png",
"content_type": "image/png",
"size_bytes": 48213,
"created_at": "2026-07-06T14:09:31Z",
"agent_session_id": "session_7Hq2mX4p"
},
"url": "https://app.ellipsis.dev/assets/9f2c4b7d1e8a4c02b6f3a1d5e7c90842"
}
```
The `url` is what the agent pastes into a PR comment or description. Paste it as a link, not an inline image: the page behind it requires login, so GitHub cannot render it inline, and that is the point.
## An agent that screenshots its changes
A react agent that runs the app, captures the affected screen, and attaches the screenshot to the pull request it is reviewing. The browser comes from `image.setup`, so it is baked into the cached image rather than installed on every run:
```yaml
ellipsis:
version: v1
name: UI screenshotter
description: Posts a screenshot of the affected screen on splitshift-web PRs
trigger:
type: react
pull_request:
on: [opened]
repositories: [splitshift-web]
claude:
model: claude-haiku-4-5-20251001
system: |
When a pull request touches UI code in splitshift-web:
1. Check out the PR branch, install dependencies, and start the dev server.
2. Open the affected page with playwright and capture a PNG screenshot.
3. Upload it: POST it to $ELLIPSIS_API_BASE_URL/v1/assets as base64 JSON
(filename, content_type "image/png", data_b64), authorized with
$ELLIPSIS_API_TOKEN.
4. Comment on the pull request with a one-line description of what changed
visually and the asset url from the upload response.
If the PR touches no UI code, do nothing.
sandbox:
repositories:
- name: splitshift-web
image:
setup: |
cd splitshift-web && npm install
npx playwright install --with-deps chromium
budget:
session: 3.00
```
On a `splitshift-web` PR that restyles the schedule grid, the session ends with a PR comment like:
```text
Schedule grid after this change (weekly view, manager account):
https://app.ellipsis.dev/assets/9f2c4b7d1e8a4c02b6f3a1d5e7c90842
```
Reviewers who are members of the org click through and see the screenshot; the link stays valid after the sandbox is gone.
## Pull assets back down
Listing and downloading work from anywhere with a credential, not just inside sessions. `GET /v1/assets` lists metadata newest first (filter to one session with `agent_session_id`); `GET /v1/assets/{asset_id}` adds a `download_url`, a presigned link for the bytes that expires in 60 seconds:
```bash
curl -s "https://api.ellipsis.dev/v1/assets/9f2c4b7d1e8a4c02b6f3a1d5e7c90842" \
-H "Authorization: Bearer $ELLIPSIS_API_TOKEN" | jq -r .download_url | xargs curl -s -o schedule-grid.png
```
## Next
Endpoint details: [Assets](/docs/reference/api#assets) in the API reference. Installing tools like playwright into the image: [Set up your agent's cloud development environment](/docs/guides/set-up-your-agents-cloud-development-environment).
---
# 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.
Source: https://www.ellipsis.dev/docs/guides/set-up-your-agents-cloud-development-environment
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](/docs/concepts/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](/docs/reference/cli#authenticate)), 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](/docs/reference/api#get-an-api-key) as `ELLIPSIS_API_TOKEN` instead of logging in.
## Define the agent
Scaffold a config file, then edit it:
```bash
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:
```yaml
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.
```yaml fragment
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](#inject-credentials-with-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](#inject-credentials-with-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`](#bake-tools-and-dependencies-into-the-image) 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 `, or store from the dashboard or the API:
```bash
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:
```yaml fragment
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 --watch`.
- `--config ` 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 ` (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:
```bash
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](/docs/concepts/agents-as-code)) and the cron trigger takes over. Confirm it is live and watch it run:
```bash
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:
```yaml fragment
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:
```yaml fragment
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](/docs/concepts/budgets) 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:
```yaml
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:
```yaml fragment
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
- Every CLI command and flag: [CLI](/docs/reference/cli).
- What the sandbox is and what runs inside it: [The sandbox](/docs/concepts/sandbox).
- Load Claude Code skills into the sandbox: [Share skills across your team's agents and repositories](/docs/guides/share-skills-across-your-teams-agents-and-repositories).
- Start sessions from CI and scripts: [Spawn an agent session from the API and CLI](/docs/guides/spawn-an-agent-session-from-the-api-and-cli).
---
# Share skills across your team's agents and repositories
> Load Claude Code skills from your repositories, share one skill across agents, and pull skills from other repos.
Source: https://www.ellipsis.dev/docs/guides/share-skills-across-your-teams-agents-and-repositories
A skill is a directory with a `SKILL.md`: instructions and support files Claude Code loads by name when the task calls for them. Where the system prompt is what an agent must do every session, a skill is expertise it pulls in only when relevant, so one skill serves many agents without bloating any prompt.
Agents pick up skills two ways: automatically, from the `.claude/skills/` directory of every repository in the sandbox, and explicitly, from the config's `skills` list.
## Check skills into a repository
Every repository in the sandbox contributes its `.claude/skills/` at the session's checkout. No config change: add the directory, and every agent that clones the repository sees the skill.
A skill that teaches agents how `splitshift-api` migrations work, at `splitshift-api/.claude/skills/schema-migrations/SKILL.md`:
```md
---
name: schema-migrations
description: How schema migrations work in splitshift-api. Use when writing, reviewing, or debugging a migration.
---
Migrations are dated SQL files under db/migrations/, applied in filename
order by `make migrate`. Rules:
- Never edit an applied migration; fix forward with a new file.
- Every new table needs a `restaurant_id` column and an index on it.
- Run `make check-migrations` before opening a pull request.
```
The frontmatter `name` and `description` are what the agent sees at startup; it reads the body when the work matches the description. When the Linear issue implementer picks up a schedule-conflict bug that needs a schema change, it reads the skill, writes the migration as a dated file, and runs `make check-migrations` before opening the PR.
Because repository skills load at the session's checkout, skill and code move together: a PR session that edits both sees its own version of the skill.
## Declare skills in the config
The `skills` list installs skills the sandbox repositories don't provide: a skill elsewhere in the config's repository, a shared skills repository your whole org maintains, or a public repository. Each entry names a directory containing a `SKILL.md`:
```yaml
ellipsis:
version: v1
name: Linear issue implementer
description: Implements Linear issues as pull requests
claude:
system: |
Implement the Linear issue this session was started for. Open a
pull request and link the issue in its description.
trigger:
type: react
linear_issue:
on: [opened]
sandbox:
repositories:
- name: splitshift-api
skills:
- path: .agents/skills/pr-conventions
- path: skills/release-notes
repository:
name: platform-skills
```
The first entry resolves in the repository this config lives in. The second pulls `skills/release-notes` from `splitshift-hq/platform-skills`, a repository this sandbox never clones: that is how one skills repository serves every agent in the org. `repository` takes any repository of your installation, or a public repository from another owner (`owner: anthropics`); private repositories outside your account are rejected.
Config-declared skills also work for sessions with no repositories at all, where there is no `.claude/skills/` to load from.
## How skills resolve
Config-declared skills are fetched at session start, before the agent begins:
- A bare `path` resolves in the config's repository: at the session's checkout when that repository is in the sandbox, otherwise at the head of the branch the config syncs from.
- An entry with `repository` resolves there: at the session's checkout when that repository is in the sandbox, otherwise at `repository.ref` if set, otherwise at the head of its default branch.
- The files are installed at the sandbox's personal skill level (`~/.claude/skills/`) under the last segment of `path` (the entry `path: skills/release-notes` installs as `release-notes`). Personal skills take precedence over repository skills, so a config-declared skill deterministically overrides a same-named repository skill.
A skill that cannot be resolved fails the session before the agent starts: `status` is `error`, and the session's `status_reason` names the entry and what went wrong (no `SKILL.md` at the path, an unreachable repository, a limit exceeded). A session never runs with a silently missing skill.
## Limits
- At most 10 `skills` entries per config.
- Each skill: at most 50 files and 512 KiB total, 64 KiB per file, UTF-8 text only. Binary assets are not supported.
- Installed names must be unique: two entries whose paths end in the same directory name fail validation.
Sync and pull request validation check each bare-`path` entry's `SKILL.md` at the pushed commit, so a typo'd path or broken frontmatter fails before the config registers. Entries with `repository` are checked at session start.
## Next
- Every `skills` field, default, and constraint: [Agent config](/docs/reference/agent-config#skills).
- What else is in the sandbox skills land in: [The sandbox](/docs/concepts/sandbox).
---
# Spawn an agent session by mentioning @ellipsis
> Route mentions on GitHub, Slack, and Linear to an agent.
Source: https://www.ellipsis.dev/docs/guides/spawn-an-agent-session-by-mentioning-ellipsis
Mention `@ellipsis` where the work lives and Ellipsis answers in the thread, with the conversation as context and the session's tools available for code changes. This works with no config at all: a built-in responder answers every mention.
Add a `mention` trigger to take over mention handling with your own agent, controlling which sandbox repositories, model, and limits those sessions use:
```yaml
ellipsis:
version: v1
name: Mentions
description: Answers @ellipsis on GitHub, Slack, and Linear
trigger:
type: mention
platforms: [github, slack, linear]
sandbox:
repositories:
- name: splitshift-web
- name: splitshift-api
budget:
session: 2.00
```
A mention agent needs no `system` prompt: its response is built from the conversation it was mentioned in. If several enabled configs claim the same platform, the oldest one answers.
## Platforms
`platforms` scopes where this agent answers. Leave it empty to answer everywhere.
| Platform | Where it answers |
| --- | --- |
| `github` | Pull request and issue comments. |
| `slack` | App mentions, direct messages, and thread replies. |
| `linear` | Issue comments. |
## Where mentions work
| Surface | Context the session gets |
| --- | --- |
| GitHub pull request comments | The pull request, repository, and triggering comment. |
| GitHub issue comments | The issue, repository, and triggering comment. |
| Slack messages and app mentions | The Slack thread and any linked GitHub identity. |
| Linear issue comments | The Linear issue and comment thread. |
On GitHub, `@ellipsis` is detected anywhere in the comment. On Linear, the comment must lead with `@ellipsis` so a mirrored GitHub thread does not summon Ellipsis twice. On every platform, a mention starts a durable [conversation](#conversations) on its surface.
A GitHub mention adds the PR's or issue's repository to the sandbox automatically, so a PR comment mention can read that PR's code even if the config lists other repositories. Slack and Linear mentions have no repository of their own; those sessions clone the repositories the config declares.
## Conversations
A mention starts a durable conversation, not a single-shot session. The surface is the conversation: a Slack thread, a Slack DM, a GitHub pull request, a GitHub issue, or a Linear issue is one remembered conversation each. Every later mention on the same surface is delivered into it, and the agent keeps everything it already said and did as context.
How a follow-up reaches the conversation differs by platform. On Slack, any reply in a thread continues that thread's conversation, no repeat mention needed. A DM's top-level messages are their own continuous conversation, answered top-level like a person chatting; replying in a thread under any DM message forks a separate conversation for that thread, so several workstreams can run in parallel in one DM. On GitHub and Linear, a comment must mention `@ellipsis` again to be delivered; comments that do not are not seen by the mention agent.
A mention conversation never ends on its own: it goes idle between messages, and a later mention on the same surface picks it up again, even after the PR merges or the issue closes.
```text
@ellipsis why do expired shift trades still show as pending in the manager view?
```
After the agent answers in the Slack thread, a bare reply continues the conversation:
```text
fix it and open a PR
```
The follow-up needs no context restated: the agent already diagnosed the bug in this conversation and opens the PR from the same working tree.
How a conversation behaves, on every platform:
- A message into an active conversation is delivered within about a second; the agent attends to it at its next natural pause, exactly like typing to Claude Code while it works.
- Each turn posts its reply on the surface it came from: the Slack thread or DM, the PR or issue comment thread, the Linear issue. In a DM's top-level conversation the reply is a top-level message, and the emoji reaction lands on the message the turn answered.
- After a turn, the conversation stays warm for 10 minutes. While warm, the sandbox keeps running and its compute bills like any other sandbox time.
- After the window, state is snapshotted. The next message resumes the conversation, working tree and memory intact, in a fresh sandbox within seconds.
- Stopping a session stops the current turn only; the next message re-engages the same conversation.
- `limits.run` caps each burst of activity, and a resumed conversation runs on the budget remaining after prior spend. Trailing [spend limits](/docs/concepts/budgets) apply as always.
## While a mention runs
Ellipsis acknowledges a mention two ways while the session works: a 👀 reaction on the triggering comment or message, and a one-line "Working…" reply linking to the live session. When the session succeeds, the "Working…" message is deleted; the agent's answer is the lasting reply. When it fails, the message is updated in place with a short reason and keeps the session link. A session cancelled by a pre-execution check posts the same style of message directly. Either way, a failed mention never disappears silently.
On Slack, the "Working…" reply is a toggle (Agents settings, "Post a Working… link in Slack", on by default): turn it off to keep just the 👀 reaction in threads. Errors and cancellations post either way.
## Mention settings
The dashboard's Agents settings page controls who and what can summon Ellipsis on GitHub:
- **Author and repository matrix.** Human and bot authors are gated independently for public and private repositories. Human mentions trigger everywhere by default; bot-authored mentions (dependabot, renovate) are ignored by default, so a bot writing `@ellipsis` in a PR body does not spend your budget unless you opt in.
- **Leading `...` shorthand.** A GitHub comment that starts with `...` (or `…`) summons Ellipsis exactly like a leading `@ellipsis`. On by default; always leading-only, since `...` is far too common mid-sentence.
- **Require a leading mention.** By default `@ellipsis` triggers anywhere in a GitHub comment body. Turn this on to require the comment to start with the mention.
## Autocomplete on GitHub
GitHub's @-mention suggester only offers accounts with access to the repository, and app bots never appear in it. `@ellipsis` works without setup but does not autocomplete.
Two ways around that. The zero-setup one: start the comment with `...` instead of a mention. A leading `...` summons Ellipsis exactly like `@ellipsis` and needs no autocomplete at all (the shorthand is on by default; see [Mention settings](#mention-settings)):
```text
...why does the shift-trade expiry migration need a backfill?
```
The other: get real autocomplete by inviting the `ellipsis-agent` machine account to the repository as a read-only collaborator. Any repository admin can run:
```bash
gh api repos/splitshift-hq/splitshift-api/collaborators/ellipsis-agent -X PUT -f permission=pull
```
Ellipsis accepts the invitation within a minute. From then on, `@ellipsis-agent` autocompletes for everyone with access to the repository, and mentioning it starts a session the same way `@ellipsis` does.
- Collaborator access is per repository: run the command once for each repository where you want autocomplete.
- `ellipsis-agent` receives `pull` (read) access only.
- Ellipsis accepts invitations only from accounts with an active installation and declines everything else.
- On GitHub Team and Enterprise plans, an outside collaborator on a private repository uses a paid seat. Public repositories and Free-plan organizations are unaffected.
## What to ask
Mentions fit work that needs repository context or integration actions:
```text
@ellipsis why does the shift-trade expiry migration need a backfill?
```
```text
@ellipsis fix the double-booking race from this thread and open a PR
```
The first gets an answer in the thread grounded in the migration code. The second produces a reply linking the pull request the session opened.
## Next
Mentions are one of three trigger types; the same agent can also declare [cron](/docs/guides/spawn-an-agent-session-on-a-cron-schedule) and [react](/docs/guides/spawn-an-agent-session-in-response-to-pull-request-lifecycle-events) triggers.
---
# Spawn an agent session from the API and CLI
> Trigger and watch sessions from the API and CLI.
Source: https://www.ellipsis.dev/docs/guides/spawn-an-agent-session-from-the-api-and-cli
Triggers cover schedules, events, and mentions; everything else starts a session explicitly. The CLI and the REST API drive the same `/v1` surface, so anything shown here works from a laptop, a CI job, or your own service.
Authenticate non-interactive environments with an API key (created under **Platform → API keys** in the dashboard) exported as `ELLIPSIS_API_TOKEN`. The CLI and your own `curl` calls both read it.
## Start a session
From a saved agent, by config id:
```bash
agent session start --config agent_x9Kd3Fq2 --watch
```
The same over HTTP:
```bash
curl https://api.ellipsis.dev/v1/sessions \
-H "Authorization: Bearer $ELLIPSIS_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"config_id": "agent_x9Kd3Fq2", "metadata": {"trigger": "post-deploy"}}'
```
The response is the created session, including its `id` (a `session_...` identifier) and `status`. Sessions started this way record `source: api` (or `cli`), so they are distinguishable from cron and event sessions in history.
## Steer a single session
Two per-session knobs need no config edit:
- `prompt` appends session-specific instructions after the config's system prompt.
- `config_override_yaml` merges a partial config onto the saved one and re-validates it. Raising one session's budget:
```bash
curl https://api.ellipsis.dev/v1/sessions \
-H "Authorization: Bearer $ELLIPSIS_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"config_id": "agent_x9Kd3Fq2",
"prompt": "Focus on the trade-expiry changes merged this week.",
"config_override_yaml": "budget:\n session: 5.0"
}'
```
## Watch and fetch results
```bash
agent session get session_7Hq2mX4p --watch # follow until it finishes
agent session list --source api --days 7 # recent API-started sessions
```
Over HTTP, `GET /v1/sessions/{session_id}` returns the session with its status, output, and cost; the WebSocket at `/v1/sessions/{session_id}/stream` streams live output and supports resuming with `?after_seq=`. See the [REST API](/docs/reference/api#stream-a-session) for frame formats.
## Stop a session
Stop an in-flight session early:
```bash
agent session stop session_7Hq2mX4p
```
The same over HTTP:
```bash
curl https://api.ellipsis.dev/v1/sessions/session_7Hq2mX4p/stop \
-H "Authorization: Bearer $ELLIPSIS_API_TOKEN" \
-X POST
```
The session's `status` becomes `stopped`.
## Replay a session
Re-run a finished session, optionally with changes. Useful when a session hit its budget and you want a second attempt with more room:
```bash
curl https://api.ellipsis.dev/v1/sessions/session_7Hq2mX4p/replay \
-H "Authorization: Bearer $ELLIPSIS_API_TOKEN" \
-H "Content-Type: application/json" \
-X POST \
-d '{"config_override_yaml": "budget:\n session: 10.0"}'
```
By default a replay reuses the original session's config snapshot, so it reproduces the session as it was, not as the config is now.
## Get structured output
When a script consumes a session's result, free-text summaries are the wrong interface. Declare `structured_output` on the config and the session must exit through a submission matching your JSON schema:
```yaml fragment
structured_output:
json_schema:
type: object
properties:
unfilled_shifts:
type: integer
needs_attention:
type: boolean
required: [unfilled_shifts, needs_attention]
```
The agent cannot finish with prose: its exit is a `StructuredOutput` tool call validated against the schema, retried on validation failure, and a session that never produces a valid submission ends with exit status `tool_call_failed` instead of silently returning junk.
The submission is recorded as the session's last `StructuredOutput` tool call. Read it from the [steps endpoint](/docs/reference/api#read-a-sessions-steps):
```bash
curl -s https://api.ellipsis.dev/v1/sessions/session_7Hq2mX4p/steps \
-H "Authorization: Bearer $ELLIPSIS_API_TOKEN" |
jq '[.steps[] | select(.step_type == "assistant") | .data.message.content[]?
| select(.name? == "StructuredOutput")] | last.input'
```
```json
{
"unfilled_shifts": 4,
"needs_attention": true
}
```
A custom schema replaces the standard exit flow, so it belongs on API, CLI, cron, and react agents. Do not combine it with a mention trigger: a mention session's reply is built from the standard output, so a custom schema means no response is posted back to the thread.
## In CI
Store the API key as a CI secret exported as `ELLIPSIS_API_TOKEN` and run the same `curl` (or the CLI) as a pipeline step, e.g. starting a post-deploy audit agent with the deploy SHA in `metadata`. Sessions record who started them, so CI-started sessions stay attributable in history.
## Next
Everything the API exposes: [REST API](/docs/reference/api). Every CLI command: [CLI](/docs/reference/cli).
---
# Spawn an agent session in response to Pull Request lifecycle events
> Start sessions when pull requests open, push, merge, close, or receive reviews and comments.
Source: https://www.ellipsis.dev/docs/guides/spawn-an-agent-session-in-response-to-pull-request-lifecycle-events
A `react` trigger runs an agent when something happens on a pull request: it opens, the head advances, a review is submitted, a comment lands, it merges or closes. The agent acts on the event itself; there is no typed prompt, just the agent's `system` plus the event context.
Splitshift runs a describer on every new pull request:
```yaml
ellipsis:
version: v1
name: PR describer
description: Write a reviewer-ready description on every new pull request
claude:
system: |
Read the diff and the surrounding code. Rewrite the pull request
description: what changed, why it likely changed, and what a
reviewer should look at first. Keep the author's checklist items
if they wrote any. Update the description with gh.
trigger:
type: react
pull_request:
on: [opened]
sandbox:
repositories:
- name: splitshift-api
budget:
session: 1.00
day: 10.00
```
When `priya-shah` opens "Expire pending trades on account deactivation" with a one-line description, the session reads the diff and replaces it with a summary of the change, the motivation pulled from the linked issue, and the two files reviewers should start with.
## Surfaces
A react trigger sets exactly one surface block, and each config is one automation. Two surfaces cover the pull request lifecycle: `pull_request` reacts to the actions you list, and `code_review` follows the head as a review bot. Every surface carries only the filters that make sense for it.
React fires every matching config independently, so a reviewer, a describer, and a labeler can all run on the same pull request.
Actions do not each start a session. Everything an agent reacts to on one pull request is delivered into one durable [conversation](/docs/concepts/sessions#durable-conversations): the first action starts it, later ones continue it with full memory of what the agent already did, and a merge or close ends it. A react agent posts no reply per action; it acts through the sandbox.
### `pull_request`
`on` lists the pull request actions to react to:
| Action | Fires on |
| --- | --- |
| `opened` | A pull request is opened, reopened, or marked ready for review (draft or not; filter with `draft`). |
| `pushed` | The head advances. This includes the open, so `[pushed]` covers open plus every later commit. |
| `merged` | A pull request is merged. |
| `closed` | A pull request is closed without merging. |
| `review_submitted` | A review is submitted. |
| `commented` | A comment is added. |
Filters: `repositories` (the repos to watch, by name), `base` and `head` (branch names, prefixes ending in `*`, or `default`), `draft` (true or false), `labels` (any match), `paths` (changed-file globs, `**` allowed), and `for` (see [Who triggers it](#who-triggers-it)).
```yaml fragment
trigger:
type: react
pull_request:
on: [pushed]
base: [main]
paths: ["src/api/**"]
```
### `code_review`
`code_review` reviews the unreviewed part of a pull request on every head advance, and never re-comments lines it already reviewed. It is action-less: it fires when the head advances (open plus each push) and there are new commits past what the agent last reviewed. Use it for a review bot; use `pull_request: {on: [pushed]}` when you want to run something on each push (tests, a description update).
It reuses the `pull_request` filters minus `on`: `repositories`, `base`, `head`, `draft`, `labels`, `paths`, `for`.
```yaml fragment
trigger:
type: react
code_review:
base: [main]
for: { users: true, exclude_users: [priya-shah] }
```
## Scoping to a repository
`repositories` is the watch set: a list of repository names to react in. Owners default to the account. An empty list watches every repository. This is independent of `sandbox.repositories` — the repository that triggered the event is always cloned into the sandbox.
```yaml fragment
trigger:
type: react
pull_request:
on: [opened]
repositories: [splitshift-api]
```
## Who triggers it
`for` decides which authors trigger a config, classified on the pull request author. It is include minus exclude:
- `users` — `true` (all humans), a list of logins, or `false`/`[]` (none). Defaults to `true`.
- `bots` — same shape, for bot accounts. Defaults to `false`, so automation like Dependabot never triggers by accident.
- `exclude_users` / `exclude_bots` — logins to subtract from the include set.
Because react fires every matching config, `for` is how you make two configs mutually exclusive: a general reviewer `for: { users: true, exclude_users: [priya-shah] }` and a per-author reviewer `for: { users: [priya-shah] }` never both fire on one pull request.
## Next
Answer conversational requests instead of events with [mentions](/docs/guides/spawn-an-agent-session-by-mentioning-ellipsis), or see [how a session executes](/docs/concepts/sessions) for what happens after the trigger fires.
---
# Spawn an agent session on a cron schedule
> Cron triggers, schedule syntax, and multiple schedules per agent.
Source: https://www.ellipsis.dev/docs/guides/spawn-an-agent-session-on-a-cron-schedule
A `cron` trigger runs an agent on a schedule. Ellipsis registers the schedule when the config syncs from your default branch, and each fire starts a fresh session with the agent's `system` as its instruction. There is no typed prompt.
A daily triage agent for Splitshift's API team:
```yaml
ellipsis:
version: v1
name: Daily issue triage
description: Triages new issues every weekday morning
claude:
system: |
Find open GitHub issues in splitshift-api that need attention.
Prioritize bugs in shift trading and schedule assignment, stale
customer reports, and blocked pull requests. Post a concise
triage summary as a comment on each issue you touch.
trigger:
type: cron
schedule: "0 14 * * 1-5"
sandbox:
repositories:
- name: splitshift-api
budget:
session: 1.00
day: 2.00
```
Each weekday at 14:00 UTC this produces a session whose summary reads like:
```text
Triaged 6 issues. Flagged #431 (trade requests stuck in pending
when the counterparty account is deactivated) as the priority:
likely regression from #418. Labeled 3 as needs-repro, closed 2
duplicates of #405.
```
For a complete, deployable example of the pattern, use the [Daily Standup template](/agents/templates/daily-standup): a cron agent with a read-only GitHub token that turns yesterday's merged pull requests into a product-level summary each weekday morning. [Automating your daily standup with Ellipsis](/blog/automating-your-daily-standup-with-ellipsis) walks through how it works.
## Schedule syntax
`schedule` takes a standard five-field cron expression: `minute hour day-of-month month day-of-week`. All schedules run in UTC.
| Expression | Runs |
| --- | --- |
| `0 14 * * 1-5` | 14:00 UTC, Monday through Friday. |
| `0 6 * * *` | 06:00 UTC every day. |
| `0 9 * * 1` | 09:00 UTC every Monday. |
You cannot restrict both day-of-month and day-of-week in the same expression; leave one as `*`.
Ellipsis also accepts AWS EventBridge Scheduler expressions verbatim: `cron(...)`, `rate(...)`, or `at(...)`.
```yaml fragment
trigger:
type: cron
schedule: "rate(6 hours)"
```
## Multiple schedules
A config can declare several cron triggers; each fires independently. Editing or removing a trigger updates the schedule on the next sync, and an invalid cron expression fails validation before anything is stored.
## Next
Cap what scheduled sessions can spend with [budgets](/docs/concepts/budgets), or mix a cron trigger with [react triggers](/docs/guides/spawn-an-agent-session-in-response-to-pull-request-lifecycle-events) in the same agent.
---
# Sync laptop Claude Code sessions into one searchable history
> Ingest Claude Code sessions from developer laptops into Ellipsis for one searchable history, linked pull requests, insights, and laptop-to-cloud handoff.
Source: https://www.ellipsis.dev/docs/guides/sync-laptop-claude-code-sessions-into-one-searchable-history
Agents run in the Ellipsis cloud, but plenty of AI work happens in Claude Code on a laptop. Session sync ingests that work too: enroll a repository, and every Claude Code session you run in it lands in Ellipsis as a session with `source: laptop`, next to your cloud sessions. The result is one history for all agent work, searchable, attributed, and linked to the pull requests it produced, whether the session ran in a sandbox or on someone's machine.
Sync is built into the [CLI](/docs/reference/cli) and driven by Claude Code's own lifecycle hooks. Nothing changes about how you use Claude Code.
## Set up sync
Install the CLI and log in. Sync requires a logged-in user (not an API key) so every synced session attributes to the developer who ran it.
```bash
brew install ellipsis-dev/cli/agent
agent login
```
Install the hooks:
```bash
agent hooks install
```
```text
Installed Stop + SessionEnd hooks in /Users/priya/.claude/settings.json.
No repositories enrolled yet — nothing will sync. Run `agent hooks enroll` inside a repo to opt it in.
```
Then opt in the repository you work in, from inside it:
```bash
cd ~/code/splitshift-api
agent hooks enroll
```
```text
Enrolled splitshift-hq/splitshift-api. Claude Code sessions in this repo will sync.
```
`agent hooks install` writes a `Stop` and a `SessionEnd` hook into `~/.claude/settings.json`, each running `agent session sync` in the background. Installation is idempotent, preserves hooks you wrote yourself, and `agent hooks uninstall` removes exactly what it added.
Consent is per repository, never account-wide: installing the hooks alone syncs nothing. `agent hooks enroll` derives `owner/name` from the current directory's `origin` remote (or takes it explicitly: `agent hooks enroll splitshift-hq/splitshift-web`), and sessions in repositories you have not enrolled are silently skipped. `agent hooks unenroll` opts a repository back out.
Confirm the setup:
```bash
agent hooks status
```
```text
HOOK INSTALLED
Stop yes
SessionEnd yes
ENROLLED REPOSITORY
splitshift-hq/splitshift-api
```
## How sync works
Claude Code fires the `Stop` hook at the end of every turn and `SessionEnd` when the session terminates. On each firing, `agent session sync` reads the session's on-disk transcript, redacts it, compresses it, and uploads it. One Claude Code session becomes exactly one Ellipsis session:
- A mid-session sync creates the session with status `running`, so a live laptop session is visible on the Sessions page while you work, with a one-line summary of what it is working on.
- The session-end sync marks it `completed` and triggers a recap: a written summary, a judged outcome, and links to any pull requests it created.
- `claude --resume` reopens the same Ellipsis session and later syncs extend it. Uploads are idempotent (the longest transcript snapshot wins), so repeated syncs across `/clear` and `--resume` never duplicate a session.
A sync that fails never disturbs the Claude Code session: the hooks run in the background, stay quiet on every failure, and spool the transcript to disk when the API is unreachable. The next successful sync flushes anything spooled. One upload caps at 8 MiB compressed (64 MiB uncompressed).
## What leaves your laptop
Three properties bound what sync can expose:
- **Only enrolled repositories.** Sessions anywhere else are skipped before anything is read.
- **Secrets are redacted on your machine.** Before upload, recognizable credential shapes in the transcript are replaced with `[REDACTED]`: GitHub tokens, AWS keys, Anthropic and OpenAI keys, Slack tokens and webhook URLs, Stripe, npm, and PyPI tokens, JWTs, and private key blocks. Redaction happens client-side; the unredacted transcript never leaves the laptop.
- **Sessions belong to their developer.** Attribution comes from your `agent login` identity (your GitHub account), the same identity as your GitHub-triggered work, and a teammate's sync can never overwrite your session.
The stored transcript lives in your account's namespace alongside cloud session transcripts and follows the same log retention setting.
## What you get
Once synced, a laptop session is a first-class session everywhere Ellipsis shows agent work:
- **One history.** The dashboard Sessions page and `agent session list --source laptop` show laptop sessions beside cron, react, mention, and API sessions, attributed to their developer, with the model the session actually ran.
- **Full steps.** The session detail page renders the step timeline; `agent session steps` and [`GET /v1/sessions/{session_id}/steps`](/docs/reference/api#read-a-sessions-steps) return the same steps, and `agent session transcript` downloads the raw `.jsonl`. Per-step tokens and Claude Code's reported cost come along.
- **Search that answers "did anyone look into this?"** [Session search](/docs/reference/api#search-sessions) covers laptop transcripts with all four arms, including semantic similarity over recaps. `agent session search "rest-period validator" --author priya` finds the laptop session where Priya investigated it, even if she never pushed a commit.
- **Pull request links.** A PR opened with `gh pr create` inside a local session is extracted onto the session, so searching `splitshift-hq/splitshift-api#512` finds the laptop session that created that PR.
- **Insights.** Laptop sessions feed the sessions insights dashboard: volume and outcomes by source, and the tool and CLI leaderboards built from what sessions actually invoked.
## Hand a session off to the cloud
A synced session can move to the cloud mid-task: `agent session handoff` snapshots your working tree as a commit without touching it (uncommitted changes included), pushes it to a hidden `refs/ellipsis/handoff/*` ref, and starts a cloud session checked out at that exact state, chained to the laptop session it continues.
```bash
agent session handoff "Finish the rest-period validator; tests fail on overnight shifts" \
--parent session_3fKq8Zw1
```
```text
✓ pushed working-tree snapshot 4c9d2ab871e0 to refs/ellipsis/handoff/4c9d2ab871e0
✓ started handoff session session_9Lw4tR6c (scheduled)
https://app.ellipsis.dev/splitshift-hq/sessions/session_9Lw4tR6c
follow with: agent session get session_9Lw4tR6c --watch
```
`--parent` is the synced laptop session to continue (find it with `agent session list --source laptop`). The cloud session runs the built-in Handoff agent with a $10 budget: it orients on the WIP commit, creates a working branch, follows your handoff note, and typically finishes by opening a pull request. You need push permission on the repository, since the snapshot is pushed as a git ref.
Close the laptop; the agent finishes the work.
## Monitor and troubleshoot
Background hooks are deliberately silent inside Claude Code, so the CLI keeps a local activity log instead:
```bash
agent hooks logs -n 2
```
```text
TIME OUTCOME REPO REASON DETAIL
2026-07-06 14:02 synced splitshift-hq/splitshift-api stop 41 events → session_3fKq8Zw1
2026-07-06 14:11 synced splitshift-hq/splitshift-api session_end 63 events → session_3fKq8Zw1
```
Every attempt records one of six outcomes:
| Outcome | Meaning |
| --- | --- |
| `synced` | Delivered; the detail column shows the event count and session id. |
| `skipped_unenrolled` | The repository is not enrolled (or has no git remote). Enroll it to start syncing. |
| `not_logged_in` | No credential found. Run `agent login`. |
| `no_transcript` | The transcript path from the hook was missing or empty. |
| `spooled` | A network or server failure; the snapshot is queued and retried on the next sync. |
| `rejected` | A permanent rejection (for example, over the size cap); never retried. |
`agent hooks logs --failures` filters to problems, and `agent hooks stats` summarizes: last sync, 24-hour counts, spooled backlog, and recent session ids (also available as plain JSON in `~/.config/ellipsis/hooks/stats.json` for scripts and status bars).
## Turn it off
```bash
agent hooks unenroll # stop syncing one repository
agent hooks uninstall # remove the hooks entirely; enrollment is kept
```
## Next
How sessions store steps, recaps, and costs: [How a session executes](/docs/concepts/sessions). The underlying endpoint, for building your own uploader: [`POST /v1/sessions/sync`](/docs/reference/api#sync-a-laptop-session).
---
# Agent config
> Every field, default, and constraint in agent config YAML files.
Source: https://www.ellipsis.dev/docs/reference/agent-config
The field-by-field reference for Ellipsis agent YAML files. The canonical path is `agents/*.yaml`; `.agents/`, `ellipsis/`, and `.ellipsis/` are also accepted, including any subdirectory under them. The machine-readable source of truth is `GET /agents/schema`.
Every config file must declare a top-level `ellipsis:` mapping. Its presence is what identifies the file as an Ellipsis agent config: a `.yaml`/`.yml` file in a config directory without an `ellipsis:` block is treated as unrelated YAML and ignored.
For how agents deploy and sync, see [Agents as code](/docs/concepts/agents-as-code); for trigger behavior, the [schedule](/docs/guides/spawn-an-agent-session-on-a-cron-schedule), [react](/docs/guides/spawn-an-agent-session-in-response-to-pull-request-lifecycle-events), and [mention](/docs/guides/spawn-an-agent-session-by-mentioning-ellipsis) guides; for spend, [Budgets](/docs/concepts/budgets). This page documents the fields.
## Top-level fields
The shape of an agent file. Every field has a default; a useful agent sets a `trigger` and a `claude.system` prompt (mention agents need neither).
| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `ellipsis` | object | required | Ellipsis namespace: schema version, name/description, organizational metadata, and the enabled/interactive/ide flags. Its presence marks the file as an agent config. |
| `claude` | object | {} | Claude runtime config: the system prompt, model, reasoning effort, turn cap, and an optional Claude Code settings file. Optional for mention-only agents. |
| `trigger` | trigger object \| null | null | How the agent runs automatically. One trigger of type cron, react, or mention. Omit for a manual-only agent. |
| `sandbox` | object | {} | The sandbox the agent runs in: which repositories are checked out, which stored environment variables are injected, exposed ports, compute sizing, image customization, lifecycle hooks, and the scope of its GitHub token. |
| `skills` | list of skill objects | [] | Claude Code skills installed for the session beyond what the sandbox repositories already provide. Skills checked into a cloned repository's .claude/skills/ load automatically and need no entry here. |
| `structured_output` | object \| null | null | JSON Schema contract for machine-readable run output. |
| `budget` | object | all null | Per-session and trailing spend caps for this agent, in US dollars. |
## ellipsis
The Ellipsis namespace: schema version, identity, organizational metadata, and the enabled/interactive/ide flags. Its presence is what marks a YAML file as an agent config; a config-path file without it is ignored.
| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `version` | string | v1 | Config schema version. Bumped only on a breaking shape change; an unsupported value fails validation. v1 is the only supported version today. |
| `name` | string \| null | null | Display name. Dashboard-created files slug this into the file path. |
| `description` | string \| null | null | Short summary shown in product surfaces. |
| `enabled` | boolean | true | When false, cron, react, and mention starts are skipped. Manual runs still work. |
| `interactive` | boolean | true | When true, a human can `agent session connect` to a live session and inject messages into it over the CLI relay, and an on-demand session stays warm as a durable conversation after its first turn. Set false for fire-and-forget automations. React and cron sessions are always single-turn regardless. |
| `ide` | boolean | true | When true, a human can open an editor or a port URL into the running sandbox. |
| `metadata` | object | {} | Freeform labels and annotations. Does not affect how the agent runs; excluded from the config content identity. |
### ellipsis.metadata
Freeform organization, modeled on Kubernetes labels and annotations. It has no effect on how the agent runs and is excluded from the config content identity.
| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `labels` | list of strings | [] | Flat string tags for grouping and filtering. |
| `annotations` | object | {} | Arbitrary key-value pairs. |
## claude
| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `system` | string \| {file} \| list of both | "" | The agent's instructions: inline text, a {file: ...} reference to a repository file, or an ordered list interleaving both, joined into one prompt at run start and appended to Claude Code's default system prompt (never replacing it). Required (non-empty) for cron, react, and manual agents; ignored for mention agents, whose reply is built from the conversation. |
| `system[].file` | string | required | Path of a UTF-8 text file in the repository the config lives in, relative to the repository root (no leading /, no ..). Read at run start at the run's commit; 64 KiB max per file and for the composed prompt. |
| `model` | string | claude-opus-4-8 | Model passed to Claude Code. Omit unless Ellipsis gave your account explicit guidance. |
| `effort` | enum \| null | null | Reasoning effort passed to Claude Code. Higher effort trades latency and cost for depth. Omit to use the model default. |
| `fallback_model` | string \| null | null | Model Claude Code falls back to when the primary model is overloaded. Omit for no fallback. |
| `max_turns` | integer \| null | null | Hard cap on the number of agent turns. Must be a positive integer. The session budget bounds the run independently; use this to stop a loop by turn count. |
| `settings` | object \| null | null | Point the session at a Claude Code settings.json in a repository. It is applied below Ellipsis-managed settings, so it customizes behavior (permissions, commit co-author trailer, status line) but cannot weaken a managed setting; hooks stay disabled. Environment keys that would redirect the model off the Ellipsis proxy (ANTHROPIC_*, ELLIPSIS_*, provider routing, apiKeyHelper) are rejected; use sandbox.variables for your own environment variables. |
| `settings.path` | string | required | Path of the settings.json, relative to the repository root (no leading /, no ..). Read at session start; must be a UTF-8 JSON object, 64 KiB max. |
| `settings.repository` | object \| null | null | Where path resolves. Omitted: the repository this config lives in, at the session's checkout when that repository is in the sandbox, otherwise at the head of the config's branch. Set: the named repository, which may be any repository of your installation or a public repository from another owner (external private repositories are rejected). |
| `settings.repository.name` | string | required | The repository name, without the owner. |
| `settings.repository.owner` | string \| null | the config account | GitHub owner (organization or user login). Defaults to the account the config belongs to. |
| `settings.repository.ref` | string \| null | default-branch head | Branch, tag, or commit SHA to fetch the settings file at. When the repository is cloned into the session's sandbox the checkout SHA wins; otherwise an omitted ref resolves the default-branch head at session start. |
Allowed `effort` values:
| Value | Levels |
| --- | --- |
| `low` | Fastest, cheapest. |
| `medium` | Moderate reasoning. |
| `high` | More reasoning for harder tasks. |
| `xhigh` | Very high reasoning. |
| `max` | Maximum reasoning; slowest. |
settings is applied below Ellipsis-managed settings and cannot weaken them. An unresolvable settings file (missing, not a JSON object, over 64 KiB, or setting a protected environment key) fails the session before the agent starts.
### System prompts from repository files
`system` takes three shapes: inline text, a single file reference, or an ordered list interleaving both. A file reference points at a UTF-8 text file in the repository the config lives in, by a path relative to the repository root:
```yaml fragment
claude:
system:
file: .agents/prompts/release-notes.md
```
A list composes the prompt from its parts in order. Each part is trimmed, and parts are joined with a blank line:
```yaml
ellipsis:
version: v1
name: Linear issue implementer
description: Implements Linear issues as pull requests
claude:
system:
- file: .agents/prompts/engineering-standards.md
- |
Implement the Linear issue this session was started for. Open a
pull request and link the issue in its description.
trigger:
type: react
linear_issue:
on: [opened]
sandbox:
repositories:
- name: splitshift-api
- name: splitshift-web
```
Referenced files are read at session start, at the commit the session has the config's repository checked out at, so prompt and code always move together. Every agent that references `.agents/prompts/engineering-standards.md` picks up an edit to it on its next session: no config change, no sync. When the config's repository is not in the session's sandbox, files resolve at the head of the branch the config syncs from.
Constraints:
- A path must be relative to the repository root and normalized: no leading `/`, no `\`, no `..` escaping the root.
- A referenced file must exist at the resolved commit, be valid UTF-8, be non-empty, and decode to at most 64 KiB. The composed prompt is capped at 64 KiB total.
- A file reference must not point at another agent config file.
Sync and pull request validation check every referenced file at the pushed commit. A file that breaks a constraint at session start (deleted, emptied, grown past the limit) fails the session: `status` is `error`, with the reason in the session's `status_reason`.
### Settings file
`claude.settings` points the session at a Claude Code `settings.json` in a repository, so a team can reuse the settings file it runs locally instead of re-encoding it. Ellipsis passes it to Claude Code as `--settings`, which applies below Ellipsis-managed settings: it can set permissions, the commit co-author trailer, a status line, and other Claude Code options, but cannot override a managed setting, and hooks stay disabled.
```yaml fragment
claude:
system: Review the push and draft release notes.
settings:
path: .claude/settings.json
```
Omit `repository` to resolve the path in the repository the config lives in, at the session's checkout when that repository is in the sandbox. Set it to pull a shared file from another repository of your installation or a public repository:
```yaml fragment
claude:
system: Review the push and draft release notes.
settings:
path: claude/settings.json
repository:
name: platform-config
```
Constraints:
- The file is read at session start, must be a UTF-8 JSON object, and decodes to at most 64 KiB.
- Environment keys that would redirect the model off the Ellipsis proxy (`ANTHROPIC_*`, `ELLIPSIS_*`, provider-routing variables, `apiKeyHelper`) are rejected. Use `sandbox.variables` for your own environment variables.
- An unresolvable settings file (missing, not a JSON object, over the limit, or setting a protected key) fails the session before the agent starts: `status` is `error`, with the reason in `status_reason`.
## triggers
An agent can declare zero or more triggers; each independently decides when the agent runs. A config with no triggers still runs on demand from the CLI, API, or dashboard.
### Cron trigger
Run the agent on a schedule. See [Spawn an agent session on a cron schedule](/docs/guides/spawn-an-agent-session-on-a-cron-schedule).
Run the agent on a schedule.
| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `type` | "cron" | required | Selects the cron trigger shape. |
| `schedule` | string | required | Five-field cron, or an EventBridge cron(...), rate(...), or at(...) expression. Five-field cron is converted to EventBridge format. |
EventBridge Scheduler cannot restrict both day-of-month and day-of-week in one expression.
### React trigger
Run the agent when a pull request, issue, push, Linear, Sentry, or Slack event happens. For the pull request surfaces, see [Spawn an agent session in response to Pull Request lifecycle events](/docs/guides/spawn-an-agent-session-in-response-to-pull-request-lifecycle-events); the fields below cover every surface.
Run the agent in response to a repository, issue, Sentry, or Slack event. A react trigger sets exactly one surface block (pull_request, push, code_review, issue, linear_issue, sentry, or slack_channel); each block carries its own action list and filters.
| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `type` | "react" | required | Selects the react trigger shape. |
| `pull_request` | object \| null | null | React to pull-request lifecycle actions. Filters: repositories, base, head, draft, labels, paths, for. |
| `pull_request.on` | list of pull-request actions | required | Pull-request actions that can start the agent. |
| `push` | object \| null | null | React to branch pushes. Action-less. Filters: repositories, branch, paths, for. |
| `code_review` | object \| null | null | Review the unreviewed delta on every head advance of a matched PR, never re-commenting reviewed lines. Action-less; reuses the pull_request filters minus on: repositories, base, head, draft, labels, paths, for. |
| `issue` | object \| null | null | React to GitHub issue actions. Filters: repositories, labels, for. |
| `issue.on` | list of issue actions | required | GitHub issue actions that can start the agent. |
| `linear_issue` | object \| null | null | React to Linear issue actions. Filter: for. |
| `linear_issue.on` | list of Linear issue actions | [opened] | Linear issue actions that can start the agent. |
| `sentry` | object \| null | null | React to Sentry alerts. Filter: projects (Sentry project slugs; empty matches every project in the connected Sentry org). |
| `sentry.on` | list of Sentry actions | required | Sentry alert kinds that can start the agent. |
| `slack_channel` | object \| null | null | React when a new public channel is created in your Slack workspace. Takes no fields. |
| `repositories` | list of strings | [] | Repository name filter (pull_request, push, code_review, issue). Empty matches every repository of the installation. |
| `base` | list of strings | [] | Base-branch filter (pull_request, code_review). Exact names or prefixes ending in *. Empty matches any base. |
| `head` | list of strings | [] | Head-branch filter (pull_request, code_review). Exact names or prefixes ending in *. Empty matches any head. |
| `branch` | list of strings | [] | Branch filter for push. Exact names or prefixes ending in *. Empty matches every branch. |
| `draft` | boolean \| null | null | Draft filter (pull_request, code_review). true matches only draft PRs, false only non-draft; null matches both. |
| `labels` | list of strings | [] | Label filter (pull_request, code_review, issue). Empty matches regardless of labels. |
| `paths` | list of strings | [] | Changed-path glob filter (pull_request, push, code_review). Empty matches regardless of paths. |
| `projects` | list of strings | [] | Sentry project slugs to react to (sentry). Empty matches every project in the connected Sentry org. |
| `for` | audience object | {users: true} | Who may trigger the surface, classified on the stable entity author (the pusher for push). An author matches if in the include set (users/bots) and in no exclude set. |
| `for.users` | list of strings \| boolean | true | Human authors. true is all humans, a list is specific logins, [] or false is none. |
| `for.bots` | list of strings \| boolean | false | Bot authors. true is all bots, a list is specific logins, [] or false is none. |
| `for.exclude_users` | list of strings | [] | Human logins to exclude, applied after the include set. |
| `for.exclude_bots` | list of strings | [] | Bot logins to exclude, applied after the include set. |
Allowed `pull_request.on` values:
| Value | Fires for |
| --- | --- |
| `opened` | PR opened or marked ready. |
| `pushed` | New commits pushed to the PR. |
| `merged` | PR merged. |
| `closed` | PR closed without merge. |
| `review_submitted` | A review was submitted on the PR. |
| `commented` | A comment was added to the PR. |
Allowed `issue.on` values:
| Value | Fires for |
| --- | --- |
| `opened` | Issue opened. |
| `closed` | Issue closed. |
| `commented` | Comment added to the issue. |
Allowed `linear_issue.on` values:
| Value | Fires for |
| --- | --- |
| `opened` | Linear issue opened. |
Allowed `sentry.on` values:
| Value | Fires for |
| --- | --- |
| `issue_alert` | A Sentry issue alert rule fired (via the Ellipsis alert rule action). |
| `metric_alert` | A Sentry metric alert entered critical status. |
Exactly one surface block may be set on a react trigger.
### Mention trigger
Route `@ellipsis` mentions to this agent instead of the built-in responder. See [Spawn an agent session by mentioning @ellipsis](/docs/guides/spawn-an-agent-session-by-mentioning-ellipsis).
Route @ellipsis mentions to this agent instead of the built-in responder.
| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `type` | "mention" | required | Selects the mention trigger shape. |
| `platforms` | list of platform strings | [] | Surfaces this agent answers @ellipsis on. Empty means all of them. |
Allowed `platforms` values:
| Value | Covers |
| --- | --- |
| `github` | @ellipsis in GitHub PR and issue comments and reviews. |
| `slack` | @ellipsis messages and app mentions in Slack. |
| `linear` | @ellipsis in Linear issue comments. |
One agent answers per platform. If two configs claim the same platform the oldest one wins; the rest are ignored. With no claimant, the built-in Ellipsis responder answers.
## sandbox
The sandbox the agent runs in. See [The sandbox](/docs/concepts/sandbox) for the environment and [Inject credentials with sandbox variables](/docs/guides/set-up-your-agents-cloud-development-environment#inject-credentials-with-sandbox-variables) for variables.
The sandbox the agent runs in. Each repository entry takes name (required), owner (defaults to the config account), and ref (defaults to the default-branch head).
| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `repositories` | list of repository objects | [] | Repositories checked out in the sandbox. Each takes name (required), owner, and ref. |
| `repositories[].name` | string | required | The repository name, without the owner. |
| `repositories[].owner` | string | the config account | GitHub owner (organization or user login). Defaults to the account the config belongs to. Public repositories from other owners work; private repositories outside the owning account are rejected. |
| `repositories[].ref` | string | default-branch head | Pins the checkout: fetched and checked out verbatim (git fetch origin [ && git checkout ][), so use a commit SHA for a reproducible checkout. When omitted, the default branch head at session start. |
| `variables` | list of variable objects | [] | Environment variables injected into the sandbox (an allowlist — only the variables named here reach this agent). Each entry takes a name and an optional value. |
| `ports` | list of integers | [3000, 5173, 8000, 8080] | TCP ports exposed from the sandbox for IDE and port-URL access. |
| `variables[].name` | string | required | The environment variable name. Must be a valid shell name (letters, digits, underscores; not starting with a digit). |
| `variables[].value` | string \| null | null | A literal value injected as-is — use for non-secret config like a log level or API base URL. Omit it to resolve the value by name from your sandbox-variables store (dashboard or PUT /sandboxes/variables) at run time, keeping secrets out of YAML. |
| `image` | object | {} | Customizes the container image. Ellipsis owns the base image (FROM, user, entrypoint, and preinstalled tooling); you contribute appended layers (dockerfile_append) and a build-time script (setup). |
| `image.dockerfile_append` | string \| null | null | RUN instructions appended onto the base image, e.g. installing a CLI. Runs when the image is assembled, before any repository exists. Only RUN is supported — FROM, USER, WORKDIR, ENTRYPOINT, and COPY are not. Changing it rebuilds and re-caches the image. |
| `image.setup` | string \| null | null | A shell script run once at image-build time, after your repositories are checked out and before the snapshot that becomes the cached image. Use it for dependency installs (npm install, poetry install): later runs start with the results already on disk, and a new commit re-runs it on top of the previous build (installing only the delta). Runs as the sandbox user with the sandbox variables available. Environment variables are not captured by the snapshot, but files are, so it must never write secrets to disk. A non-zero exit fails the run; capped at 10 minutes. |
| `hooks` | object | {} | Shell scripts run on every run, before the agent starts, each as the sandbox user with the sandbox variables available. Their output is never cached; a dependency install whose result should be reused across runs belongs in image.setup. A hook that exits non-zero fails the run. |
| `hooks.post_start` | string \| null | null | Runs after the container starts, before any repository is cloned. Use for repo-independent setup such as authenticating a CLI (e.g. doppler setup). Must not depend on repository contents. |
| `hooks.post_clone` | string \| null | null | Runs after all repositories are cloned and checked out, before the agent starts. Use for per-run, repo-dependent setup such as code generation or anything that touches run-scoped credentials. For dependency installs, prefer image.setup so the result is cached. |
| `compute` | object | {} | Sandbox compute sizing. Every field is optional; an omitted field uses the platform default. A value outside the allowed range fails config validation, it is never clamped. Compute is billed on the requested allocation over the sandbox lifetime, so a bigger or longer sandbox costs proportionally more. |
| `compute.cpu` | number \| null | 1 | vCPU cores, 0.125 to 16. Fractional values are allowed (e.g. 0.5). |
| `compute.memory` | string \| object \| null | 4GB | Memory allocation, 512MB to 64GB. Either a size string ("16GB", "512MB"; units are binary, so 1GB is 1024MB) or an object with gb / mb keys. Omit to use the default. |
| `compute.timeout` | string \| object \| null | 1h | Maximum session wall clock, 60s to 1h. Either a duration string ("15m", "1h", "90s", or combined like "1h30m") or an object with hours / minutes / seconds keys. One hour is both the default and the maximum (a sandbox and its GitHub token live at most one hour), so this only shortens a session. A session that reaches the timeout has its sandbox killed and fails. |
| `github_token` | object | {} | Scopes the GitHub installation token injected into the sandbox as GH_TOKEN (the credential git and gh authenticate with). By default the token carries the full permissions of your installation, scoped to the repositories in the sandbox. Narrowing is enforced by GitHub itself: the token is minted with the reduced scope, so nothing running in the sandbox can exceed it. |
| `github_token.permissions` | 'read_only' \| map \| null | null | What the token may do. read_only grants read access to contents, issues, metadata, and pull_requests. A map requests an explicit level per GitHub App permission scope (for example contents: read, pull_requests: write) and can only reduce what the installation granted: requesting an ungranted permission fails the session with exit status missing_token_permissions. Omit for full installation permissions. |
| `github_token.repositories` | list of strings \| null | null | Repository names the token may touch, narrowing the default (the sandbox repositories owned by your account). Names must belong to the installation; a name it lacks access to fails the session with exit status missing_repo_access. Public repositories from other owners are never in the token scope. |
Ellipsis clones repositories available to the account installation, and can clone public repositories from other owners. It rejects private repositories outside the owning account. A variable with no value and no stored value fails the run, so define it before referencing it. A lifecycle hook that exits non-zero fails the run.
## skills
Claude Code skills installed for the session, beyond the `.claude/skills/` directories the sandbox repositories already provide. See [Share skills across your team's agents and repositories](/docs/guides/share-skills-across-your-teams-agents-and-repositories).
Claude Code skills installed for the session. Skills checked into a sandbox repository's .claude/skills/ load automatically at that session's checkout; a skills entry brings in skills those clones don't provide: another repository of your installation, a public repository, or a session with no repositories at all. Each entry names a repository directory containing a SKILL.md; it is fetched at session start and installed at the sandbox's personal skill level (~/.claude/skills//), where it overrides a same-named repository skill.
| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `path` | string | required | Directory containing the SKILL.md, relative to the repository root (no leading /, no ..; not the root itself). The last path segment becomes the installed skill name, so it must be unique across the skills list. |
| `repository` | object \| null | null | Where path resolves. Omitted: the repository this config lives in, at the session's checkout when that repository is in the sandbox, otherwise at the head of the config's branch. Set: the named repository, which may be any repository of your installation or a public repository from another owner (external private repositories are rejected). |
| `repository.name` | string | required | The repository name, without the owner. |
| `repository.owner` | string \| null | the config account | GitHub owner (organization or user login). Defaults to the account the config belongs to. |
| `repository.ref` | string \| null | default-branch head | Branch, tag, or commit SHA to fetch the skill at. When the repository is cloned into the session's sandbox the checkout SHA wins, so skill and code move together; otherwise an omitted ref resolves the default-branch head at session start. |
At most 10 skills per config; each skill at most 50 files and 512 KiB total, 64 KiB per file, UTF-8 text only. A skill that cannot be resolved (missing SKILL.md, unreachable repository, over a limit) fails the session before the agent starts.
## structured_output
| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `type` | "json_schema" | "json_schema" | Structured output type. |
| `json_schema` | object | required | JSON Schema the agent output must satisfy. |
When present, Ellipsis passes this schema to the Claude Code runtime for the run's output.
## limits
Per-session and trailing spend limits, in US dollars. See [Budgets](/docs/concepts/budgets) for defaults and ceilings.
## Validation and sync behavior
| Case | Result |
| --- | --- |
| File has no top-level `ellipsis:` block | Not an agent config: ignored entirely, even at a config path. |
| `ellipsis.version` is unsupported | Validation fails. Only `v1` exists. |
| YAML is empty | Rejected: an empty config has no `claude.system`. |
| `claude.system` is empty on a non-mention agent | Validation fails. Inline text or a file reference satisfies the requirement. |
| A `system` file path is absolute, unnormalized, or escapes the repository root | Validation fails. |
| A `system` file is missing, empty, or over 64 KiB at the validated commit | Validation fails at sync and on config pull requests. At session start it fails the session instead. |
| `system` is an empty list | Validation fails; a list needs at least one text part or file reference. |
| Cron expression is invalid | Validation fails before anything is stored or scheduled. |
| `branches` is set on a non-`push` react event | Validation fails. |
| A `sandbox.compute` value is out of range or unparseable | Validation fails; values are never clamped. Bounds: `cpu` 0.125 to 16 vCPU, `memory` 512MB to 64GB, `timeout` 60s to 1h. `memory` is a size string (`8GB`, `512MB`; binary, `1GB` = `1024MB`) or an object with `gb`/`mb` keys; `timeout` is a duration string (`30m`, `1h`, `90s`) or an object with `hours`/`minutes`/`seconds` keys. |
| `sandbox.github_token.permissions` names an unknown scope or an invalid level | Validation fails. The value is `read_only` or a map of GitHub App permission scopes to `read`/`write`. |
| `sandbox.github_token.permissions` requests a permission the installation was not granted | The session fails at sandbox creation with exit status `missing_token_permissions`. |
| `sandbox.github_token.repositories` is empty, has duplicates, or names a repository the installation cannot access | An empty list or duplicates fail validation. An inaccessible name fails the session with exit status `missing_repo_access`. |
| A `skills` path is absolute, unnormalized, escapes the repository root, or is the root itself | Validation fails. |
| A `skills` path points at an agent config file | Validation fails; a skill is a directory containing a `SKILL.md`. |
| More than 10 `skills` entries, or two entries whose paths end in the same directory name | Validation fails; the last path segment is the installed skill name. |
| A `skills` entry without `repository:` has no `SKILL.md`, or unparseable frontmatter, at the validated commit | Validation fails at sync and on config pull requests. Entries with `repository:` are checked at session start. |
| A skill cannot be resolved at session start (missing `SKILL.md`, unreachable repository, over a size limit, not UTF-8) | The session fails before the agent starts: `status` is `error`, with the reason in `status_reason`. |
| New file is invalid | The sync reports the error and does not create an agent. |
| Existing file becomes invalid | The sync records the error and keeps the last good config. |
| File is removed | The agent is soft-deleted and its schedules removed. |
---
# API reference
> REST API for starting sessions, searching session history, streaming output, and discovering integrations.
Source: https://www.ellipsis.dev/docs/reference/api
The Ellipsis REST API lets you start agent sessions, search and read session history, stream live output, discover your connected integrations, and inspect your agent configs and usage from your own scripts and services. It is the same `/v1` surface the [Ellipsis CLI](/docs/reference/cli) uses.
The base URL is `https://api.ellipsis.dev`. Every endpoint returns JSON.
## Get an API key
Create an API key under **Platform → API keys** in the Ellipsis dashboard. The secret is shown only once, so copy it when you create it.
There are two kinds of credential:
- **API keys** authenticate scripts and automations against the public API as your organization. Use these for CI pipelines, deploy hooks, and backend services.
- **User tokens** authenticate the Ellipsis CLI as you. The CLI mints one for you through `agent login`, so you rarely create these by hand.
Store the key in an environment variable rather than committing it. `ELLIPSIS_API_TOKEN` is the same name the [CLI](/docs/reference/cli) reads, so one variable serves both:
```bash
export ELLIPSIS_API_TOKEN="ellipsis_key_..."
```
## Authentication
Authenticate with your API key sent as a bearer token:
```bash
curl https://api.ellipsis.dev/v1/me \
-H "Authorization: Bearer $ELLIPSIS_API_TOKEN"
```
A request without a valid `Authorization: Bearer` header is rejected with `401`.
## Endpoints
| Method | Path | Description |
| --- | --- | --- |
| `GET` | `/v1/me` | The customer, user, and API key behind the current credential. |
| `GET` | `/v1/budget` | Current spend and the configured run, daily, weekly, and monthly limits. |
| `GET` | `/v1/usage` | Usage dashboard data for the account. |
| `POST` | `/v1/sessions` | Start an agent session. |
| `GET` | `/v1/sessions` | List agent sessions. |
| `GET` | `/v1/sessions/search` | Search sessions by step text, recap text, created PR, or similarity. |
| `GET` | `/v1/sessions/{session_id}` | Fetch a single agent session. |
| `GET` | `/v1/sessions/{session_id}/steps` | Read a session's rendered steps. |
| `GET` | `/v1/sessions/{session_id}/turns` | Read a durable conversation's turns and messages. |
| `GET` | `/v1/sessions/{session_id}/transcripts` | Download a session's raw transcript files. |
| `POST` | `/v1/sessions/{session_id}/replay` | Re-run a finished session. |
| `POST` | `/v1/sessions/{session_id}/stop` | Stop an in-flight session. |
| `WS` | `/v1/sessions/{session_id}/stream` | Stream a session's live output over WebSocket. |
| `POST` | `/v1/sessions/sync` | Ingest a Claude Code session from a developer's machine. |
| `POST` | `/v1/configs` | Create an agent config by opening a pull request. |
| `GET` | `/v1/configs` | List the account's saved agent configs. |
| `GET` | `/v1/configs/{config_id}` | Fetch a single saved agent config. |
| `GET` | `/v1/defaults` | List the account and per-repository default agents. |
| `PUT` | `/v1/defaults` | Set the account default agent, or one repository's. |
| `DELETE` | `/v1/defaults` | Clear the account default agent, or one repository's. |
| `GET` | `/v1/templates` | List the built-in starter templates. |
| `GET` | `/v1/templates/{template_id}` | Fetch one template by slug. |
| `GET` | `/v1/sandboxes/variables` | List the account's sandbox environment variable names. |
| `PUT` | `/v1/sandboxes/variables` | Create or update sandbox environment variables. |
| `DELETE` | `/v1/sandboxes/variables/{name}` | Delete a sandbox environment variable. |
| `GET` | `/v1/integrations` | Every connected integration, in one call. |
| `GET` | `/v1/github/repos` | Repositories connected to the GitHub installation. |
| `GET` | `/v1/github/members` | The GitHub org roster, with linked Slack identities. |
| `GET` | `/v1/slack/channels` | Channels in the connected Slack workspace. |
| `GET` | `/v1/slack/members` | Members of the connected Slack workspace, with linked GitHub identities. |
| `GET` | `/v1/linear/teams` | Teams in the connected Linear organization. |
| `GET` | `/v1/sentry/organizations` | Connected Sentry organizations. |
| `GET` | `/v1/analytics/metrics` | Pull request and review analytics: totals, trends, and leaderboards. |
| `GET` | `/v1/analytics/pull-requests` | Pull request volume and trends, split human vs bot. |
| `GET` | `/v1/analytics/reviews` | Review activity: totals, verdicts, and top reviewers. |
| `POST` | `/v1/assets` | Upload an image an agent can link on a pull request. |
| `GET` | `/v1/assets` | List the account's assets, newest first. |
| `GET` | `/v1/assets/{asset_id}` | Fetch one asset, with a short-lived download URL. |
| `DELETE` | `/v1/assets/{asset_id}` | Delete an asset. |
## Start an agent session
`POST /v1/sessions` starts a session from a saved config, an inline config, a built-in template, or no config source at all. Pass `config_id` to run a saved agent, `config` to run an inline [agent config](/docs/reference/agent-config), or `template_id` to run a template. A config source is optional: with none, the session resolves your team's [default agent](/docs/concepts/sessions#which-config-a-session-runs): the `repository` field's repo default if one is set, else the account default, else a bare ad-hoc config (an empty system prompt on `claude-opus-4-8`, enabled, no triggers, default sandbox and limits). The `prompt` is the per-session instruction either way.
```bash
curl https://api.ellipsis.dev/v1/sessions \
-H "Authorization: Bearer $ELLIPSIS_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"config_id": "agent_x9Kd3Fq2",
"metadata": {"trigger": "nightly-batch"}
}'
```
| Field | Type | Description |
| --- | --- | --- |
| `config_id` | string | ID of a saved agent config to run. |
| `config` | object | An inline agent config to run instead of a saved one. |
| `template_id` | string | Slug of a built-in template to run, e.g. `welcome-to-ellipsis`. |
| `metadata` | object | String key/value pairs recorded on the session. |
| `config_override` | object | A partial config merged onto the config named by `config_id` or `template_id`, or onto the resolved default when no source is given, then re-validated, e.g. `{"budget": {"session": 5.0}}` to set just this session's budget. With an inline `config`, fold the override into the config you send instead. |
| `config_override_yaml` | string | The same override as a YAML/JSON string, e.g. read from a file. Pass one of `config_override` or `config_override_yaml`, not both. |
| `prompt` | string | Per-session instructions, appended to the initial prompt after the config's system prompt. Use it to steer a single session without changing the config. With no config source, it is the session's sole instruction. |
| `idle_start` | boolean | Start the session with no initial message: the sandbox spins up and the agent waits idle for the first message sent to it, which opens the conversation the way typing into a local `claude` does. This is what a promptless `agent` in the CLI sends. Cannot be combined with `prompt` or `handoff`; ignored when the resolved config sets `interactive: false` (that session runs its workflow kickoff as usual, since it can never receive a message). |
| `repository` | string | The repository the client is standing in, as `owner/name` (the CLI sends its `origin` remote automatically). With no config source it selects that repository's default agent; either way the repository is cloned into the sandbox at its default branch, on top of the config's own `sandbox.repositories`. A repository outside your installation is ignored, with no error. |
| `handoff` | object | Laptop-to-cloud [handoff](/docs/guides/sync-laptop-claude-code-sessions-into-one-searchable-history#hand-a-session-off-to-the-cloud) parameters: `parent_session_id` (the synced laptop session to continue), `repo`, `sha` (the pushed working-tree commit the sandbox checks out), and optional `ref`. The session runs the built-in Handoff agent, so omit `config_id`, `config`, and `template_id`. |
Pass at most one of `config_id`, `config`, or `template_id`, or omit all three to resolve the default agent. A `handoff` runs the built-in handoff config and cannot be combined with any config source or config override. The response is the created agent session, including its `id` and `status`, plus two resolution fields: `resolved_config_name` (the name of the config the session runs under) and `resolution_source` (`repo_default`, `account_default`, `none` for a bare ad-hoc start, or `null` when an explicit config source bypassed resolution). A default that is set but broken (its config was deleted, disabled, or its pull request has not merged) fails the request with `409` and a message naming the setting, never a silent fallback. A session's `source` is set automatically from your credential (an API key records `api`), so you can tell apart sessions started via the API, the CLI, the dashboard, or a webhook when you list them.
## List agent sessions
`GET /v1/sessions` returns recent sessions, newest first. All query parameters are optional.
```bash
curl "https://api.ellipsis.dev/v1/sessions?config_id=agent_x9Kd3Fq2&limit=20" \
-H "Authorization: Bearer $ELLIPSIS_API_TOKEN"
```
| Query | Type | Description |
| --- | --- | --- |
| `config_id` | string | Only sessions for this config. |
| `source` | string | Filter by session source. Repeat the parameter to pass several. |
| `author_id` | integer | Only sessions attributed to this developer, by GitHub account id. Resolve ids with [`GET /v1/github/members`](#discover-integrations). |
| `days` | integer | Only sessions from the last N days. |
| `start` | datetime | Only sessions at or after this timestamp. |
| `end` | datetime | Only sessions at or before this timestamp. |
| `limit` | integer | Maximum sessions to return. Defaults to 50. |
The response is `{ "sessions": [...] }`.
## Search sessions
`GET /v1/sessions/search` searches everything a session leaves behind and returns matches grouped by session, newest first. One query runs up to four match arms, and each result says which arms hit:
| `matched` value | The session matched because |
| --- | --- |
| `steps` | Every term in `q` appears in one of its steps. |
| `recap` | Every term in `q` appears in its recap (the summary and retrospective written when it finished). |
| `pr` | `q` is a pull request reference, and this session created that pull request. |
| `similar` | Its recap is semantically close to `q`, even with no exact word overlap. |
Terms in `q` are split on whitespace and matched case-insensitively; a session matches the `steps` or `recap` arm only if every term is found. A PR-shaped `q` (`#512`, `splitshift-hq/splitshift-api#512`, or a full PR URL) additionally matches the session that created that exact pull request, so you never depend on the URL appearing in step text.
```bash
curl "https://api.ellipsis.dev/v1/sessions/search?q=shift+trade+webhook&source=laptop&author_id=5201153" \
-H "Authorization: Bearer $ELLIPSIS_API_TOKEN"
```
| Query | Type | Description |
| --- | --- | --- |
| `q` | string | Required. At least 3 characters. |
| `scope` | string | `steps`, `recaps`, or `both`. Defaults to `both`. `recaps` skips step text; `steps` skips recap text and similarity. |
| `source` | string | Filter by session source (`laptop`, `cron`, `react`, `mention`, `manual`, `api`, `cli`). Repeatable. |
| `author_id` | integer | Sessions attributed to these GitHub account ids. Repeatable, and combined with `agent_config_id` as one author-or-agent filter. |
| `agent_config_id` | string | Sessions run by these agent configs. Repeatable. |
| `session_ids` | string | Restrict the search to these sessions. Repeatable. |
| `repo` | string | Sessions on this repository: `owner/name` for an exact match, or a bare name like `splitshift-api`. |
| `status` | string | Filter by session status. Repeatable. |
| `start` / `end` | datetime | Only sessions inside this window. |
| `limit` | integer | Maximum result sessions. Defaults to 20, capped at 100. |
The response is `{ "results": [...], "attributed_users": {...} }`. Each result carries:
| Field | Type | Description |
| --- | --- | --- |
| `session` | object | The full agent session, including its status, recap output, and created PRs. |
| `matched` | array | Which arms matched: `steps`, `recap`, `pr`, `similar`. |
| `recap_snippet` | string | A short window of the recap text around the first match. Present when the `recap` arm matched. |
| `step_hits` | array | Up to 5 matching steps, each with a `snippet`, `step_type`, `step_index`, and `created_at`, plus session context. |
| `step_hit_count` | integer | How many steps matched in total, so you can render "and N more". |
`attributed_users` maps each result's `attribution_id` to a GitHub account (`id`, `login`, `type`, `avatar_url`) so you can show who ran the session without a second lookup.
A typical investigation composes three calls: resolve a teammate's GitHub account id with [`GET /v1/github/members`](#discover-integrations), search with `q`, `author_id`, and a date window, then read the winning session's recap with [`GET /v1/sessions/{session_id}`](#fetch-a-session) and its full steps with [`GET /v1/sessions/{session_id}/steps`](#read-a-sessions-steps).
## Fetch a session
`GET /v1/sessions/{session_id}` returns one session, including its status, cost, tokens, config snapshot, and output.
```bash
curl https://api.ellipsis.dev/v1/sessions/session_7Hq2mX4p \
-H "Authorization: Bearer $ELLIPSIS_API_TOKEN"
```
A session's `status` is one of `scheduled`, `creating_sandbox`, `running`, `retrying`, `completed`, `error`, `cancelled`, or `stopped`. Terminal sessions also carry an `exit_status` explaining why they ended, e.g. `completed`, `budget_hit`, or `tool_call_failed`.
For a [durable conversation](/docs/concepts/sessions#durable-conversations), `status` describes only the most recent turn's execution — it returns to `scheduled` and `running` each time a new message wakes the conversation. The conversation's own lifecycle is `session_state`: `running` (a turn is executing), `idle` (waiting for the next message), or `closed` (ended by its surface, e.g. a merged PR). Single-shot sessions have `session_state: null`, so it also tells you whether a session is a conversation at all. `last_message_at` records when the most recent message arrived.
## Read a session's steps
`GET /v1/sessions/{session_id}/steps` returns the session's full stored steps: every event the agent produced, in order.
```bash
curl https://api.ellipsis.dev/v1/sessions/session_7Hq2mX4p/steps \
-H "Authorization: Bearer $ELLIPSIS_API_TOKEN"
```
The response is `{ "steps": [...] }`, ordered by `created_at` then `step_index`. Each step is one event from the agent's run: an assistant turn, a tool call, a tool result, or the final result. The `step_type` says which, and `data` carries the full event payload, including message text, tool names and inputs, and tool output. Use it after [search](#search-sessions) points you at a session: the search hit gives you the `step_index` to read around.
Steps from laptop sessions are parsed from a transcript that is redacted on the developer's machine before it is synced, so they never contain more than what the session's owner uploaded.
## Read a conversation's turns
`GET /v1/sessions/{session_id}/turns` returns a [durable conversation](/docs/concepts/sessions#durable-conversations)'s structure: the messages its surface delivered and the turns that answered them. For a single-shot session both lists are empty.
```bash
curl https://api.ellipsis.dev/v1/sessions/session_7Hq2mX4p/turns \
-H "Authorization: Bearer $ELLIPSIS_API_TOKEN"
```
The response is `{ "turns": [...], "messages": [...] }`. Turns are ordered by `turn_index` and each carries its own `status`, `exit_status`, timing, cost breakdown, and token totals, plus the `react_action` of the event that scheduled it. Messages are the conversation's inbox, oldest first: each has the rendered text the agent received (`body`), when it arrived, and `delivered_turn_id`, the turn that consumed it, or `null` while it is still waiting for one. A message with `closes_session: true` is the conversation's final message. The per-turn work itself (tool calls, output) lives in [the steps](#read-a-sessions-steps).
## Download a session's raw transcripts
`GET /v1/sessions/{session_id}/transcripts` returns download URLs for the session's raw transcript files: the unparsed Claude Code event stream, one gzipped JSONL file per process. A session that retried has one file per attempt, in order; the failed attempt's transcript is preserved. Where [steps](#read-a-sessions-steps) is the parsed view, this is the original artifact, suitable for archiving or for tooling that consumes the stream format directly.
```bash
curl https://api.ellipsis.dev/v1/sessions/session_7Hq2mX4p/transcripts \
-H "Authorization: Bearer $ELLIPSIS_API_TOKEN"
```
The response is `{ "session_id": ..., "transcripts": [...] }`. Each entry carries the `process_id`, the `format` (`claude_stream_json` for cloud sessions, `claude_transcript` for synced laptop sessions), `event_count`, compressed `bytes`, `written_at`, a `write_status`, and a `download_url`.
The `download_url` is a presigned link that expires after `expires_in` seconds (60): fetch this endpoint, then `GET` the URL immediately — the JSON API never carries the file itself. The body is gzipped JSONL:
```bash
curl -s "$DOWNLOAD_URL" | gunzip > transcript.jsonl
```
Three states to know about. A running session serves its transcript so far (`write_status: null` — the file grows as the session runs). A `write_status` of `"failed"` means the session's final flush did not land: the file is still usable but may be missing its tail. And a transcript past your log retention setting has been deleted — its `download_url` returns a 404 from storage.
A session with no stored transcripts returns an empty list. By default it reuses the original session's config snapshot; pass `config_id`, `config_override` or `config_override_yaml`, or `prompt` to rerun with changes, e.g. a higher `budget.session` budget.
```bash
curl https://api.ellipsis.dev/v1/sessions/session_7Hq2mX4p/replay \
-H "Authorization: Bearer $ELLIPSIS_API_TOKEN" \
-X POST
```
## Stop a session
`POST /v1/sessions/{session_id}/stop` stops an in-flight session. The response is the session with `status` set to `stopped`.
```bash
curl https://api.ellipsis.dev/v1/sessions/session_7Hq2mX4p/stop \
-H "Authorization: Bearer $ELLIPSIS_API_TOKEN" \
-X POST
```
## Stream a session
`GET /v1/sessions/{session_id}/stream` upgrades to a WebSocket and streams the session's live output. Pass the bearer token in the `Authorization` header, or as a `?token=` query parameter when a header is not available (for example from a browser).
```bash
websocat "wss://api.ellipsis.dev/v1/sessions/session_7Hq2mX4p/stream?token=$ELLIPSIS_API_TOKEN"
```
Each frame is a JSON object with a `type`:
| Type | Fields | Meaning |
| --- | --- | --- |
| `status` | `status`, `session`, `run`, `cost_millicents`, `ts` | The session's status or running cost changed. `cost_millicents` is the total cost so far; it climbs during a turn as the session spends. |
| `stdout` / `stderr` | `data`, `seq`, `ts` | A line of output. `seq` increases monotonically. |
| `delta` | `text`, `output_tokens` | In-progress assistant output: a streamed text fragment and the running token count for the current response. Ephemeral, no `seq`; the completed output arrives again as a `stdout` frame. |
| `done` | `status`, `session`, `run`, `exit_status`, `cost_millicents` | The session finished. |
| `error` | `message` | A server-side stream error. |
To resume after a disconnect without missing or duplicating output, reconnect with `?after_seq=`, where `` is the highest `seq` you received. `delta` frames are not replayed on resume.
## Sync a laptop session
`POST /v1/sessions/sync` ingests one Claude Code session from a developer's machine. It is the endpoint behind [local session sync](/docs/guides/sync-laptop-claude-code-sessions-into-one-searchable-history); call it directly only to build your own uploader. It requires a user token from `agent login`, not an API key: synced sessions attribute to the developer who ran them, and one developer's sync can never overwrite another's session.
| Field | Type | Description |
| --- | --- | --- |
| `cc_session_id` | string | Required. The Claude Code session uuid. One Ellipsis session exists per uuid per developer; syncs upsert it. |
| `transcript_gzip_b64` | string | Required. The transcript JSONL, redacted, gzipped, then base64-encoded. Caps: 8 MiB compressed, 64 MiB decompressed. |
| `reason` | string | Which hook fired: `stop` (mid-session; the session shows `running`) or `session_end` (marks it `completed`). Defaults to `stop`. |
| `repo` | string | The repository the session ran in, as `owner/name`. |
| `cwd` | string | The session's working directory. |
| `git_branch` | string | The checked-out branch. |
The response is `{ "session_id": ..., "process_id": ..., "event_count": ..., "accepted": ... }`. `accepted` is `false` when the stored transcript was already at least as long: the longest snapshot wins, so repeated syncs of one session (including across `claude --resume`) are acknowledged without duplicating anything. After an accepted sync, the transcript is parsed into [steps](#read-a-sessions-steps) asynchronously.
## List agent configs
`GET /v1/configs` returns the account's saved agent configs, including each config's GitHub source, last session, and any pending "store in GitHub" pull request.
```bash
curl https://api.ellipsis.dev/v1/configs \
-H "Authorization: Bearer $ELLIPSIS_API_TOKEN"
```
The response is `{ "configs": [...] }`. Fetch one config with `GET /v1/configs/{config_id}`.
`POST /v1/configs` creates an agent the same way the dashboard does: it opens a pull request adding the YAML file to a repository you name, and the agent goes live when that pull request merges. Pass exactly one of `config` (an inline agent config) or `template_id`, plus the target `repository` (a bare repo name in your account) and an optional `path` (defaults to `agents/.yaml`).
## Default agents
The [default agent](/docs/concepts/sessions#which-config-a-session-runs) a session runs when you name no config source is a pointer to a saved config, set account-wide or per repository. These endpoints manage those pointers. They are addressed by rung, never by row id: the account default is `repository` omitted (or `null`), a repository default is `repository` as `"owner/name"`.
`GET /v1/defaults` lists every rung that is set.
```bash
curl https://api.ellipsis.dev/v1/defaults \
-H "Authorization: Bearer $ELLIPSIS_API_TOKEN"
```
The response is `{ "defaults": [...] }`. Each entry has the rung's `repository` (`"owner/name"`, or `null` for the account default), the pointed-at `config_id` and `config_name`, `updated_at`, and `broken`: `null` when the pointer can serve sessions, else why it cannot (`config_deleted`, `config_disabled`, `config_pending_pr`, or `repo_inaccessible`). A broken rung fails session starts with `409` rather than silently running a different agent, so fix or clear it.
`PUT /v1/defaults` sets a rung's pointer, replacing whatever it pointed at before.
```bash
curl https://api.ellipsis.dev/v1/defaults \
-H "Authorization: Bearer $ELLIPSIS_API_TOKEN" \
-H "Content-Type: application/json" \
-X PUT \
-d '{"repository": "acme/api", "config_id": "agent_x9Kd3Fq2"}'
```
| Field | Type | Description |
| --- | --- | --- |
| `repository` | string | The repository whose default to set, as `owner/name`. Omit to set the account default. A repository outside your installation is rejected with `404` (unlike [start session](#start-an-agent-session)'s `repository` context, which is silently ignored). |
| `config_id` | string | The saved config to point the rung at. Only a live, synced config is accepted: an unknown or deleted id is `404`, a disabled config or one whose pull request has not merged is `400`. |
The response is the updated rung, in the same shape `GET` returns.
`DELETE /v1/defaults` clears the account default; `DELETE /v1/defaults?repository=owner/name` clears that repository's. Clearing a rung that is not set returns `404`.
```bash
curl "https://api.ellipsis.dev/v1/defaults?repository=acme/api" \
-H "Authorization: Bearer $ELLIPSIS_API_TOKEN" \
-X DELETE
```
The in-sandbox token an agent uses to reach the API can **read** the defaults ladder but cannot set or clear it: code running in a sandbox must not be able to repoint which agent your account runs by default. `PUT` and `DELETE` require an API key or user token; a sandbox token is rejected with `403`.
## Templates
`GET /v1/templates` lists the built-in starter templates, each with its slug, description, and complete YAML. Fetch one with `GET /v1/templates/{template_id}`. A template slug works anywhere a config does: `template_id` on [start session](#start-an-agent-session) runs it once, and on create config it opens the pull request that deploys it.
## Sandbox variables
Sandbox variables are environment variables, stored once on your account, that an agent can pull into its sandbox at session start. Store a secret or shared config value here, then reference it by name from an agent's `sandbox.variables` so the secret never lives in your config file. Only the variables an agent names reach that agent's sandbox.
Values are write-only. You upload them, but the API never returns them. Listing a variable shows its name and timestamps, never its value.
The in-sandbox token an agent uses to reach the API can **list** sandbox variables but cannot create, update, or delete them: code running in a sandbox can see which variables exist but cannot change your stored secrets. `PUT` and `DELETE` require an API key or user token; a sandbox token is rejected with `403`. (The `GET` works with any credential.)
`GET /v1/sandboxes/variables` lists the stored variables, sorted by name.
```bash
curl https://api.ellipsis.dev/v1/sandboxes/variables \
-H "Authorization: Bearer $ELLIPSIS_API_TOKEN"
```
The response is `{ "variables": [...] }`, where each entry has a `name`, `created_at`, and `updated_at`.
`PUT /v1/sandboxes/variables` creates or updates one or more variables in a single request. Each variable is upserted by name: an existing name is overwritten, a new name is created, and variables you do not send are left untouched.
```bash
curl https://api.ellipsis.dev/v1/sandboxes/variables \
-H "Authorization: Bearer $ELLIPSIS_API_TOKEN" \
-H "Content-Type: application/json" \
-X PUT \
-d '{
"variables": [
{"name": "NPM_TOKEN", "value": "npm_..."},
{"name": "API_BASE_URL", "value": "https://example.com"}
]
}'
```
| Field | Type | Description |
| --- | --- | --- |
| `name` | string | Variable name. Must start with a letter or underscore and contain only letters, digits, and underscores. |
| `value` | string | The value to store. |
Names are validated before anything is written, so one invalid name rejects the whole request with `400` and stores nothing. An account can hold up to 500 variables; a request that would exceed that is rejected with `400`. Both `GET` and `PUT` return the full, current variable list.
`DELETE /v1/sandboxes/variables/{name}` removes one variable by name and returns the remaining list. Deleting a name that does not exist still succeeds.
```bash
curl https://api.ellipsis.dev/v1/sandboxes/variables/NPM_TOKEN \
-H "Authorization: Bearer $ELLIPSIS_API_TOKEN" \
-X DELETE
```
## Discover integrations
These read-only endpoints let a script (or an agent writing another agent's config) learn what is connected to the account: which repositories, channels, teams, and people a config can name. They work with any credential, including the in-sandbox token, and never return OAuth tokens or other secrets.
`GET /v1/integrations` returns everything in one call:
```bash
curl https://api.ellipsis.dev/v1/integrations \
-H "Authorization: Bearer $ELLIPSIS_API_TOKEN"
```
| Key | When connected | When not connected |
| --- | --- | --- |
| `github` | `account_login`, `account_type`, `repository_selection` (`all` or `selected`), `suspended`, `repository_count` | `null` (the GitHub App was uninstalled) |
| `slack` | `team_id`, `team_name`, `operations_channel_id` | `null` |
| `linear` | `organization_id` plus `teams`, each with `id`, `name`, `key`, and `is_enabled` | `null` |
| `jira` | `cloud_id` | `null` |
| `sentry` | A list of organizations, each with `integration_id` and `organization_slug` | `[]` |
The per-provider endpoints return the resources an agent config can reference:
- `GET /v1/github/repos` returns `{ "repositories": [...] }`: every repository connected to the installation, each with `id`, `name`, `full_name`, `private`, `default_branch`, and `description`. These are the valid values for `repository` on [create config](#list-agent-configs) and for `sandbox.repositories` in an [agent config](/docs/reference/agent-config).
- `GET /v1/github/members` returns `{ "members": [...] }`: the GitHub organization's roster (for a personal account, just that account), each with `id`, `login`, `name`, `avatar_url`, and `role` (`admin` or `member`; `null` for a personal account). The `id` values are what `author_id` accepts on [list](#list-agent-sessions) and [search](#search-sessions). When a member's Slack identity is linked, `slack` carries their `slack_user_id` and `slack_email`.
- `GET /v1/slack/channels` returns `{ "team_id", "team_name", "channels": [...] }`, each channel with `id`, `name`, `is_private`, and `is_member`. Listed live from Slack.
- `GET /v1/slack/members` returns `{ "team_id", "team_name", "members": [...] }`: the workspace's human members (bots are excluded), each with `id`, `name`, `real_name`, `display_name`, and `email`. Listed live from Slack. When a member's GitHub identity is linked, `github` carries their GitHub `id` and `login`, which lets you go from a Slack mention to the same person's sessions.
- `GET /v1/linear/teams` returns `{ "organization_id", "teams": [...] }`, each team with `id`, `name`, `key`, and `is_enabled`.
- `GET /v1/sentry/organizations` returns `{ "organizations": [...] }`, each with `integration_id` and `organization_slug`. An empty list means Sentry is not connected.
The Slack and Linear endpoints return `404` when that integration is not connected. GitHub endpoints always work (an Ellipsis account is a GitHub account), and Sentry returns an empty list instead of `404` because the organization is the connection.
## Analytics
The `/v1/analytics` endpoints return the same aggregation behind the dashboard's Analytics pages, so a script or an agent can answer questions like "which apps review the most pull requests?" over plain HTTP. They work with any credential and expose only the account's own GitHub pull request and review activity.
All three share the same windowing: pass `days` to look back N days (default 30), or an explicit `start`/`end`.
```bash
curl "https://api.ellipsis.dev/v1/analytics/metrics?days=90&account_type=bot" \
-H "Authorization: Bearer $ELLIPSIS_API_TOKEN"
```
- `GET /v1/analytics/metrics` returns the org-wide picture: a per-day `series`, `totals`, per-repository usage, `contributors` and `reviewers` leaderboards, and the repo/author facets. Filters: `repo` (`owner/name`, repeatable), `author` (pull request author login, repeatable), `account_type` (`all`, `user`, or `bot`, where `bot` means apps and agents), and `status` (`open`, `draft`, `merged`, `closed`, repeatable). The `reviewers` leaderboard with `account_type=bot` answers the which-apps-review-the-most question directly.
- `GET /v1/analytics/pull-requests` returns pull request volume and trend with human-vs-bot splits per day: `series`, `totals`, `facets`, `recent` (newest first, capped), and `truncated` (`true` when the window hit the server's scan cap and the figures undercount). Filters: `account_type` (raw GitHub account types `User`/`Bot`, repeatable), `repository_id`, `author_id`, and `status` (all repeatable).
- `GET /v1/analytics/reviews` returns review activity: `reviews` and `review_comments` feeds (newest first, capped), a per-day `series`, `totals`, and `facets`. Filters: `repo` (repeatable), `author` (reviewer login, repeatable), `account_type`, and `review_state` (`APPROVED`, `CHANGES_REQUESTED`, `COMMENTED`, repeatable).
The same data drives `agent analytics reviewers|prs|reviews` in the [CLI](/docs/reference/cli#analytics).
## Assets
An asset is a file an agent persists past its sandbox's lifetime, so a session can show its work: upload a screenshot, get back a link, paste the link on a pull request. v1 accepts PNG images only.
`POST /v1/assets` uploads one image, base64-encoded in the JSON body:
```bash
curl https://api.ellipsis.dev/v1/assets \
-H "Authorization: Bearer $ELLIPSIS_API_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"filename\": \"schedule-grid.png\",
\"content_type\": \"image/png\",
\"data_b64\": \"$(base64 -i schedule-grid.png)\"
}"
```
| Field | Type | Description |
| --- | --- | --- |
| `filename` | string | Original basename, for display only. |
| `content_type` | string | Must be `image/png`. The decoded bytes must actually be a PNG; the declared type is never trusted. |
| `data_b64` | string | The file bytes, base64-encoded. 10 MiB cap per upload. |
The `201` response is `{ "asset": {...}, "url": "https://app.ellipsis.dev/assets/{asset_id}" }`. The `url` is the link to paste: it is gated by org membership, so only members of the owning organization can open it, and anyone else gets a `404`. Uploads from a sandbox record the originating session on `asset.agent_session_id` and are capped at 100 assets and 100 MiB per session.
`GET /v1/assets` lists the account's assets newest first, metadata only. `agent_session_id` scopes the list to one session's uploads; `limit` defaults to 50. `GET /v1/assets/{asset_id}` returns the metadata, the gated `url`, and a `download_url`: a presigned link for the raw bytes, valid for 60 seconds, so fetch it and download immediately.
```bash
curl -s https://api.ellipsis.dev/v1/assets/9f2c4b7d1e8a4c02b6f3a1d5e7c90842 \
-H "Authorization: Bearer $ELLIPSIS_API_TOKEN" | jq -r .download_url | xargs curl -s -o schedule-grid.png
```
`DELETE /v1/assets/{asset_id}` deletes an asset and returns `204 No Content`. Afterward the asset is gone from `GET /v1/assets` and `GET /v1/assets/{asset_id}`, and its gated `url` stops resolving. A missing id, another account's id, and an already-deleted id all return an identical `404`, so ids can't be enumerated. Sandbox tokens get a `403`: an agent can upload but not delete, so deletion is reserved for API keys and user tokens.
```bash
curl -X DELETE https://api.ellipsis.dev/v1/assets/9f2c4b7d1e8a4c02b6f3a1d5e7c90842 \
-H "Authorization: Bearer $ELLIPSIS_API_TOKEN"
```
See [Attach agent screenshots to pull requests](/docs/guides/attach-agent-screenshots-to-pull-requests) for the end-to-end flow.
## Identity, budget, and usage
`GET /v1/me` returns the customer, user, and API key behind the current credential. `GET /v1/budget` returns the account's current spend against its run, daily, weekly, and monthly limits. `GET /v1/usage` returns the usage dashboard: per-day and per-model token and cost breakdowns for the current period, with a prior-period comparison.
```bash
curl https://api.ellipsis.dev/v1/budget \
-H "Authorization: Bearer $ELLIPSIS_API_TOKEN"
```
---
# CLI
> Install and authenticate the agent binary and run agents from your terminal.
Source: https://www.ellipsis.dev/docs/reference/cli
The Ellipsis CLI is a single binary named `agent` that drives the same `/v1` [REST API](/docs/reference/api) from your terminal. It is a thin, open-source client: the agent runs in the Ellipsis cloud; the CLI authenticates, starts sessions, and reads results.
## Install
```bash
brew install ellipsis-dev/cli/agent
agent ping # confirms the CLI can authenticate and reach /v1
```
## Authenticate
Interactive use: log in once. `agent login` runs a device-code flow (prints a verification URL, opens it unless `--no-browser`, polls until you approve). The user token lands in `~/.config/ellipsis/config.json` and attributes sessions to you.
```bash
agent login # --no-browser over SSH
agent me # identity behind the current credential
agent logout # remove the stored token
```
Scripts and CI: create an [API key](/docs/reference/api#get-an-api-key) and export it as `ELLIPSIS_API_TOKEN` instead of logging in. Credentials resolve highest-wins: explicit argument, then `ELLIPSIS_API_TOKEN`, then the stored config file. `ELLIPSIS_API_BASE_URL` points the CLI at a non-default host (default `https://api.ellipsis.dev`).
## Teach your coding agent about Ellipsis
A public [Agent Skill](https://agentskills.io) teaches a local coding agent (Claude Code, Cursor, Codex, and others) what Ellipsis is and how to drive it with this CLI, so the agent can start, watch, search, and hand off sessions for you. Install it once:
```bash
npx skills add ellipsis-dev/cli
```
That copies `skills/ellipsis/` from the public [ellipsis-dev/cli](https://github.com/ellipsis-dev/cli) repository into your coding agent's skills directory (for example `.claude/skills/`). The agent loads it by name the next time a task mentions Ellipsis or the `agent` CLI. To pin it globally instead, copy the same directory into `~/.claude/skills/`.
## Start a session
A config source is optional. With none, the session runs on your team's [default agent](/docs/concepts/sessions#which-config-a-session-runs): the repository default for the repo you are standing in if one is set, else the account default, else a bare ad-hoc config (an empty system prompt on `claude-opus-4-8`, no triggers). The prompt is the sole per-session instruction, so a positional prompt is enough:
```bash
agent session start "triage the failing CI on splitshift-api"
```
When a default resolved, the CLI says which agent picked up the work:
```
using config "CI failure triager" (repo default)
✓ started session session_7Hq2mX4p (scheduled)
```
The CLI also detects the repository you are standing in (the `origin` remote, as `owner/name`) and sends it with the request. The server clones that repository into the sandbox at its default branch, on top of whatever the config declares, even when you pass `--config`. Outside a git repository, nothing is sent. To hand off uncommitted work instead of the default branch, use `agent session handoff`.
The positional prompt is shorthand for `-p/--prompt`; pass one or the other, not both. To start from a saved config, an inline config file, or a built-in template, pass at most one of `--config` (a saved config id), `--config-file`, or `--template`:
```bash
agent session start --config agent_x9Kd3Fq2
agent session start --config-file my_agent.json
agent session start --template welcome-to-ellipsis --watch
```
Sugar flags set individual config fields for one session without editing a config: `--model`, `--system` (the system prompt), `--repo` (repeatable, `owner/name` or a bare `name`), `--cpu`, `--memory`, `--timeout`, and `--budget` (per-run USD, `limits.run`). They merge into one override on top of the chosen or default config; `--config-override` and `--config-override-file` take a full partial config.
```bash
agent session start "audit the trade-expiry changes" \
--repo splitshift-api --model claude-opus-4-8 --budget 5
```
By default `start` returns as soon as the session is created (`-d/--detach`). To block until it reaches a terminal status, pass one attach mode: `-w/--watch` streams live output; `--watch --quiet` blocks without streaming and exits with a code matching the outcome, for scripts; `--connect` waits for the sandbox, then opens the interactive view. If the resolved config sets `interactive: false`, `--connect` degrades to `--watch` with a printed note (the backend refuses the message surface for non-interactive sessions).
List, inspect, follow, and stop sessions:
```bash
agent session list --limit 20 # filter with --config, --source, --author, --days
agent session get session_7Hq2mX4p # inspect one session
agent session get session_7Hq2mX4p --watch # follow until it finishes (--quiet for the result only)
agent session steps session_7Hq2mX4p # render the session's steps, one line each
agent session transcript session_7Hq2mX4p -o run.jsonl # download the raw transcript (.jsonl)
agent session stop session_7Hq2mX4p # stop an in-flight session
```
Connect to a running session to follow it live and send messages, or follow read-only:
```bash
agent session connect session_7Hq2mX4p # view, follow live, and send messages
agent session connect session_7Hq2mX4p --no-input # follow read-only (no composer), for scripts
```
`--no-steps` skips replaying prior steps on open.
Replay a finished session, optionally with changes (`--config`, `--config-override`, `--prompt`, `--watch`, `--quiet`):
```bash
agent session replay session_7Hq2mX4p
```
## Search session history
`agent session search` runs the same four-arm [session search](/docs/reference/api#search-sessions) as the API: step text, recap text, created pull requests, and recap similarity. Results are grouped by session, newest first, with the matching snippet under each.
```bash
agent session search "shift trade webhook"
agent session search "acme/api#512" # the session that created that pull request
agent session search "webhook retries" --author tony --since "3 days ago"
```
`--author` takes a GitHub login and resolves it against `agent github members` (it also works on `agent session list`). `--since` and `--until` accept ISO 8601 or the natural forms `today`, `yesterday`, and `N days ago`. Narrow further with `--repo`, `--source`, `--status`, `--config` (a config id, repeatable), `--session`, and `--scope steps|recaps|both`. Follow a hit with `agent session get` for the recap and `agent session steps` for the full steps.
## Sync local Claude Code sessions
`agent hooks` manages [local session sync](/docs/guides/sync-laptop-claude-code-sessions-into-one-searchable-history): Claude Code hooks that upload each session you run in an enrolled repository, so laptop work appears in the same session history as cloud work.
```bash
agent hooks install # write the Stop + SessionEnd hooks to ~/.claude/settings.json
agent hooks enroll # opt the cwd's repository in (or pass owner/name)
agent hooks status # hook state, enrolled repositories, last sync
agent hooks logs --failures # the sync activity log (-n to tail, --json for NDJSON)
agent hooks stats # sync counters, also in ~/.config/ellipsis/hooks/stats.json
agent hooks unenroll # opt a repository back out
agent hooks uninstall # remove the hooks; enrollment is kept
```
`agent session sync` is the upload itself, normally invoked by the hooks with their context on stdin; `--transcript`, `--session-id`, `--reason`, and `--cwd` exist for manual runs. `agent session handoff "" --parent ` pushes a snapshot of your working tree and [continues a synced session in the cloud](/docs/guides/sync-laptop-claude-code-sessions-into-one-searchable-history#hand-a-session-off-to-the-cloud).
## Manage configs
```bash
agent config init agents/my_agent.yaml # scaffold a starter config
agent config list # list saved configs
agent config get agent_x9Kd3Fq2 # print one config as YAML (-o json)
```
`agent config create` creates an agent the same way the dashboard does: Ellipsis opens a pull request adding the config file to your repository, and the agent goes live when it merges.
```bash
agent config create --repo api --file agents/foo.yaml # from a local config file
agent config create --repo api --template code-reviewer # from a template
```
`agent template list` prints the built-in templates with their slugs; a slug works anywhere a template is accepted (`agent session start --template`, `agent config create --template`).
## Default agents
`agent config default` (alias `defaults`) manages which agent a session runs when you name no config source: the [defaults ladder](/docs/concepts/sessions#which-config-a-session-runs) of repository default, then account default, then a bare ad-hoc config. Every verb maps to the [`/v1/defaults`](/docs/reference/api#default-agents) endpoints.
```bash
agent config default # the effective default for the repo you are standing in
agent config default list # every rung that is set, account and per-repo
agent config default set agent_x9Kd3Fq2 # set the ACCOUNT default
agent config default set agent_x9Kd3Fq2 --repo # set the default for the repo you are standing in
agent config default set agent_x9Kd3Fq2 --repo acme/api # set a named repo's default
agent config default clear --repo acme/api # clear a rung (alias rm)
```
Bare `agent config default` reads like session start resolves: the repo default for your `origin` remote if set, else the account default, else `no default set` (sessions start on the bare config).
```
$ agent config default
using config "CI failure triager" (repo default for acme/api)
```
Writes are always explicit about their target: without `--repo` they address the account default, `--repo` with no value addresses the repo you are standing in, and `--repo owner/name` addresses that repository. A rung pointing at a config that was deleted, disabled, or never merged shows `broken: ` in `list`; sessions refuse to start on it until you fix or clear it. Sandbox tokens can read but not set or clear (the server's `403`), so an agent cannot repoint your account's default.
## Assets
`agent asset` manages [assets](/docs/guides/attach-agent-screenshots-to-pull-requests): PNG images an agent stores past its sandbox, each behind an org-gated link. Every verb maps to the [`/v1/assets`](/docs/reference/api#assets) endpoints.
```bash
agent asset upload schedule-grid.png # store a PNG; prints its org-gated link
agent asset list # stored assets, newest first (--session scopes to one run)
agent asset get 9f2c4b7d1e8a4c02b6f3a1d5e7c90842 -o schedule-grid.png # show one, or -o downloads the bytes
agent asset delete 9f2c4b7d1e8a4c02b6f3a1d5e7c90842 # delete an asset (alias rm)
```
`delete` prints `✓ deleted ` on success. A missing, foreign, or already-deleted id prints `asset not found: ` and exits nonzero. Sandbox tokens can't delete, so an agent's own token gets the server's `403`. Every verb accepts `--json`.
## Sandbox variables
`agent sandbox variable` (alias `var`) manages the write-only [sandbox variable store](/docs/guides/set-up-your-agents-cloud-development-environment#inject-credentials-with-sandbox-variables) that `sandbox.variables` entries resolve from. Values can be written and deleted but never read back.
```bash
agent sandbox variable list # names and timestamps; values stay hidden
agent sandbox variable set LOG_LEVEL=debug # inline KEY=VALUE pairs
agent sandbox variable set --from-file .env # or load from a .env or flat-JSON file
agent sandbox variable rm NPM_TOKEN # delete one variable
```
`set` accepts inline pairs, `--from-file`, or both; file pairs load first, so an inline pair with the same name wins.
## Sandbox builds
`agent sandbox build` (alias `builds`) runs a config's environment definition (base image plus `dockerfile_append`, repository checkout, `image.setup`, snapshot) without starting an agent, and caches the resulting image for future sessions. The workflow: [Set up your agent's cloud development environment](/docs/guides/set-up-your-agents-cloud-development-environment#build-the-sandbox-image).
```bash
agent sandbox build start --config-file agents/foo.yaml --watch # build a local config, stream to completion
agent sandbox build start --config agent_x9Kd3Fq2 # re-verify a deployed config's image
agent sandbox build list # recent builds, newest first
agent sandbox build get build_3fKq82Lm # status, phase timings, cache tier, cost
agent sandbox build logs build_3fKq82Lm --watch # persisted log; --watch follows live
```
`start` takes exactly one of `--config-file` or `--config`, plus `--hooks` to also run the config's `post_start` and `post_clone` hooks in the built sandbox. Like `session start`, it returns the build record immediately; `--watch` streams the build and exits with a code matching the outcome. Builds bill compute only, never tokens.
## Discover integrations
The discovery commands mirror the [integration discovery endpoints](/docs/reference/api#discover-integrations): what is connected and which repositories, channels, teams, and people a config can name.
```bash
agent integrations # every connected integration, in one table
agent github repos # repositories connected to the installation
agent github members # org roster, with linked Slack identities
agent slack channels # channels in the connected Slack workspace
agent slack members # workspace members, with linked GitHub identities
agent linear teams # teams in the connected Linear organization
agent sentry orgs # connected Sentry organizations
```
`agent github members` lists the logins `--author` accepts. The Slack and Linear commands report "not connected" when that integration is not set up; Sentry lists nothing.
## Analytics
`agent analytics` mirrors the [analytics endpoints](/docs/reference/api#analytics): the same pull request and review aggregation as the dashboard, from the terminal.
```bash
agent analytics reviewers --account-type bot # which apps review the most PRs
agent analytics prs --days 90 # PR volume and trend, human vs bot
agent analytics reviews --repo splitshift-api # review totals, verdicts, top reviewers
```
All three take the window flags `-d/--days` (default 30, mutually exclusive with `--start`) and `--start`/`--end` (ISO timestamps). `--account-type` is `all`, `user` (humans), or `bot` (apps and agents). `reviewers` adds `-r/--repo` (repeatable), `--sort reviews|approved|changes-requested|comments|lines`, and `-l/--limit` (default 10). `prs` adds `--status open|draft|merged|closed` (repeatable). `reviews` adds `--author` (reviewer login) and `--review-state APPROVED|CHANGES_REQUESTED|COMMENTED` (both repeatable).
## Budget and usage
```bash
agent budget # current spend against every limit
agent usage # usage dashboard for the period
```
## Notes
- Most commands accept `--json` to print the raw API response for piping into `jq`.
- Every command maps to the public [REST API](/docs/reference/api); anything the CLI does you can do over HTTP.
- `agent --help` and `agent --help` list every command and flag.
]