Skip to content

Run a Claude Code self-hosted runner

Claude Code cloud sessions, running on a VM you control.

Deploy on exe.dev

Shelley handles the setup. About 10 minutes.

Prefer a terminal?
curl -fsSL https://exe.dev/deploy/claude-code-runner/prompt.txt | ssh exe.dev new --prompt=/dev/stdin

Claude 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-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
What's running, and where
Private ssh my-claude-code-runner.exe.xyz a shell on the machine your sessions run on
Private https://my-claude-code-runner.exe.xyz:8080/healthz the runner's health check
Private https://my-claude-code-runner.exe.xyz:3000/ anything a session serves on a port from 3000 to 9999
my-claude-code-runner.exe.xyz exe.dev VM
Claude Code runner claude self-hosted-runner, the claude-runner systemd service, as the claude-runner user :8080/home/claude-runner/work
Runner settings the environment key, and the account the runner serves /etc/claude

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 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
Prefer a terminal?
curl -fsSL \
  https://exe.dev/deploy/claude-code-runner/prompt.txt \
  | ssh exe.dev new --prompt=/dev/stdin
What 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.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 install what your builds need. Sessions use it from then on.
  • Docker. Sessions can't use Docker until you add the runner to the docker group. 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 ... as claude-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-secret and restart.
  • cannot create or write to base directory: /home/claude-runner/work isn't the runner's. Run sudo 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 session in 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