# Run Claude Code headless on real tickets, in a sandbox

> Run Claude Code without a terminal in an isolated sandbox that never holds your key, with CI, review and QA around it. 77 of 80 benchmark missions resolved.

- URL: https://letsship.ai/blog/run-claude-code-headless
- Author: Önder Ceylan
- Published: 2026-10-15

To run Claude Code headless on real tickets, add an Anthropic API key or subscription credential to SHIP and assign an issue to it. SHIP runs the `claude-code` harness for every role by default. Each stage works in an isolated sandbox with no terminal attached and no key inside it. Your CI, the Reviewer and QA check the Builder's work. You start a mission from an issue or with `ship delegate plan`, and read the stored session afterwards. In SWE-in-a-team, claude-code builders resolved 77 of 80 missions.

## Compare Claude Code's own headless and sandbox options

Claude Code gives you the two parts of an unattended run, and leaves you to put them together. Anthropic's [headless docs](https://code.claude.com/docs/en/headless) cover the headless half:

* `claude -p "..."`, or `--print`, runs Claude Code non-interactively and exits with a non-zero code when the run fails.
* `--bare` skips the auto-discovery of hooks, skills, plugins, MCP servers, auto memory and `CLAUDE.md`. The docs call it "the recommended mode for scripted and SDK calls". In bare mode Claude Code reads `ANTHROPIC_API_KEY`, because it "doesn't use your subscription login".
* `--allowedTools` and `--permission-mode` (`acceptEdits`, `dontAsk` or `auto`) decide what runs without a person to approve it.
* `--output-format json` returns the result with `total_cost_usd`, which the docs describe as client-side estimates that can differ from your bill.

