Claude Code Self-Hosted Environments
Claude Code self-hosted environments run cloud sessions — the ones developers start from claude.ai, the mobile and desktop apps, claude --cloud, and scheduled routines — on compute you operate instead of Anthropic’s. Anthropic publishes no runner image and leaves provisioning to you, so the runner host can be a Sprite created per session and destroyed when it finishes.
How it works
Section titled “How it works”Self-hosting has three parts:
- An environment is a named destination, created in claude.ai admin settings, that appears in the environment picker when a developer starts a session. Its ID has the form
ccpool_…. - A runner is a process you deploy. It registers with the environment, polls for work, and spawns a child Claude Code process per session. The runner is a subcommand of the standard
claudebinary, not a separate download. - A session is one Claude Code task.
You can run a fixed fleet of runners, or run the orchestrator, a second process that polls Anthropic for queued sessions and invokes a spawn-runner hook once per session. The orchestrator is the better fit for Sprites, and it is also the posture Anthropic recommends: on a fixed fleet the environment secret sits on every host that runs user code, where any session can read it, while the orchestrator keeps the secret on a host that never runs user code and hands each runner a single-use work order that registers exactly one runner.
All traffic is outbound. The runner polls api.anthropic.com; Anthropic never connects in.
Before you begin
Section titled “Before you begin”You need:
- A Claude Team or Enterprise organization with Allow self-hosted environments turned on by an Owner or admin on the Cloud environments page. The feature is in public beta and off by default.
- An environment and its environment secret. Create the environment on that page; claude.ai shows the secret once and it expires after 365 days.
- A Sprites API token (
org-slug/org-id/token-id/token-value). Export it asSPRITE_TOKEN. - The
spriteCLI, authenticated, on the orchestrator host.
Build the runner image
Section titled “Build the runner image”Anthropic doesn’t publish a runner image, so the runner host has to carry the claude binary and a git identity. On Sprites this is short: git, curl, ca-certificates, and openssh-client are already in the base image, so only the binary and the git config are left.
curl -fsSL "https://downloads.claude.ai/claude-code-releases/2.1.226/linux-x64/claude" \ -o /usr/local/bin/claudechmod +x /usr/local/bin/claude
git config --system user.name "Claude"git config --system user.email "noreply@anthropic.com"git config --system --add safe.directory '*'Pin the version rather than tracking latest. Each session’s child process runs the runner’s own binary and the runner disables auto-update inside sessions, so the version you install is the version every session runs until you replace it. The runner requires 2.1.224 or later; earlier builds don’t recognize the self-hosted-runner subcommand.
Provision a runner per session
Section titled “Provision a runner per session”The orchestrator runs ${hooks-dir}/spawn-runner once per queued session. The hook must submit the workload and return within --hook-timeout (60 seconds by default) without waiting for the runner to boot.
Save this as spawn-runner in your hooks directory and make it executable:
#!/bin/shset -eu
: "${CLAUDE_RUNNER_WORK_ORDER_FILE:?no work order}": "${CLAUDE_RUNNER_ORDER_ID:?no order id}"
# The order ID is unique per spawn request and safe as a resource name, so# using it as the Sprite name makes a redelivered request collide with the# Sprite it already created instead of provisioning a second runner.SPRITE="ccr-$(printf '%s' "$CLAUDE_RUNNER_ORDER_ID" | tr '[:upper:]_' '[:lower:]-' | cut -c1-40)"
if ! sprite create --skip-console "$SPRITE" >/dev/null 2>&1; then # Already provisioned by an earlier delivery of this same order: report # success, because the runner it started is the one this order wanted. if sprite exec -s "$SPRITE" -- true >/dev/null 2>&1; then echo "order $CLAUDE_RUNNER_ORDER_ID already spawned $SPRITE" >&2 exit 0 fi echo "could not provision sprite for order $CLAUDE_RUNNER_ORDER_ID" >&2 exit 1fi
# One exec does the rest. Each `sprite exec` is a round trip, and on a hook# with a 60-second budget the number of round trips matters more than the# work inside them.## The work order is a single-use JWT that registers exactly one runner. It# arrives on stdin so it never reaches argv or a process listing, and the# orchestrator deletes its own copy as soon as this hook exits.sprite exec -s "$SPRITE" -- sudo sh -c " set -e mkdir -p /etc/claude umask 077 && cat > /etc/claude/work-order curl -fsSL 'https://downloads.claude.ai/claude-code-releases/2.1.226/linux-x64/claude' \ -o /usr/local/bin/claude chmod +x /usr/local/bin/claude git config --system user.name 'Claude' git config --system user.email 'noreply@anthropic.com' git config --system --add safe.directory '*' mkdir -p /workspace chown sprite:sprite /workspace /etc/claude/work-order /.sprite/bin/sprite-env services create claude-runner \ --cmd /usr/local/bin/claude \ --args self-hosted-runner,--environment-secret-file,/etc/claude/work-order,--base-dir,/workspace,--capacity,1,--use-anthropic-git-proxy" < "$CLAUDE_RUNNER_WORK_ORDER_FILE" >/dev/null
exit 0Start the orchestrator on a host that never runs sessions, pointing it at the environment secret and the hooks directory:
claude self-hosted-runner orchestrator \ --environment-secret-file /etc/claude/environment-secret \ --hooks-dir /etc/claude/hooks \ --expected-spawn-seconds 30What the hook has to get right
Section titled “What the hook has to get right”Exit codes are a contract. 0 means submitted, 1 means retryable and the session is re-offered after a backoff, and 2 or higher blocks that session from spawning again until an admin selects Retry in the environment’s Activity tab. The tail of the hook’s stderr is shown there as the failure reason, so write something actionable to stderr and never write secrets.
Don’t retry the workload yourself. One order ID means at most one created runner. If a runner never registers, Anthropic re-requests with a fresh order ID after --expected-spawn-seconds.
Set --expected-spawn-seconds to your p99 boot time. It is a server-side lease, not a hint, and every orchestrator replica must use the same value. A Sprite that installs the binary on demand registers in well under ten seconds, so the 120-second default is far more headroom than this path needs; lowering it makes a genuinely failed spawn get re-offered sooner.
Use --capacity 1. A session-bound work order registers exactly one runner bound to one session, so extra slots would never receive work. It is also required by --use-anthropic-git-proxy.
Git credentials
Section titled “Git credentials”--use-anthropic-git-proxy clones through Anthropic using the session creator’s own stored GitHub token, which means the Sprite needs no git credentials at all: no deploy keys, no credential helper, no .netrc. That suits a per-session Sprite, where any credential baked into the image would be readable by every session that image ever runs. It requires git 2.32 or newer and --capacity 1, and the runner refuses to start if either is unmet.
The proxy fetches from Anthropic’s side, so it needs your git host to be reachable from Anthropic. For a git host that is only routable inside your own network, drop the flag and supply a checkout lifecycle hook that clones however you need.
Keep the Sprite alive for the session
Section titled “Keep the Sprite alive for the session”A runner with an active session is busy, but it takes no inbound traffic, so the Sprite counts as quiet and would pause on its idle window, stalling the session mid-turn. Hold the Sprite active with a Task for as long as the runner is working, and drop it when the runner exits.
Wrap the runner in a small command rather than invoking the binary directly:
sprite-env curl -X POST /v1/tasks -d '{"name": "claude-runner", "expire": "5m"}'( while true; do sprite-env curl -X PUT /v1/tasks/claude-runner -d '{"expire": "5m"}' sleep 60 done ) & heartbeat=$!
/usr/local/bin/claude self-hosted-runner \ --environment-secret-file /etc/claude/work-order \ --base-dir /workspace --capacity 1 --use-anthropic-git-proxy
kill "$heartbeat" 2>/dev/null || truesprite-env curl -X DELETE /v1/tasks/claude-runner || trueThe short expiry is the crash-safety net: if the runner dies without cleaning up, the task expires on its own and the Sprite is free to pause instead of being held open indefinitely.
This is also why a Sprite doesn’t need --retire-at, which exists for hosts that are destroyed at a known wall-clock time without a signal. The task is dynamic — it lasts exactly as long as the work does — where --retire-at requires guessing a deadline in advance.
Network egress
Section titled “Network egress”Sessions run model-directed code, so restrict what they can reach with a network policy rather than relying on the session’s own permission settings. Sessions need api.anthropic.com for the control plane, session streaming, and model inference; your git host, unless you use the Anthropic git proxy; and whichever internal services the sessions are there to reach.
Anthropic’s network requirements list the conditional hosts — plugin marketplaces, documentation lookups, npm for MCP servers — that you only need if you use those features.
Clean up
Section titled “Clean up”The hook creates one Sprite per session and nothing removes it. A paused Sprite stops compute billing but keeps its filesystem, and its storage cost, so an active environment accumulates them. Delete each one once its session finishes with DELETE /v1/sprites/{name}, from a post-session lifecycle hook or a periodic sweep over Sprites whose claude-runner service has stopped.
Destroying the Sprite per session is also the isolation posture Anthropic asks for: a runner executes model-directed code on behalf of any member of your organization, and dispatch into an environment is organization-wide with no per-environment access control.
Troubleshooting
Section titled “Troubleshooting”| Symptom | Likely cause | Check / fix |
|---|---|---|
Environment creation returns 403 permission_error | Allow self-hosted environments is off for the organization | An Owner or admin turns it on from the Cloud environments page. It also requires Claude Code on the web to be enabled. |
Runner exits immediately with [runner:fatal] RegisterRunner auth failed | The work order or environment secret is invalid, revoked, or expired; or the host clock is more than five minutes off | Read /.sprite/logs/services/claude-runner.log. Authentication fails on clock skew, so check the clock before rotating the secret. |
Service crash-loops with cannot create or write to base directory /workspace (EACCES) | Services run as the unprivileged sprite user, and --base-dir was created by root | chown sprite:sprite /workspace. The runner fails at startup rather than at session pickup, so the log shows this before any session arrives. |
| Sessions stay queued and no Sprite appears | The orchestrator isn’t running, or the hook is failing | Check the orchestrator’s /healthz body for queue counts, then the environment’s Activity tab, where a failed session shows the hook’s stderr and a Retry button. |
| Sessions fail right after pickup | Missing build tools in the image, or a git credential problem | Open the session at claude.ai/code for the error. The runner preserves the session’s debug log and prints its path in the runner log. |
| A long session stalls mid-turn | The keep-alive task is missing or expired, so the Sprite paused | Inside the Sprite, sprite-env curl -s /v1/tasks should show claude-runner with a future expiry while a session is active. |
| Runner logs a warning about capacity at startup | --capacity above 1 with a session-bound work order | Set --capacity 1; the extra slots can never receive work. |
See also
Section titled “See also”- Self-hosted environments: Anthropic’s reference for the runner, orchestrator, and hook contracts.
- Claude Managed Agents: the other Anthropic integration, where only tool execution runs in your Sprite.
- Services: supervised long-running processes inside a Sprite.
- Keeping Sprites running: tasks and the idle window.
- Checkpoints: snapshot a prepared Sprite to skip cold-start setup.