
Claude Code Self-Hosted Environment Feature Arrives: Trying an EC2 Instance as the Execution Environment
This page has been translated by machine translation. View original
製造ビジネステクノロジー部 This is Kazue.
In Claude Code v2.1.224 released on August 7th, self-hosted environments became available as execution environments for Claude Code on the Web. As of August 12, 2026, this is a public beta feature available only on Team and Enterprise plans.
Cloud sessions for Claude Code on the Web run by default on Anthropic-managed infrastructure. Using a self-hosted environment allows you to bring that session execution portion into your own managed network.
What's the benefit
- Network access: Since sessions run within your own network, you can access internal services, databases, registries, etc. without exposing them to the public internet
- Custom tools: Compilers, SDKs, internal CLIs, etc. can be pre-installed in the runner image
- Compliance: Repository checkout contents and build artifacts can remain within your own managed infrastructure (however, the session content itself is sent to
api.anthropic.comfor model inference)
Conversely, unless you have requirements like those above, the Anthropic-hosted environment—which requires no infrastructure setup or operation—is more suitable. When choosing self-hosting, you take on operational costs such as creating and maintaining runner images, operating runners, and managing networks.
Understanding the concepts

※ The above diagram can be enlarged by clicking on it
The main concepts involved are as follows.
- Environment (Environment / Pool): A named group that serves as the destination for cloud sessions. Created in the claude.ai organization settings, it groups multiple runners under it. Sessions are routed to an environment rather than to individual runners (meaning developers select an environment, not a runner, when starting a session)
- Environment secret: A shared credential that runners use for authentication and registration with the environment. Displayed only once when the environment is created (does not appear in the diagram above)
- Runner: A resident process running on a host within your own network. It registers with an environment, retrieves sessions via polling, and executes them
- Session: A single Claude Code task started by a developer. The runner launches Claude Code as a child process to execute it
The flow until a session is executed is as follows.
- When a developer selects a self-hosted environment when starting a cloud session, Anthropic's control plane queues the session for that environment
- A runner with available capacity retrieves the session from the queue, clones the repository, and launches Claude Code as a child process
- The child process communicates with Anthropic via HTTPS for event streaming and model inference
Key points to understand about network and data boundaries are as follows.
- All communication is outbound HTTPS from your own network; no inbound connections from Anthropic to your network are required
- Repository checkouts, build artifacts, secrets, and files created or modified by sessions remain in your own infrastructure
- On the other hand, the conversation content itself—prompts, responses, tool execution results—is sent to Anthropic for model inference, and session transcripts are stored on Anthropic's side
- Session orchestration, queuing, and the claude.ai UI continue to be hosted by Anthropic. In other words, what a self-hosted environment moves to your side is "session execution," not the control plane
Commands have been added
The claude self-hosted-runner command for configuring self-hosted environments and subcommands such as claude self-hosted-runner setup have been added.
※ The following are command execution results from v2.1.226.
% claude self-hosted-runner
error: No environment secret provided. Use --environment-secret-file or set SELF_HOSTED_RUNNER_ENVIRONMENT_SECRET.
Run 'claude self-hosted-runner --help' for usage.
% claude self-hosted-runner --help
Usage: claude self-hosted-runner [options]
Connection:
--api-url <url> API base URL (default: https://api.anthropic.com)
--environment-secret-file <path>
Path to environment secret file (or set SELF_HOSTED_RUNNER_ENVIRONMENT_SECRET)
(--pool-secret-file / SELF_HOSTED_RUNNER_POOL_SECRET are deprecated aliases.)
--lock-to-account <id> Lock runner to a single account at registration (webhook-driven on-demand
spawn). Only that account's sessions are assigned.
[env: SELF_HOSTED_RUNNER_LOCK_TO_ACCOUNT]
Runtime:
--capacity <n> Max concurrent sessions (default: 1)
--base-dir <path> Base directory for repo checkouts (default: /workspace)
[env: SELF_HOSTED_RUNNER_BASE_DIR]
--exec-path <path> Binary to spawn for child sessions. Default: this process's own binary.
[env: SELF_HOSTED_RUNNER_EXEC_PATH]
--hooks-dir <path> Directory of lifecycle hook scripts (checkout, command, post-session).
Absent hooks fall through to built-in behavior.
[env: SELF_HOSTED_RUNNER_HOOKS_DIR]
--session-stop-grace-sec <n>
How long to wait for the Claude process to exit cleanly after a
session ends, before force-killing it. The post-session hook runs
after this. Default: 5.
[env: SELF_HOSTED_RUNNER_SESSION_STOP_GRACE_MS, in ms]
--post-session-hook-timeout-sec <n>
SIGTERM budget for the post-session lifecycle hook, on every session
end including runner shutdown. Default: 60.
[env: SELF_HOSTED_RUNNER_POST_SESSION_HOOK_TIMEOUT_MS, in ms]
--drain-wait-sec <n> On SIGTERM/SIGINT, wait up to N seconds for each session's in-flight
turn (a foreground tool call) and running background tasks to finish
before sending the session process its SIGTERM. Adds N to the
advertised shutdown budget.
Default: 0 (send SIGTERM immediately). Max: 86400.
[env: SELF_HOSTED_RUNNER_DRAIN_WAIT_MS, in ms]
(--drain-wait-bg-tasks-sec is a deprecated alias for this flag.)
--git-ssh-rewrite <host> Rewrite https://<host>/... source URLs to git@<host>:... (repeatable).
For SSH-only git hosts.
--git-host-rewrite <f>=<t> Rewrite https://<f>/... source URLs to https://<t>/... (repeatable).
For split-horizon DNS where the runner reaches GHE via a different
hostname than the control plane. Applied before --git-ssh-rewrite.
--use-anthropic-git-proxy Clone via Anthropic's git proxy (uses the session creator's stored
GitHub OAuth token, or the org's GitHub App installation token for
bot/agent sessions; you don't manage git auth on the runner). Supersedes
--git-host-rewrite and --git-ssh-rewrite.
[env: CLAUDE_RUNNER_USE_GIT_PROXY=1]
--configure-git Set global git identity to Claude <noreply@anthropic.com> and enable
commit signing via Anthropic's signing service, matching 1P sessions.
Writes ~/.gitconfig at runner startup. Without this flag, your image
must provide its own git identity.
[env: SELF_HOSTED_RUNNER_CONFIGURE_GIT=1]
--push-outcome-on-release On a runner-initiated non-completed session end (SIGTERM drain,
idle-release, failed), push every tracked outcome branch to origin
before deleting it, so in-flight commits survive a runner restart.
Skipped on server-initiated deassign. On a resumed session (worker
epoch > 1), the prep path fetches any previously pushed outcome
branch from origin and continues from it, so histories stay
linear. CAVEAT: the resume-fetch trusts refs/heads/<outcome-branch>
on the source remote — anyone with push access to that ref can
place content into the resumed workspace; if your source revision
is protected but claude/* refs are not, that collaborator write
surface widens on resume. Repos checked out via the checkout
lifecycle hook are NOT pushed — use the post-session hook to
snapshot those. Adds 30s
(total, shared across all pushes) to the advertised
shutdown budget.
[env: SELF_HOSTED_RUNNER_PUSH_OUTCOME_ON_RELEASE=1]
--trust-workspace [bool] Seed persisted trust for each session's repo paths so repo-level
.claude/settings.json permissions.allow and additionalDirectories
are honored by the child. Default: true.
Set to false for cli#44151's stricter gate: repo-committed grants
are dropped with an "Ignoring N permissions.allow" stderr
diagnostic; configure host-level grants via the host-config dir's
settings.json permissions.allow (userSettings source) instead.
[env: SELF_HOSTED_RUNNER_TRUST_WORKSPACE=0 to disable]
--confine-repo-settings <mode>
Repo-committed-settings confine guard mode: warn (default) logs
a would-refuse diagnostic per violation and still spawns;
enforce refuses to spawn the session; off disables the scan.
Invalid values fail closed at startup.
[env: SELF_HOSTED_RUNNER_CONFINE_REPO_SETTINGS]
[env: SELF_HOSTED_RUNNER_HOST_CONFIG_DIR]
Directory seeded into each session's CLAUDE_CONFIG_DIR (settings,
agents/, skills/, …; runtime state excluded). Default: ~/.claude.
Point at an empty dir to disable.
--health-port <port> Port for /healthz HTTP listener (default: 8080). 0 disables.
[env: SELF_HOSTED_RUNNER_HEALTH_PORT]
--log-level <level> Log level: info or debug (default: info)
--log-file <path> Tee runner logs to a file in append mode. Stdout is unchanged.
[env: SELF_HOSTED_RUNNER_LOG_FILE]
Runner lifecycle:
--exit-if-unused-min <n> Exit the runner if never assigned work for N min (autoscaler scale-down).
Default: never. Max: 10080.
[env: SELF_HOSTED_RUNNER_IDLE_SHUTDOWN_MS, in ms]
--drain-grace-sec <n> Default: 0 — exit immediately after active sessions finish, WITHOUT
polling for more (one-shot when --capacity=1).
Set a positive value (e.g. 30) to keep the runner warm and re-poll the
locked account's queue for that many seconds before exiting.
Max: 604800. [env: SELF_HOSTED_RUNNER_DRAIN_GRACE_MS, in ms]
--retire-at <epoch-seconds> Retire the runner at the given wall-clock time (absolute Unix timestamp, in
seconds): release every active session through the ReleaseSession path that
--release-idle-session-min uses (the session parks server-side and a fresh
runner picks it up on the user's next message), stop taking new work, and
exit 0 once the slots are empty. A session still mid-turn at that time is
released as soon as its turn ends; background work a finished turn left
running gets up to 60s of grace, then the session parks anyway (perpetual
monitor tasks don't hold it at all). Use this when the host hard-kills the
runner at a known time (e.g. a sandbox lifetime cap): set it far enough
before the kill to cover typical turns PLUS the per-session shutdown
budget (--session-stop-grace-sec, the push-outcome window, the full
--post-session-hook-timeout-sec, the 60s background-work grace, one poll)
so sessions park cleanly and the post-session hook isn't truncated by
the kill. Default: never. [env: SELF_HOSTED_RUNNER_RETIRE_AT, in seconds]
Per-session watchdogs:
--release-idle-session-min <n> Release a session slot after N min of no user input (turn finished,
or parked at a permission prompt, user idle). Runner exits if this
drops it to zero active sessions.
Default: never. Max: 10080.
[env: SELF_HOSTED_RUNNER_SESSION_IDLE_MS, in ms]
--startup-timeout-min <n> Release a session slot if the child has not completed initialization
N min after spawn — covers a child hung during --resume hydration or
MCP connect, and a session assigned with no pending input. Cleared
once the child emits system:init, after which --release-idle-session-min
takes over. Default: 15. 0 disables. Max: 10080.
[env: SELF_HOSTED_RUNNER_STARTUP_TIMEOUT_MS, in ms]
--kill-session-after-min <n> SIGTERM a session child after N min wall-clock (runaway backstop).
If a turn is in flight at the deadline, the kill is deferred until
the turn finishes, with a hard cap of 15 min past the deadline
(override: SELF_HOSTED_RUNNER_MAX_LIFETIME_GRACE_MS, in ms).
Default: never. Max: 10080.
[env: SELF_HOSTED_RUNNER_MAX_LIFETIME_MS, in ms]
Debug:
--debug-token-dir <path> DEBUG ONLY — writes live tokens to disk. Do not use in production.
[env: SELF_HOSTED_RUNNER_DEBUG_TOKEN_DIR]
--help, -h Show this help message
When I ran % claude self-hosted-runner setup, the following session was launched. I understood this to be a command that assists with setup work when executed within the runner. (You can see that similar steps to the "Let's try it" section below are returned as session responses)
❯ Start the self-hosted runner setup wizard. Greet me and begin Phase 1 (create an environment in the Admin UI). Walk me through one step at a time.
Starting the self-hosted runner setup wizard. Welcome! Let's begin by creating an environment in the Admin UI.
Step 1: Please create an environment in the Admin UI
1. Open https://claude.ai/admin-settings/claude-code in your browser
2. Scroll down to the Self-hosted environments section
3. Click Create environment and give it a name
4. Copy the environment secret that is displayed (be sure to copy it as it will only be shown on this screen)
5. Paste the copied secret into ./runner-setup/ENVIRONMENT_SECRET on this machine and save it (I will apply chmod 600 to it later)
6. Also copy the environment ID (the one starting with ccpool_)
When you are ready, please tell me the environment ID and say "done".
Let's try it
I'm testing this on the Team plan.
Based on the Self-hosted environments quickstart - Claude Code Docs quickstart guide, I'll build a configuration using an EC2 instance as the execution environment.
Enable the self-hosted environment in organization settings
The self-hosted environment is disabled by default, so it needs to be enabled. Owner or administrator permissions are required.
Turn on the toggle for Allow self-hosted environments at Organization Settings → Cloud Features. This will display the "Self-hosted environments" section at the bottom.

Create an environment
Click "+ New" in the upper right of the "Self-hosted environments" section on the same screen to create an environment. First, enter a name.

Save the secret displayed on the next screen.

Next screen. The environment has been created.

It is also displayed in the list.

Create an EC2 instance
I created one EC2 instance with Terraform using the following configuration.
- Placed in a private subnet
- NAT Gateway is set up to allow outbound communication
- OS is Ubuntu 24.04
- Instance type is m8i-flex.large
- Security Group
- No inbound rules (deny all)
- For outbound rules, looking at the network requirements, only 443/tcp needs to be fully open. However, since plain Ubuntu 24.04 uses http://archive.ubuntu.com (port 80) as the default apt mirror, port 80/tcp should also be allowed
- Attach the
AmazonSSMManagedInstanceCorepolicy to enable shell access via SSM Session Manager
Install Claude Code
Connect via shell using SSM Session Manager and run the following.
$ curl -fsSL https://claude.ai/install.sh | bash
Setting up Claude Code...
✔ Claude Code successfully installed!
Version: 2.1.226
Location: ~/.local/bin/claude
Next: Run claude --help to get started
⚠ Setup notes:
● Native installation exists but ~/.local/bin is not in your PATH. Run:
echo 'export PATH="$HOME/.local/bin:$PATH"' >> your shell config file && source your shell config file
✅ Installation complete!
$ echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.profile
$ . ~/.profile
$ claude -v
2.1.226 (Claude Code)
Actually, at this point there is an issue where claude command returns not found in a different session (~/.profile is not automatically loaded), but I'll ignore that for now.
Runner setup
First, create a location to place the secret issued at Organization Settings → Cloud Features.
$ mkdir ~/claude
Place the secret.
# Copy the secret value to the clipboard first
$ (umask 077 && cat > ~/claude/environment-secret)
# Paste the secret value → Enter → Ctrl-D to finish writing
Create the base directory.
$ mkdir ~/claude-runner
Now it's time to register the runner.
$ claude self-hosted-runner --environment-secret-file '/home/ssm-user/claude/environment-secret' --base-dir '/home/ssm-user/claude-runner'
2026-08-10T09:49:17.942Z [self-hosted-runner] [runner] This runner needs up to 80s to stop the Claude process and run the post-session hook on shutdown, and force-exits after 80s. Configure your process supervisor's stop timeout to at least 80s (e.g. terminationGracePeriodSeconds on Kubernetes, stop_grace_period on Docker Compose, TimeoutStopSec on systemd, or your platform's equivalent).
2026-08-10T09:49:17.942Z [self-hosted-runner] [runner] watchdog config: idle-release=disabled (not configured); startup-timeout=15m (default); kill-session-after=disabled (not configured); exit-if-unused=disabled (not configured)
2026-08-10T09:49:17.942Z [self-hosted-runner] Connecting to https://api.anthropic.com (capacity=1, baseDir=/home/ssm-user/claude-runner, execPath=/home/ssm-user/.local/share/claude/versions/2.1.226)
2026-08-10T09:49:18.276Z [self-hosted-runner] Registered: runner_id=ccrunner_01L9xnpeTinmjvs24jnxxxxx
2026-08-10T09:49:18.281Z [self-hosted-runner] [runner:health] /healthz and /metrics listening on :8080
2026-08-10T09:49:18.313Z [self-hosted-runner] [runner:startup] host config snapshot: 434 file(s), 5917.0 KiB from /home/ssm-user/.claude
2026-08-10T09:52:20.568Z [self-hosted-runner] [runner:health] polling ok · 0/1 slots · last_poll=20215ms ago · locked_account=no · runner_token expires in 3h · 0 sessions handled
2026-08-10T09:55:42.805Z [self-hosted-runner] [runner:health] polling ok · 0/1 slots · last_poll=20221ms ago · locked_account=no · runner_token expires in 3h · 0 sessions handled
After a while, the runner was recognized in the claude.ai Organization Settings → Cloud Features!

Try the Self-hosted Environment
Under "Self-hosted environment pool", the environment you created is now available for selection.

Let's select it and start a session with a simple prompt.
An error occurred.

Up to this point, we had been following the Self-hosted environments quickstart - Claude Code Docs, a quick start guide for using a local environment as a runner, but it seems there are some missing pieces when doing this with an EC2 instance.
The error message is as follows:
The session process failed to start. You can try again by sending a new message or starting a new session.
Last output before exit:
Repository access validation failed after 3 attempts: git ls-remote failed: fatal: could not read Username for 'https://github.com': terminal prompts disabled
When using the Git command, we're getting a could not read Username error. Indeed, nothing has been configured.
By specifying the --use-anthropic-git-proxy option, the clone process will go through Anthropic's Git proxy and use the GitHub or GitHub Enterprise OAuth token of the user who created the session. (Reference: Use the Anthropic git proxy) Let's try using this.
$ claude self-hosted-runner --environment-secret-file '/home/ssm-user/claude/environment-secret' --base-dir '/home/ssm-user/claude-runner' --use-anthropic-git-proxy
2026-08-12T07:01:31.733Z [self-hosted-runner] [runner] This runner needs up to 80s to stop the Claude process and run the post-session hook on shutdown, and force-exits after 80s. Configure your process supervisor's stop timeout to at least 80s (e.g. terminationGracePeriodSeconds on Kubernetes, stop_grace_period on Docker Compose, TimeoutStopSec on systemd, or your platform's equivalent).
2026-08-12T07:01:31.734Z [self-hosted-runner] [runner] watchdog config: idle-release=disabled (not configured); startup-timeout=15m (default); kill-session-after=disabled (not configured); exit-if-unused=disabled (not configured); bg-result-grace=30s (default)
2026-08-12T07:01:31.734Z [self-hosted-runner] Connecting to https://api.anthropic.com (capacity=1, baseDir=/home/ssm-user/claude-runner, execPath=/home/ssm-user/.local/share/claude/versions/2.1.228)
2026-08-12T07:01:32.048Z [self-hosted-runner] Registered: runner_id=ccrunner_01SQadmqMv8vRf7fWaNxxxxx
2026-08-12T07:01:32.052Z [self-hosted-runner] [runner:git] --use-anthropic-git-proxy: wiping HOME-level git config (/home/ssm-user/.gitconfig, /home/ssm-user/.config/git, /home/ssm-user/claude-runner/.runner) for cross-session isolation.Operator-provisioned git config must live in system config (/etc/gitconfig) or via --configure-git; see the self-hosted runners guide.
2026-08-12T07:01:32.062Z [self-hosted-runner] [runner:git] --use-anthropic-git-proxy: credential helper registered for https://api.anthropic.com (/home/ssm-user/claude-runner/.runner/git-proxy-cred)
2026-08-12T07:01:32.067Z [self-hosted-runner] [runner:health] /healthz and /metrics listening on :8080
2026-08-12T07:01:32.534Z [self-hosted-runner] [runner:startup] host config snapshot: 435 file(s), 5920.4 KiB from /home/ssm-user/.claude
2026-08-12T07:01:32.857Z [self-hosted-runner] Picked up session cse_01QgQaksiAiCKWQMszAFUQGa (1/1 active)
2026-08-12T07:01:40.362Z [self-hosted-runner] [runner:confine] WARN (would refuse): [runner:session] repo-committed operator-posture override entry 'env: {ENABLE_TOOL_SEARCH}' in /home/ssm-user/claude-runner/kazuemasaki/nippo/.claude/settings.json sets env vars that reach every child subprocess (BASH_ENV/LD_PRELOAD/NODE_OPTIONS/GIT_* are unsandboxed-exec inlets). Runner-level env belongs in the operator wrapper script or runner env, not repo settings — refusing to spawn.Host-specific write-scope entries belong in the operator's user-level settings.json (userSettings source, not trust-gated — see --trust-workspace docs).
2026-08-12T07:01:40.952Z [self-hosted-runner] [runner:session] cse_01QgQaksiAiCKWQMszAFUQGa child spawned pid=1582 cwd=/home/ssm-user/claude-runner/kazuemasaki/nippo
2026-08-12T07:01:40.953Z [self-hosted-runner] [runner:session] cse_01QgQaksiAiCKWQMszAFUQGa idle clock armed (startup): releases in 15m at 2026-08-12T07:16:40.953Z unless new activity arrives
The session that had errored resumed, the repository was cloned, and a response was successfully returned! 🙌

There are still many things↓ that need to be worked out for actual production use, but this concludes our verification for now.
Known Issues at This Point (= Areas That Need Further Configuration)
- The number of sessions that can run concurrently in this self-hosted environment is limited to 1
- This is because only 1 runner has been created and the
--capacityoption for specifying the number of parallel sessions within a runner has not been specified, so the default value of 1 is set
- This is because only 1 runner has been created and the
- Sessions do not become inactive, making it impossible to start the next session
- This can also be resolved with options for
claude self-hosted-runner
- This can also be resolved with options for
- (As mentioned earlier,)
~/.profileis not read, so theclaudecommand (especially theclaude self-hosted-runnercommand required for runner setup) cannot be used in new Session Manager sessions - Systemd-ifying the
claude self-hosted-runnercommand- With the current approach, when the Session Manager session running
claude self-hosted-runnerends, this EC2 instance will no longer be recognized as a runner. It should be systemd-ified to run continuously in the background
- With the current approach, when the Session Manager session running
Summary
We actually built a self-hosted environment for Claude Code on the Web using a single EC2 instance.
The concept itself is simple: register a runner to an "environment", and the runner polls for sessions and launches a child process of Claude Code.
Teams with requirements for a closed development environment within their own network might want to consider adopting this. However, please keep in mind, as mentioned repeatedly, that this is currently a public beta feature.
References
- Self-hosted environments - Claude Code Docs
- Self-hosted environments quickstart - Claude Code Docs
- Deploy self-hosted environments to production - Claude Code Docs
- Customize sessions in self-hosted environments - Claude Code Docs
- Test self-hosted environments end to end - Claude Code Docs
- Self-hosted environments reference - Claude Code Docs
- Verify session identity in self-hosted environments - Claude Code Docs