The [sandbox docs](https://code.claude.com/docs/en/sandboxing) cover the other half. The Bash sandbox is off by default, and you turn it on with `/sandbox` or `sandbox.enabled`. It uses Seatbelt on macOS and `bubblewrap` with `socat` on Linux and WSL2. It wraps shell commands only. Claude's file tools, hooks and MCP servers run outside it.

Two defaults matter for an unattended run. By default, sandboxed commands can read most of the machine, and the docs name credential files such as `~/.ssh` and `~/.aws/credentials`. They also get the environment variables of Claude Code, with any secrets in them. For one boundary around everything, Anthropic's advice is to "run the Claude Code process itself in a container, virtual machine, or the sandbox runtime". That container, the key inside it, the output and who checks it are then yours to build.

SHIP does that part for you. Its Builder works in an isolated sandbox, runs your formatter, linter and tests before it pushes, and opens the pull request ([how a mission runs](https://letsship.ai/docs)). Each sandbox gets a scoped, single-use callback token. Before the agent reports back, SHIP stores the full session, the assembled prompt, the token usage and the cost. Nobody attaches to the run, and that record is what you read afterwards.

## Add an Anthropic key or subscription

On the **Connect your AI providers** step, add an Anthropic credential. An API key works, and so does a subscription credential. SHIP checks it with a live call when you save it. A typo or a key from the wrong account fails there, before your first mission ([LLM credentials](https://letsship.ai/docs/getting-started/llm-credentials)).

One credential is enough. The Planner, the Builder, the Reviewer and QA all default to the `claude-code` harness on the `opus` alias, so a single Anthropic credential runs a complete mission.

The key never enters the sandbox. SHIP starts the agent's container with a placeholder and puts the real credential on the outbound request, outside the container. If SHIP does not replace the placeholder, the provider gets the placeholder and rejects the call.

## Give the sandbox your toolchain

Every agent starts in a sandbox built on the platform base. The platform base is an isolated container with Node, pnpm, git and the agent CLIs ([Custom sandboxes](https://letsship.ai/docs/configuration/sandboxes)). If that is all your repository needs, skip this step.

If Claude Code needs more, such as Python, a JDK or a system library, write a custom sandbox. Its definition is a short Dockerfile with no `FROM` line, because it always starts from the platform base:

```dockerfile title=".ship/sandbox.Dockerfile"
RUN export DEBIAN_FRONTEND=noninteractive && apt-get update \
  && apt-get install -y --no-install-recommends python3 python3-venv python3-pip \
  && rm -rf /var/lib/apt/lists/*
ENV UV_LINK_MODE=copy
```

SHIP supports `RUN`, `ENV`, `ARG` and `WORKDIR`, and refuses `COPY`, `ADD` and `USER` with the line and the reason. It builds the definition once, keeps a snapshot, and starts every mission from that snapshot, so no mission pays to install the tools.

You choose where each role runs with `agents.<role>.sandbox` in `ship.yml`, or with a `.ship/sandbox.Dockerfile` in your repository. Use `agents.<role>.size` for more CPU or memory, from `standard-1` up to `standard-4`, which is the default. Credentials for a private package registry go on the outbound request too, so they never enter the sandbox, its build log or its snapshot.

SHIP reads `ship.yml` and `.ship/sandbox.Dockerfile` from your default branch only. A pull request therefore cannot change the sandbox that reviews or tests it.

## Pin a model and effort to each role

With no `ship.yml`, every role runs `claude-code` on `opus`. To choose per role, add the file at the repository root:

```yaml title="ship.yml"
# yaml-language-server: $schema=https://letsship.ai/schema/ship.yml.json
version: 1

deployments: {}

agents:
  builder:
    harness: claude-code
    model: sonnet
    effort: high
```

The roles you leave out keep their defaults. A model alias (`opus`, `sonnet` or `haiku`) follows the newest model of its family, and a versioned model id stays exactly where you put it. On `claude-code`, `effort` must be `low`, `medium`, `high`, `xhigh` or `max`, or the stage fails before it starts ([Schema](https://letsship.ai/docs/configuration/schema)).

[SWE-in-a-team](https://letsship.ai/blog/swe-in-a-team) has numbers for the builder. Four Claude models ran as the claude-code builder on the same 20 tickets, one run each, while the Planner, Reviewer and Tester stayed on `claude-opus-4.8`. A ticket counts as resolved when the mission reached Ready for Acceptance and hidden tests passed. The figures are recomputed from the [per-run data](https://letsship.ai/data/swe-in-a-team-per-task.csv), API-equivalent at list price.

| Builder model on claude-code | Resolved | Per resolved ticket | Median mission time |
| ---------------------------- | -------- | ------------------- | ------------------- |
| `claude-sonnet-5`            | 20 of 20 | $4.31               | 20.7 min            |
| `claude-opus-5`              | 20 of 20 | $5.48               | 23.0 min            |
| `claude-fable-5`             | 19 of 20 | $6.44               | 20.7 min            |
| `claude-haiku-4-5`           | 18 of 20 | $4.23               | 22.7 min            |

The most expensive model did not resolve the most tickets. The Fable and Haiku misses passed CI, review and the Tester, and only the hidden tests caught them. The study is one trial per cell in one repository, so read the table as directional.

To try a different model on one mission without a change to the file, pass it on the hand-off:

```bash
ship delegate plan SHIP-412 --file docs/plan.md --harness claude-code --model opus
```

## Hand Claude Code a ticket or a spec

Work reaches Claude Code from a tracker or from your terminal ([how work reaches SHIP](https://letsship.ai/docs/getting-started/ways-of-working)). On a Linear project, assign the issue to SHIP. On GitHub Issues, comment `@SHIP` from an account with write access to the repository. The Planner writes the plan from the issue and the repository, and the Builder works from that plan. [Claude Code on Linear](https://letsship.ai/blog/claude-code-on-linear-issues) covers the tracker setup.

From a terminal, `ship delegate plan` skips the Planner and has the Builder work from a spec you wrote:

```bash
ship delegate plan --title "Rate-limit the search endpoint" --file docs/plan.md
```

If the change already exists on a branch, `ship delegate pr` hands the open pull request to the Reviewer and QA instead.

You follow the mission without a terminal session attached to it. The command `ship status SHIP-412 --json` prints the stage, the review and test state, the pull request and the agent runs ([Command reference](https://letsship.ai/docs/cli/reference)). To change course, `ship instruct SHIP-412 -m "..."` sends the agents back to the open pull request with your instruction. `@SHIP pause`, `@SHIP resume` and `@SHIP stop` on the issue control the mission itself.

## Cap what an unattended run can spend

SHIP limits an unattended mission in four ways, and all four limits are on by default:

| Limit                                   | Default                     | When it is reached                                                            |
| --------------------------------------- | --------------------------- | ----------------------------------------------------------------------------- |
| Spend per mission                       | $5                          | The mission stops                                                             |
| Credits per mission                     | 2 credits, or 6 agent-hours | The mission pauses until a member of your organization approves 1 more credit |
| Identical failures in a row at one gate | 2                           | The mission shows Needs attention and the pull request stays open             |
| Builder attempts in one mission         | 25                          | The same result, as an outer backstop                                         |

The spend limits are on the [LLM credentials](https://letsship.ai/docs/getting-started/llm-credentials) page, and the loop limits sit under `retries` in `ship.yml` ([Retry limits](https://letsship.ai/docs/configuration/retry-limits)). If you raise only `maxRetries`, usually nothing changes, because a stuck mission hits an oscillation limit long before the ceiling.

SHIP prices every stage from the list rates of the model that served each call, not from what the harness reports about itself. A role on a larger model can cost about 5 times as much. Read the per-stage cost of your first few Claude Code missions before you move a role up a model size.

To run an open-weight model as the builder instead, the same setup works with a different harness: see [Run the pi coding agent headless](https://letsship.ai/blog/run-pi-coding-agent-headless).

## Questions

### What is Claude Code headless mode?

Claude Code headless mode is the non-interactive run you start with claude -p, or --print, and a prompt. Anthropic recommends --bare for scripts and CI. In SHIP, no terminal is attached to Claude Code. A mission starts from an issue or a CLI hand-off, and SHIP stores the full session for you to read.

### How do I sandbox Claude Code?

Claude Code has a Bash sandbox that is off by default and covers shell commands only. Anthropic suggests a container or virtual machine for one boundary around everything. SHIP runs each Claude Code stage in an isolated sandbox that never holds your Anthropic key and is gone when the stage finishes.

### Can I run Claude Code in Docker?

You can run Claude Code in a container yourself, which Anthropic's sandbox docs suggest for one boundary around the whole process. In SHIP you do not build that container. To add tools, you write a custom sandbox as a short Dockerfile with no FROM line, and every mission reuses its snapshot.

### Can headless Claude Code use my Claude subscription?

Claude Code's bare mode does not use a subscription login and needs ANTHROPIC_API_KEY. SHIP accepts an Anthropic subscription credential in addition to an API key. SHIP verifies it with a live call when you save it, and never puts it inside the sandbox.

### Which model should the Claude Code builder use?

In SWE-in-a-team, claude-sonnet-5 and claude-opus-5 on claude-code each resolved all 20 tickets. They cost $4.31 and $5.48 per resolved ticket. claude-fable-5 resolved 19 at $6.44, and claude-haiku-4-5 resolved 18 at $4.23.

### How much does a headless Claude Code mission cost?

SHIP records what every stage of a mission cost, priced from list rates for the model that served each call. A single mission stops at $5 by default, and pauses at 2 credits or 6 agent-hours until a member of your organization approves more.
