Run a Claude Code self-hosted runner
Claude Code cloud sessions, running on a VM you control.
Shelley handles the setup. About 10 minutes.
Prefer a terminal?
curl -fsSL https://exe-staging.dev/deploy/claude-code-runner/prompt.txt | ssh exe.dev new --prompt=/dev/stdinClaude Code's cloud sessions
are the ones you start from claude.ai/code, the
Claude mobile and desktop apps, or claude --cloud in a terminal. They
normally run on Anthropic's machines. A
self-hosted environment
sends them to machines you run instead: you create the environment on
claude.ai, and a runner, claude self-hosted-runner, picks up each session
that someone starts in it, clones the repository, and runs Claude Code
there.
This page makes that runner an exe.dev VM. Sessions get a whole Ubuntu machine with Git, Go, Python, and whatever else you install, which stays put between sessions. Anything a session serves on a port opens at a private HTTPS address only you can reach. The runner connects out to Anthropic, so nothing on the VM is public. Claude still runs the conversation from claude.ai, so you can start a task on your phone and check on it from your laptop.
What you get#
- Claude Code 2.1.224 or later installed for a dedicated
claude-runneruser - The environment key in /etc/claude/environment-secret, readable only by
claude-runner - The claude-runner systemd service, enabled so it comes back after a reboot
Registered: runner_id=injournalctl -u claude-runner, with no[runner:fatal]line after it
What's running, and where
ssh my-claude-code-runner.exe.xyz
a shell on the machine your sessions run on
https://my-claude-code-runner.exe.xyz:8080/healthz
the runner's health check
https://my-claude-code-runner.exe.xyz:3000/
anything a session serves on a port from 3000 to 9999
claude self-hosted-runner, the claude-runner systemd service, as the claude-runner user
Deploy with Shelley#
Click Deploy and Shelley will set up Claude Code self-hosted runner on a new VM, then check that it works. No config-file wrangling required.
Have these ready; Shelley asks for them:
CLAUDE_ENVIRONMENT_SECRET: the environment key claude.ai showed once when you created the self-hosted environmentCLAUDE_RUNNER_ACCOUNT: the email of the claude.ai account whose sessions this runner serves, ornonefor a runner anyone in your organization can use
Prefer a terminal?
curl -fsSL \
https://exe-staging.dev/deploy/claude-code-runner/prompt.txt \
| ssh exe.dev new --prompt=/dev/stdinWhat Shelley is told
CLAUDE_ENVIRONMENT_SECRET=<fill-this-in>
CLAUDE_RUNNER_ACCOUNT=<fill-this-in>
Set up Claude Code self-hosted runner on this VM by following the exe.dev recipe at https://exe-staging.dev/deploy/claude-code-runner.md from top to bottom.
The values at the top of this message are mine:
- CLAUDE_ENVIRONMENT_SECRET: the environment key claude.ai showed once when you created the self-hosted environment
- CLAUDE_RUNNER_ACCOUNT: the email of the claude.ai account whose sessions this runner serves, or `none` for a runner anyone in your organization can use
Ask me for any that still say <fill-this-in> before you need them.
The recipe's example VM name is my-claude-code-runner. This VM's name is the output of `hostname`: use it wherever the recipe writes my-claude-code-runner. Skip the step that creates the VM; you are on it.
You are done when all of these are true. Check each one yourself:
- Claude Code 2.1.224 or later installed for a dedicated `claude-runner` user
- The environment key in /etc/claude/environment-secret, readable only by `claude-runner`
- The claude-runner systemd service, enabled so it comes back after a reboot
- `Registered: runner_id=` in `journalctl -u claude-runner`, with no `[runner:fatal]` line after it
Run "Set up the runner by hand" from "On the VM", skipping VM creation
and SSH. CLAUDE_ENVIRONMENT_SECRET is a secret: write it to
/etc/claude/environment-secret with
`printf '%s\n' "$value" | sudo -u claude-runner sh -c 'cat > /etc/claude/environment-secret'`
instead of the interactive paste, and never print it. If
CLAUDE_RUNNER_ACCOUNT is `none`, skip /etc/claude/runner.env. Don't start
a session: that happens on claude.ai. If the log says `RegisterRunner
auth failed`, tell the owner the environment key is wrong or revoked.
Finish by pointing the owner to "Start a session on your VM".
Finish with the URLs I should open.
Also at /deploy/claude-code-runner/prompt.txt. The recipe it follows is /deploy/claude-code-runner.md.
Before you start#
- A Claude Team or Enterprise plan. Self-hosted environments are in public beta there, and off until an Owner turns on Allow self-hosted environments on the Cloud environments admin page.
- GitHub connected: the GitHub connector turned on by an Owner, and your own GitHub account connected on claude.ai, as for any cloud session. The runner fetches and pushes your code through Anthropic, so the VM needs no GitHub credentials.
- The Claude GitHub App installed on the repositories Claude should push to, by someone who administers their GitHub organization. Without it, sessions can clone public repositories but can't push.
- An environment and its key. On the
Cloud environments
page, under Self-hosted environments, select New, name it (say
exe-dev), and select Create. On the next step, select Copy environment key. claude.ai shows the key only once.
Set up the runner#
Deploy sets it up on a new VM and asks for the environment key and the account it serves. To do it yourself:
Set up the runner by hand
On your computer (any free name works for the VM):
ssh exe.dev new --name=my-claude-code-runner
ssh my-claude-code-runner.exe.xyz
Install Claude Code for the runner#
On the VM, create a user for the runner. Anthropic's git integration replaces the global git config of the user it runs as, so it gets one of its own. Then install Claude Code for it with Anthropic's installer:
sudo useradd --create-home --shell /bin/bash claude-runner
sudo -iu claude-runner bash -c 'curl -fsSL https://claude.ai/install.sh | bash'
sudo -iu claude-runner claude self-hosted-runner --help | head -3
Success is Usage: claude self-hosted-runner [options]; the installer's
note about PATH doesn't matter here. The runner is part of the claude
binary from version 2.1.224 on; older versions print the general help
instead.
Give the runner uv and uvx too. Go is on its PATH in the service
below; the GitHub CLI deliberately isn't, so that sessions get Claude
Code's own gh, which reaches GitHub through Anthropic without a token:
sudo -u claude-runner ln -sf /usr/local/bin/uv /usr/local/bin/uvx /home/claude-runner/.local/bin/
Save the environment key#
Create /etc/claude and write the key to a file only the runner can
read. The last command reads from your terminal, so the key stays out of
your shell history: paste it, press Enter, then Ctrl-D.
sudo install -d -m 755 /etc/claude
sudo install -m 600 -o claude-runner -g claude-runner /dev/null /etc/claude/environment-secret
sudo -u claude-runner sh -c 'cat > /etc/claude/environment-secret'
To keep the runner for your own sessions, name your claude.ai account. Skip this for a runner your whole organization can use:
echo 'SELF_HOSTED_RUNNER_LOCK_TO_ACCOUNT=you@example.com' | sudo tee /etc/claude/runner.env
Tell sessions where they are#
The runner copies the claude-runner user's ~/.claude into every
session, so a CLAUDE.md there reaches them all. This one tells Claude
how to show you what it builds and to push its work:
sudo -u claude-runner mkdir -p /home/claude-runner/.claude
sudo -u claude-runner tee /home/claude-runner/.claude/CLAUDE.md > /dev/null <<EOF
This session runs on $(hostname), an exe.dev VM.
- To show the user a web app, listen on 0.0.0.0 on a port from 3000 to
9999 other than 8080, and give them https://$(hostname).exe.xyz:<port>/.
Only the VM's owner, and people they share it with, can open it.
- Commit and push your work before you finish a task. A session that sits
idle for an hour is released and resumes from a fresh checkout.
EOF
Run the runner#
Run it as a service. It checks repositories out under
/home/claude-runner/work, fetches and pushes GitHub repositories through
Anthropic (--use-anthropic-git-proxy), and signs commits as Claude
(--configure-git). After a session ends the runner exits by design, and
systemd starts it again for the next one:
sudo tee /etc/systemd/system/claude-runner.service > /dev/null <<'EOF'
[Unit]
Description=Claude Code self-hosted runner
Wants=network-online.target
After=network-online.target
[Service]
User=claude-runner
WorkingDirectory=/home/claude-runner
Environment=PATH=/home/claude-runner/.local/bin:/usr/local/go/bin:/usr/sbin:/usr/bin:/sbin:/bin
EnvironmentFile=-/etc/claude/runner.env
ExecStart=/home/claude-runner/.local/bin/claude self-hosted-runner \
--environment-secret-file /etc/claude/environment-secret \
--base-dir /home/claude-runner/work \
--use-anthropic-git-proxy --configure-git \
--release-idle-session-min 60 --kill-session-after-min 480 \
--remove-session-state
Restart=always
RestartSec=15
KillMode=mixed
TimeoutStopSec=120
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now claude-runner
sleep 10; journalctl -u claude-runner -n 20 --no-pager | grep -E 'Registered|fatal'
Expect Registered: runner_id=... and no [runner:fatal] line after it.
On the Cloud environments page, the environment changes from No runners
deployed to Healthy. RegisterRunner auth failed means the key is
wrong or was revoked: write it again and run
sudo systemctl restart claude-runner.
Start a session on your VM#
Open claude.ai/code, pick a GitHub repository,
and choose your environment (exe-dev) in the environment picker, where
it sits beside Anthropic's. The desktop app lists it under Cloud, and
the Code tab of the mobile app has the same picker. Try:
Run hostname, then add a hello.py that prints the date.
Claude answers with the VM's name. The runner's log shows the pickup:
ssh my-claude-code-runner.exe.xyz journalctl -u claude-runner -n 30 --no-pager | grep 'Picked up session'
From a terminal, run /remote-env in Claude Code once and pick the
environment. After that, claude --cloud "<task>" from a checkout of a
GitHub repository sends the task to your VM.
One runner serves one session at a time. A session keeps the runner until
you archive it, or until it has been idle for an hour, when the runner
releases it. Your next message resumes it on a fresh checkout, so work
Claude hadn't pushed is gone; the CLAUDE.md above asks Claude to push.
A session that runs eight hours is released the same way. Other sessions
wait in the environment's queue meanwhile. For more at once, see
More runners.
See what Claude builds#
When a session starts a web app on a port from 3000 to 9999, open it at
https://my-claude-code-runner.exe.xyz:<port>/. exe.dev's
HTTPS proxy keeps those addresses private to you and anyone
you share the VM with. Port 8080 is the runner's own health
check.
To look around the machine, SSH in. Checkouts are under
/home/claude-runner/work/<owner>/<repo>:
ssh my-claude-code-runner.exe.xyz 'sudo ls /home/claude-runner/work'
Give sessions your tools#
Sessions run as claude-runner, without sudo, and inherit the service's
environment. To prepare the machine:
- System packages. As
exedev,sudo apt-get installwhat your builds need. Sessions use it from then on. - Docker. Sessions can't use Docker until you add the runner to the
dockergroup. That is as good as root on the VM, so do it only if you need it:sudo usermod -aG docker claude-runner && sudo systemctl enable --now docker.service && sudo systemctl restart claude-runner. - MCP servers and settings. Run
claude mcp add --scope user ...asclaude-runner(sudo -iu claude-runner), or edit its~/.claude/settings.json. The runner reads them when it starts, so restart it afterwards. - Internal services. Sessions reach whatever the VM reaches, including services you connect through exe.dev integrations, with no keys on the VM.
Warning
Sessions run model-directed commands without asking first, and every session on this VM can read the environment key in
/etc/claude/environment-secret, with which anyone could register a runner in your environment. Keep this VM for the runner, put nothing on it you wouldn't hand to Claude, and rotate the key on claude.ai if a session goes wrong. Anthropic's hardening checklist covers production fleets.
More runners#
For more sessions at once, or a runner for each person, copy the VM. On your computer:
ssh exe.dev cp my-claude-code-runner my-claude-code-runner-2
The copy registers as a runner of its own, named after the VM, with the
same key, tools, and account lock. For a teammate, set their email in
/etc/claude/runner.env on the copy and run
sudo systemctl restart claude-runner. Copies share your plan's CPU and
memory. Remove one with ssh exe.dev rm my-claude-code-runner-2.
Update#
The runner, and every session it starts, runs the Claude Code installed
for claude-runner. A new model can require a newer version. To update,
on the VM, while no session is running:
sudo -iu claude-runner claude update
sudo systemctl restart claude-runner
Restarting stops a running session's turn. The session resumes on your next message.
Back up#
The runner keeps little of its own: your code lives in GitHub, and
conversations on claude.ai. What matters is the environment key, which
claude.ai won't show again, so keep it in your password manager.
Without it, create a new key on the environment's Configuration tab,
write it to /etc/claude/environment-secret, restart the runner, and
revoke the old one. Keys expire 365 days after they are created.
To keep the whole machine, with its tools and settings, copy it as in
More runners, and stop the copy's runner with
sudo systemctl disable --now claude-runner until you need it.
Troubleshooting#
The runner's log is journalctl -u claude-runner on the VM:
RegisterRunner auth failed: the environment key is wrong, revoked, or expired. Fix/etc/claude/environment-secretand restart.cannot create or write to base directory:/home/claude-runner/workisn't the runner's. Runsudo chown -R claude-runner: /home/claude-runner.- The session stays queued: the runner is serving another session, or is
locked to another account. Check for
Picked up sessionin the log and the email in/etc/claude/runner.env; archive sessions you're done with, or add a runner. GitHub authentication required: the person who started the session hasn't connected GitHub on claude.ai, or the connection expired.- A push fails with
Claude doesn't have GitHub access to <owner>/<repo>: the Claude GitHub App isn't installed on that repository. A GitHub organization admin installs it at github.com/apps/claude; then ask Claude to push again in the same session. the server withheld Anthropic-managed git for this session: one of the session's repositories isn't on github.com. See the FAQ.This model needs a newer version of Claude Code: update.
For a guided diagnosis, sign the runner user in with
sudo -iu claude-runner claude auth login, then run
sudo -iu claude-runner claude self-hosted-runner doctor. Anthropic's
troubleshooting guide
covers the rest.
Frequently asked questions#
Which Claude plan do I need?
Self-hosted environments are in public beta on Claude Team and Enterprise plans, and an Owner has to turn them on. On Pro or Max, run Claude Code on a VM as the Claude Code page shows, and steer it from your phone with Anthropic's Remote Control.
How is this different from running Claude Code on a VM?
On the Claude Code page, you SSH in and run claude in a terminal. Here you never log in: you start cloud sessions from claude.ai, the Claude apps, or claude --cloud, and the runner on the VM picks them up. The VM replaces the machine Anthropic would otherwise run them on.
What runs where, and what leaves the VM?
The VM clones your repositories, runs every command, and keeps the files. The conversation, Claude's model calls, and the session transcript go to api.anthropic.com, as with any cloud session. The runner only connects out, so the VM needs no public port.
What does it cost?
Sessions count against your organization's Claude Code usage, the same as sessions on Anthropic's machines. The VM is part of your exe.dev plan, which on Personal includes up to 50 VMs sharing one pool of CPU and memory.
Can my whole team share one runner?
Yes, with CLAUDE_RUNNER_ACCOUNT set to none, but one person at a time: a runner locks to the owner of the first session it picks up and serves only them until that session ends. Everyone's sessions then share one disk, one after another. A VM per person, each locked to its owner's account, keeps people's code apart.
Can each session get a fresh VM?
Not with this setup. Anthropic's on-demand runners run an orchestrator that calls a spawn-runner hook for each queued session, and a hook that runs ssh exe.dev new could give each session a VM. The Claude Managed Agents page shows that pattern for Anthropic's agent API.
Can sessions use exe.dev's LLM gateway?
Claude's own model calls go to Anthropic with a token issued for each session. Code that Claude writes and tests on the VM can call https://llm.int.exe.xyz without an API key, through exe.dev's LLM integration.
My code isn't on github.com. Will this work?
Not as written: Anthropic-managed git, which keeps GitHub credentials off the VM, serves only github.com. Remove --use-anthropic-git-proxy and give the claude-runner user git credentials of its own, as Anthropic's Configure git describes.
Updated Oct 30, 2026 · tested end to end Oct 30, 2026 · Markdown · manifest.json · Something wrong? Tell us