---
title: "Run a Claude Code self-hosted runner"
description: "Turn an exe.dev VM into a Claude Code self-hosted runner: sessions you start from claude.ai, the Claude apps, or claude --cloud run on your VM."
url: https://exe-staging.dev/deploy/claude-code-runner
category: "AI agents"
updated: 2026-10-30
---

# Run a Claude Code self-hosted runner

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

- **Software:** Claude Code self-hosted runner (https://claude.com/product/claude-code)
- **License:** Proprietary (Anthropic Commercial Terms)
- **Time:** 10 minutes
- **Example VM name:** `my-claude-code-runner` (yours will differ; on the VM, `hostname` prints it)
- **Machine-readable:** https://exe-staging.dev/deploy/claude-code-runner/manifest.json

## 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

Running on the VM:

- **Claude Code runner**: `claude self-hosted-runner`, the claude-runner systemd service, as the claude-runner user, port 8080, data in `/home/claude-runner/work`
- **Runner settings**: the environment key, and the account the runner serves, data in `/etc/claude`

How it is reached:

- ssh my-claude-code-runner.exe.xyz (private): a shell on the machine your sessions run on
- https://my-claude-code-runner.exe.xyz:8080/healthz (private): the runner's health check
- https://my-claude-code-runner.exe.xyz:3000/ (private): anything a session serves on a port from 3000 to 9999

## Deploy with Shelley

Open https://exe-staging.dev/new?prompt=CLAUDE_ENVIRONMENT_SECRET%3D%3Cfill-this-in%3E%0ACLAUDE_RUNNER_ACCOUNT%3D%3Cfill-this-in%3E%0A%0ASet%20up%20Claude%20Code%20self-hosted%20runner%20on%20this%20VM%20by%20following%20the%20exe.dev%20recipe%20at%20https%3A%2F%2Fexe-staging.dev%2Fdeploy%2Fclaude-code-runner.md%20from%20top%20to%20bottom.%0A%0AThe%20values%20at%20the%20top%20of%20this%20message%20are%20mine%3A%0A-%20CLAUDE_ENVIRONMENT_SECRET%3A%20the%20environment%20key%20claude.ai%20showed%20once%20when%20you%20created%20the%20self-hosted%20environment%0A-%20CLAUDE_RUNNER_ACCOUNT%3A%20the%20email%20of%20the%20claude.ai%20account%20whose%20sessions%20this%20runner%20serves%2C%20or%20%60none%60%20for%20a%20runner%20anyone%20in%20your%20organization%20can%20use%0AAsk%20me%20for%20any%20that%20still%20say%20%3Cfill-this-in%3E%20before%20you%20need%20them.%0A%0AThe%20recipe%27s%20example%20VM%20name%20is%20my-claude-code-runner.%20This%20VM%27s%20name%20is%20the%20output%20of%20%60hostname%60%3A%20use%20it%20wherever%20the%20recipe%20writes%20my-claude-code-runner.%20Skip%20the%20step%20that%20creates%20the%20VM%3B%20you%20are%20on%20it.%0A%0AYou%20are%20done%20when%20all%20of%20these%20are%20true.%20Check%20each%20one%20yourself%3A%0A-%20Claude%20Code%202.1.224%20or%20later%20installed%20for%20a%20dedicated%20%60claude-runner%60%20user%0A-%20The%20environment%20key%20in%20%2Fetc%2Fclaude%2Fenvironment-secret%2C%20readable%20only%20by%20%60claude-runner%60%0A-%20The%20claude-runner%20systemd%20service%2C%20enabled%20so%20it%20comes%20back%20after%20a%20reboot%0A-%20%60Registered%3A%20runner_id%3D%60%20in%20%60journalctl%20-u%20claude-runner%60%2C%20with%20no%20%60%5Brunner%3Afatal%5D%60%20line%20after%20it%0A%0ARun%20%22Set%20up%20the%20runner%20by%20hand%22%20from%20%22On%20the%20VM%22%2C%20skipping%20VM%20creation%0Aand%20SSH.%20CLAUDE_ENVIRONMENT_SECRET%20is%20a%20secret%3A%20write%20it%20to%0A%2Fetc%2Fclaude%2Fenvironment-secret%20with%0A%60printf%20%27%25s%5Cn%27%20%22%24value%22%20%7C%20sudo%20-u%20claude-runner%20sh%20-c%20%27cat%20%3E%20%2Fetc%2Fclaude%2Fenvironment-secret%27%60%0Ainstead%20of%20the%20interactive%20paste%2C%20and%20never%20print%20it.%20If%0ACLAUDE_RUNNER_ACCOUNT%20is%20%60none%60%2C%20skip%20%2Fetc%2Fclaude%2Frunner.env.%20Don%27t%20start%0Aa%20session%3A%20that%20happens%20on%20claude.ai.%20If%20the%20log%20says%20%60RegisterRunner%0Aauth%20failed%60%2C%20tell%20the%20owner%20the%20environment%20key%20is%20wrong%20or%20revoked.%0AFinish%20by%20pointing%20the%20owner%20to%20%22Start%20a%20session%20on%20your%20VM%22.%0A%0AFinish%20with%20the%20URLs%20I%20should%20open.%0A to create a VM whose agent, Shelley, follows this recipe. From a terminal:

```sh
curl -fsSL https://exe-staging.dev/deploy/claude-code-runner/prompt.txt | ssh exe.dev new --prompt=/dev/stdin
```

Or follow the steps below by hand.

Claude Code's [cloud sessions](https://code.claude.com/docs/en/claude-code-on-the-web)
are the ones you start from [claude.ai/code](https://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](https://code.claude.com/docs/en/self-hosted-environments)
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.

## 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](https://claude.ai/admin-settings/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](https://github.com/apps/claude) 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](https://claude.ai/admin-settings/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:

<details id="create-the-vm" data-manual-setup class="dp-details">
<summary>Set up the runner by hand</summary>

On your computer (any free name works for the VM):

```bash
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:

```bash
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:

```bash
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**.

```bash
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:

```bash
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:

```bash
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:

```bash
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`.

</details>

## Start a session on your VM

Open [claude.ai/code](https://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:

```bash
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](#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](/docs/proxy) keeps those addresses private to you and anyone
you [share](/docs/sharing) 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>`:

```bash
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](/docs/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](https://code.claude.com/docs/en/self-hosted-environments-deploy#harden-your-deployment)
> covers production fleets.

## More runners

For more sessions at once, or a runner for each person, copy the VM. On
your computer:

```bash
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:

```bash
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](#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](https://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](#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](https://code.claude.com/docs/en/self-hosted-environments-deploy#troubleshooting)
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](/deploy/claude-code) shows, and steer it from your phone with Anthropic's [Remote Control](https://code.claude.com/docs/en/remote-control).

### How is this different from running Claude Code on a VM?

On [the Claude Code page](/deploy/claude-code), 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](/docs/billing/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](https://code.claude.com/docs/en/self-hosted-environments-configuration#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](/deploy/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](/docs/integrations-llm).

### 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](https://code.claude.com/docs/en/self-hosted-environments-deploy#configure-git) describes.


## Related

- [Run Claude Code on a remote server](https://exe-staging.dev/deploy/claude-code.md): A computer for Claude. A breather for your laptop.
- [Self-hosted sandbox for Claude Managed Agents](https://exe-staging.dev/deploy/claude-managed-agents.md): Anthropic runs the agent. Each session's tools run on a VM of its own.
- [Running a self-hosted GitHub Actions Runner](https://exe-staging.dev/deploy/github-actions-runner.md): Log in easily into your CI environment
- [Host OpenClaw](https://exe-staging.dev/deploy/openclaw.md): An AI sidekick with its own computer. Your laptop can take a nap.
- [Run Codex on a remote server](https://exe-staging.dev/deploy/codex.md): A computer for Codex. An early night for your laptop.
- [Run Codex remote control on a server](https://exe-staging.dev/deploy/codex-remote-control.md): Codex on an always-on VM, steered from the ChatGPT app on your phone.
