Shipped in v2.1.224 (August 7, 2026), self-hosted environments let a Team or Enterprise org run Claude Code’s cloud sessions — the ones normally started from claude.ai, the mobile app, or claude --cloud — on infrastructure they operate instead of Anthropic’s. The developer experience barely changes: same claude.ai picker, same mobile app, same session UI. What changes is where the container actually executes.
The name causes a specific kind of confusion worth clearing up first: “self-hosted” here does not mean self-hosted inference. Model calls still go to api.anthropic.com — there’s no way to point a self-hosted environment at Bedrock, Vertex AI, Microsoft Foundry, or an LLM gateway. What moves onto your infrastructure is code execution: the clone, the file edits, the shell commands, the build. If you came here looking for how to run Claude Code against a self-hosted model, this isn’t that feature — see the Bedrock/Vertex/Foundry setup in the GitHub Actions guide instead.
This guide walks through the architecture, setup, the production-hardening checklist Anthropic’s own docs treat as mandatory, and the gotchas that don’t show up until you’ve run it for a few weeks. Everything below is sourced from the official docs at code.claude.com/docs/en/self-hosted-environments and its linked pages, current as of v2.1.226.
The three-part model: environment, runner, session
Three terms recur throughout the docs, and mixing them up makes the rest hard to follow:
- Environment — a named destination for cloud sessions, created by an Owner/admin on the Cloud environments admin page. Internally it’s still called a
poolin metrics and API fields (pool_id, formccpool_...) — a leftover from before the public docs renamed it. - Runner — a long-lived process you deploy inside your network. It registers with an environment, polls for queued sessions, and executes them, the same idea as a self-hosted CI runner.
- Session — one Claude Code task a developer started. Each session becomes a child process the runner spawns.
The flow: a developer picks your environment from the session-start picker (alongside Anthropic-hosted options). The control plane queues the session; a runner with free capacity claims it, clones the repo, and spawns a child claude process. That child streams events back to api.anthropic.com over outbound HTTPS, and the poll loop doubles as a heartbeat — if a runner stops polling for ~60 seconds, the session gets requeued to another runner.
Critically, every connection is outbound from your network. Anthropic never connects into your network. The queue, the session’s event stream, and model inference all go out to api.anthropic.com; nothing comes in.
One rule shapes almost everything else about operating this: a runner locks to one user’s account on its first session and only serves that account until it exits. This means your minimum fleet size is the number of developers you expect active at once, not some smaller shared pool — --capacity controls how many concurrent sessions from that one user a runner can run, not how many different users.
Self-hosted environments vs. Remote Control vs. Anthropic-hosted cloud sessions
Anthropic now has three genuinely different ways to run Claude Code away from a plain local terminal, and the names invite confusion:
| Self-hosted environments | Remote Control | Anthropic-hosted cloud sessions | |
|---|---|---|---|
| Where code executes | Your infrastructure (runner you deploy) | Your own machine, already running | Anthropic-managed cloud |
| Plans | Team/Enterprise only, opt-in beta | Pro, Max, Team, Enterprise | All plans with cloud sessions enabled |
| Setup required | Runner fleet, environment secret, git credentials | claude remote-control on an already-running local session | None — pick “Anthropic” in the environment picker |
| What you get | Internal network access, custom tooling, compliance control over checkouts | Steer a local session from phone/browser | Zero-infra execution, no local dependency |
| Model inference | api.anthropic.com (fixed) | Wherever the local session’s config points | api.anthropic.com (fixed) |
If your team doesn’t use cloud sessions at all — everyone works from a terminal or IDE — none of this applies; local sessions always run on the developer’s own machine regardless. If you want to drive an always-on personal machine from your phone, that’s Remote Control, and it’s available on Pro/Max, not just Team/Enterprise. Self-hosted environments solve a different problem: teams whose network, tooling, or compliance posture won’t let any session — even a cloud one someone kicks off from their phone — touch infrastructure Anthropic operates.
Availability and what’s excluded
- Plans: public beta, Team and Enterprise only. Off by default — an Owner/admin has to turn on Allow self-hosted environments on the Cloud environments admin page, which itself requires Claude Code on the web to already be enabled org-wide.
- Zero Data Retention: not available if your org has ZDR enabled.
- Model routing: fixed to the Anthropic API. Cannot route through Bedrock, Vertex AI’s Agent Platform, Microsoft Foundry, or an LLM gateway.
- Surfaces: works for sessions from Claude Code on the web, mobile, desktop, scheduled routines, and
claude --cloud. Claude Tag, Claude Security, and Code Review sessions don’t route to self-hosted environments yet. - Repositories: GitHub only (github.com or GitHub Enterprise Server) at launch.
- Billing: identical to Anthropic-hosted sessions — self-hosting doesn’t change your usage metering.
Quickstart
Anthropic ships a guided setup as an interactive Claude Code session — it walks through creating the environment, saving the secret, starting a local runner, and confirming registration, then writes a cheat sheet to ./runner-setup/CHEAT-SHEET.md:
claude self-hosted-runner setup
This needs claude auth login with an Owner/admin account and Claude Code v2.1.224+. Where an interactive session isn’t possible, do it manually:
# 1. Confirm the host is ready (v2.1.224+ prints runner usage text)
claude self-hosted-runner --help
# 2. Create the environment in claude.ai admin settings, copy the
# environment secret (shown once, expires after 365 days), then:
mkdir -p /etc/claude
(umask 077 && cat > /etc/claude/environment-secret) # paste secret, Ctrl-D
# 3. Create a writable base directory for checkouts
mkdir -p '<writable-dir>'
# 4. Start the runner
claude self-hosted-runner \
--environment-secret-file '/etc/claude/environment-secret' \
--base-dir '<writable-dir>'
The environment’s status flips from “No runners deployed” to “Healthy” within a few seconds. Start a session at claude.ai/code, pick your environment from the picker, and the runner logs Picked up session <session-id> once it claims it.
Once a session is running, message it from any machine where you’re logged in — it doesn’t need to be the runner host:
claude -p "your message" --cloud <session-id>
Production hardening (Anthropic treats this as mandatory, not optional)
The deploy docs are blunt about the threat model: “a self-hosted runner executes arbitrary, model-directed code on your infrastructure on behalf of any member of your Anthropic organization.” Before pointing an environment at real systems:
- Ephemeral, per-session containers. Run each runner in a fresh container/VM destroyed on exit, with
--capacity 1and default--drain-grace-sec 0, so one container ever serves exactly one session. Anything higher shares a filesystem across sessions from the same locked account. - No broad credentials baked into the image. No long-lived SSH keys or cloud credentials with more access than a session needs. Mint per-session credentials from a wrapper script, or use
--use-anthropic-git-proxyfor the initial clone. - Keep the environment secret off session-running hosts. On a fixed fleet, the secret sits on every runner host, readable by any session’s code. Prefer on-demand runners (the orchestrator subcommand): the secret stays on the orchestrator, which never runs user code, and each spawned runner gets a single-use work order instead.
- Default-deny network egress, enforced at your own network boundary — the product can’t do this for you. Session code is model-directed and will attempt arbitrary outbound connections; this applies regardless of permission mode, since
Bashis pre-approved by default. - Block the cloud metadata endpoint (
169.254.169.254) from session containers explicitly — subnet-level egress rules don’t intercept link-local traffic. IMDSv2 with hop limit 1, or GKE Workload Identity with metadata concealment. - Dispatch is organization-wide. Any org member can send a session to any environment — there’s no per-environment access control on dispatch. Treat every runner host as reachable by every org member;
--lock-to-accountbounds which account’s sessions a host executes, but not who can dispatch to it. - Enforce the repo-settings guard with
--confine-repo-settings enforce(default iswarn, which logs but still runs). It scans committed repo settings for grants that escape the session’s own workspace —additionalDirectories,permissions.allowwrite rules,sandbox.filesystem.allowWrite/allowRead, non-emptyenvblocks, orsandbox.enabled: false.
None of this is enforced by the product itself — every item on this list is something you configure at your network boundary or in your runner flags.
Configuring git access
Three approaches, in increasing order of how much credential management you own:
| Approach | How | Git version floor | Notes |
|---|---|---|---|
--configure-git | Runner writes user.name/user.email matching Anthropic-hosted sessions, plus SSH commit signing via Anthropic’s signing service | 2.34+ | Doesn’t set push credentials — you still provide those |
| Ship config in your image | git config --system user.name/user.email in your Dockerfile, your own bot identity | 2.24+ | Never bake long-lived push tokens into a shared image — mint per-session instead |
--use-anthropic-git-proxy | Clones through api.anthropic.com, authenticated with the session’s own short-lived token — the same path Anthropic-hosted sessions use | 2.32+ | Requires --capacity 1; needs your git host reachable from Anthropic’s infrastructure, so it doesn’t work for internal-only git hosts |
For a git host that’s only reachable inside your network, use --git-host-rewrite <from>=<to> (split-horizon DNS) or --git-ssh-rewrite <host> instead of the proxy — both are ignored when the proxy is enabled.
Runner CLI flags worth knowing
The full reference lists ~20 flags; these are the ones that actually change behavior in a typical deployment (run claude self-hosted-runner --help for the authoritative, version-specific list):
| Flag | Default | What it does |
|---|---|---|
--environment-secret-file <path> | required | Path to the environment secret (or, for orchestrator-spawned runners, a single-use work-order JWT) |
--base-dir <path> | /workspace | Where checkouts and per-session directories live. Must be identical across every runner in an environment — mismatched values break session resume, since the resumed session’s working directory changes and recorded absolute paths go stale |
--capacity <n> | 1 | Max concurrent sessions per runner, all from the same locked account |
--drain-grace-sec <n> | 0 | 0 = exit immediately once active sessions finish (needed for per-session isolation); positive = keep polling the locked account’s queue for N more seconds |
--exit-if-unused-min <n> | 0 | Exit after N minutes of polling with zero work assigned — for autoscaler scale-down |
--kill-session-after-min <n> | 0 | Hard wall-clock kill for stuck sessions. Pair this with --release-idle-session-min, since some sessions (an unfinished background task, or one waiting on an approval from inside a running tool call) never register as idle |
--release-idle-session-min <n> | 0 | Release a session’s slot after N minutes of inactivity |
--push-outcome-on-release | off | Best-effort push of a session’s outcome branch before the workspace is deleted on release, so resumed sessions don’t lose unpushed work. Restrict push access to claude/* refs before enabling — on resume the runner fetches the branch without verifying who pushed it |
--retire-at <epoch-seconds> | unset | For infra that kills hosts at a known wall-clock time without a signal (spot reclamation, sandbox lifetime caps) — the runner releases sessions cleanly before that time instead of leaving a silent crash for the control plane to detect |
--confine-repo-settings <mode> | warn | warn / enforce / off — see hardening checklist above |
--trust-workspace [bool] | on | Set false to ignore repo-committed permissions.allow/additionalDirectories grants and rely on host-config settings.json instead. Repo-committed sandbox.* settings still get scanned by the repo-settings guard either way |
--lock-to-account <id> | unset | Pre-lock a runner to a specific account at startup instead of locking on first session |
Fixed fleet vs. on-demand orchestrator
Two ways to scale:
- Fixed fleet — a static set of replicas, scaled manually on the Prometheus metrics each runner exposes.
- On-demand — run
claude self-hosted-runner orchestrator, which polls for queued sessions with no runner available and invokes aspawn-runnerhook you provide to boot one per session, tearing it down after. This is also the recommended path for the hardening item above: the environment secret lives only on the orchestrator, never on a host that runs session code.
Remember the one-user-per-runner lock: --capacity scales sessions within one user, not across users, so orchestrator autoscaling still needs to spin up a distinct runner per concurrently-active account.
Monitoring
Each runner serves GET /healthz and GET /metrics (Prometheus) on --health-port (default 8080). /healthz returns 200 for any alive process — it doesn’t tell you if the poll loop is stuck, so alert on the metric instead:
claude_code_self_hosted_runner_last_poll_age_seconds— alert above 60sclaude_code_self_hosted_runner_active_sessionsvs.claude_code_self_hosted_runner_capacity— utilizationclaude_code_self_hosted_runner_locked_account{email}— which account a runner is currently locked to; drop or hash this label at scrape time if your metrics store is broadly readableclaude_code_self_hosted_orchestrator_queue_circuit_broken_sessions— should stay at zero; nonzero means aspawn-runnerhook is repeatedly failing and needs a manual retry from the Activity tab
Known limitations
- Connector traffic (GitHub, Slack, Linear, etc.) doesn’t stay in your network. Connectors are invoked from Anthropic’s side, so that traffic routes through
api.anthropic.comregardless of where the session executes. To keep a connector out entirely, filter it viaallowedMcpServers/deniedMcpServers; run the equivalent tool as a local MCP server on the runner image if it needs to stay internal. - Resumed sessions lose unpushed work by default. Releasing a session (idle timeout, runner restart) and resuming it clones fresh from the starting branch — anything not pushed is gone.
--push-outcome-on-releasemitigates this but doesn’t cover a dirty working tree or per-session config. - Private repos can’t be added mid-session — credentials aren’t provisioned for a repo added after the session starts. Select everything a session needs up front.
- A newly connected connector doesn’t appear in an already-running session. Connect it in Settings, then start a fresh session.
Should you actually use this?
Anthropic’s own framing is direct: “most teams are better served by Anthropic-hosted environments, which need no infrastructure to run or maintain.” Self-hosting makes sense when:
- Sessions need to reach internal services, databases, or registries that can’t be exposed to the public internet
- Compliance requires checkouts and build artifacts to stay on infrastructure you control (the conversation itself — prompts, responses, tool results — still goes to
api.anthropic.comfor inference either way) - You already run a self-hosted CI runner fleet and can extend the same operational muscle to Claude Code
It doesn’t make sense if you’re trying to avoid sending code to Anthropic at all (inference still happens on their side, full stop), or if you just want to drive a personal machine from your phone (that’s Remote Control, and it’s simpler, available on Pro/Max, and needs no fleet to operate).
FAQ
Does a self-hosted environment let me use a different model provider?
No. Model inference is fixed to api.anthropic.com; Bedrock, Vertex AI, Microsoft Foundry, and LLM gateways aren’t supported for self-hosted sessions.
What’s the difference between self-hosted environments and Remote Control? Remote Control steers a session already running on your own machine, from your phone or browser — no fleet, no environment secret, available on Pro/Max. Self-hosted environments run cloud sessions (the kind started from claude.ai or mobile with no local process involved) on infrastructure you deploy — Team/Enterprise only, opt-in, requires operating a runner fleet.
Is code ever sent to Anthropic in a self-hosted environment?
The conversation — prompts, responses, tool results — goes to api.anthropic.com for model inference either way, and Anthropic stores the transcript so the session can resume from another surface. What stays on your infrastructure is the repository checkout, build artifacts, and anything a session writes to disk.
How many runners do I need?
At minimum, one per developer you expect to have an active session at once — a runner locks to a single account on its first session and won’t serve anyone else until it exits. --capacity adds parallelism within one account, not across accounts.
Can I run this on Windows? Not as a runner host — Linux or macOS only (run it in a Linux container if your infrastructure is Windows-based). Developer workstations aren’t affected either way, since sessions start from a browser or app, not the runner host.
Related Reading
- Claude Code Remote Control: Complete 2026 Guide
- Claude Code Cross-Session Messaging: The Complete Guide
- Claude Code GitHub Actions: Complete CI/CD Integration Guide
- Claude Code Sandbox and Isolation Settings: Complete Guide
- Claude Code Monitoring and Observability
Browse how real teams configure Claude Code’s permissions, hooks, and sandboxing in our rules gallery.
Source: code.claude.com/docs/en/self-hosted-environments and its linked quickstart, deploy, and reference pages — official Anthropic documentation, current as of Claude Code v2.1.226 (August 2026).