Install Claude Code as a systemd user service on a Linux VPS. Survives SSH logout, terminal close, and reboot. Attach from anywhere via https://claude.ai/code or the Claude app.
The kit's promised UX — unattended
claude remote-controlreachable viaclaude.ai/code— does not work on a fresh deployment with current CLI versions. After the flag-form fix described inCHANGELOG.md, the workspace-trust gate still blocks the systemd unit from completing boot, and thegit init $HOMEworkaround documented inDECISIONS.mdis no longer sufficient. A Fasthosts deployment on 2026-04-24 reproduced this; the tmux fallback below is the working alternative until upstream ships an unattended-trust path.If you want a persistent
claudeyou can SSH into (terminal, not web), see Tmux fallback —./install-tmux.shpackages it. If you want theclaude.ai/codeweb UX specifically, the answer today is "wait for an Anthropic fix" — track anthropics/claude-code#53606 and re-test when an unattended-trust path lands. Full reproduction inINCIDENTS/2026-04-24-workspace-trust.md.
Tested against Claude Code 2.1.119+ (subcommand-style CLI). Handles every gotcha we hit deploying this in production — see the comments in install.sh for the full reasoning.
Phase: Build → Validate, paused at the workspace-trust blocker. The install path is implemented and tested manually on Ubuntu 22.04 + 24.04 VPSes; the kit installs cleanly but the running service can't pass workspace trust unattended on CLI 2.1.119. Validation gates (automated test suite, golden-set bootstrap test) deferred until the upstream blocker resolves; tracked in
STATE.md.
Read these before installing on a real box:
THREAT_MODEL.md— trust boundary, assumptions, in-scope risks, known gaps.SECURITY.md— vulnerability disclosure process.DECISIONS.md— why the kit makes the choices it does.INCIDENTS/— post-mortems and upstream issues (Apr 2026 workspace-trust blocker).
The kit ships two install paths for the same underlying problem ("persistent claude on a remote VPS"). They differ in how you reach the agent — and on CLI 2.1.119+ only the second one currently works end-to-end.
flowchart LR
User([You])
subgraph laptop["Your laptop / phone"]
Web["claude.ai/code<br/>(web app)"]
SSH["SSH terminal"]
end
subgraph vps["Linux VPS (non-root user, linger enabled)"]
subgraph systemd["systemd --user"]
UnitA["claude-agent.service<br/>install.sh"]
UnitB["claude-agent-tmux.service<br/>install-tmux.sh"]
end
Trust{{"workspace<br/>trust gate"}}
Claude1["claude remote-control<br/>--name VPS"]
Tmux["tmux session 'claude'"]
Claude2["claude<br/>(interactive)"]
UnitA -->|launches| Trust
Trust -->|"git init $HOME"<br/>was sufficient<br/>pre-2.1.119| Claude1
UnitB -->|launches| Tmux
Tmux --> Claude2
end
Anthropic[("api.anthropic.com")]
Claude1 -.-> Anthropic
Claude2 -.-> Anthropic
User --> Web
User --> SSH
Web -->|"device list"| Claude1
SSH -->|"tmux attach -t claude"| Tmux
classDef broken stroke:#c00,stroke-width:2px,stroke-dasharray:5 5
classDef ok stroke:#080,stroke-width:2px
class Trust,Claude1 broken
class Tmux,Claude2 ok
Red dashed = currently broken on CLI 2.1.119+. The git init $HOME workaround is no longer reliably sufficient for the workspace-trust gate under the systemd unit — see anthropics/claude-code#53606. Until upstream lands an unattended-trust path, the supervised path can't reach claude.ai/code.
Green = currently working on CLI 2.1.119+. The tmux fallback runs claude interactively inside a tmux session that survives SSH logout and reboot. You attach over SSH, not via the web app.
- A
claude-agent.servicesystemd unit under~/.config/systemd/user/. loginctllinger so the service starts at boot without a login session.- Workspace trust (via
git init $HOME) so the service doesn't trip the trust dialog. - Auto-restart on failure.
- A non-root user. Claude Code ≥ 2.1.119 refuses bypass-permissions mode as root. Create one on a fresh VPS:
adduser --disabled-password --gecos "" claude # Optional: give sudo (not required for the agent itself) usermod -aG sudo claude
- Claude Code CLI installed on PATH. See https://docs.claude.com/en/docs/claude-code/setup.
- A Claude account with Remote Control available on your plan. The installer will prompt for login and one-time consent.
From a shell logged in as the non-root user:
cd ~/claude-agent-kit
./install.shOptional: override the session name (defaults to the box's short hostname):
CLAUDE_SESSION_NAME=my-vps ./install.shThe installer walks you through two interactive prompts:
claude auth login— opens a web flow (or device code) to authenticate this user with your Claude account. Skipped if already logged in.claude remote-controlconsent — first time on an account, Claude asks "Enable Remote Control? (y/n)" and "spawn mode [1/2]". Answerythen1, then Ctrl-C to exit. Consent persists — subsequent runs (including the systemd launch) skip these prompts.
After those, the installer writes the unit, drops a runtime safety
CLAUDE.md at $HOME (skipped if you already have one — see Safety
below), and brings the service up.
The installer ships a CLAUDE.md.template and writes it to $HOME/CLAUDE.md
on first run. The agent reads $HOME/CLAUDE.md on every session start;
the template adds hard rules denying destructive operations on:
~/.ssh/**— deletingauthorized_keyslocks SSH out./usr/local/bin/claude*,/usr/bin/claude*,~/.local/bin/claude*— the CLI binary.~/.claude/auth.json,~/.claude/settings.json,~/.claude/keychain*— auth and settings the agent needs to start./etc/systemd/system/claude-agent.service,~/.config/systemd/user/claude-agent.service— the unit.
This is a runtime guard against a common failure mode: a "tidy up" or
"free disk" request that has the agent delete paths it depends on for
its own survival. The denylist is advisory to the agent (read every
session, enforced by the agent's behaviour). Defence-in-depth via
~/.claude/settings.json's permissions.deny list is the recommended
next layer.
If you already have a $HOME/CLAUDE.md, the installer leaves it alone
and reminds you to merge in the rules from CLAUDE.md.template.
The simplest approaches:
# Option A — scp directly between your boxes (if SSH is configured both ways)
scp -r user@source-host:~/claude-agent-kit/ ~/
# Option B — via your laptop
scp -r user@source-host:~/claude-agent-kit/ /tmp/
scp -r /tmp/claude-agent-kit/ user@new-vps:~/
# Option C — paste the three files (README, template, install.sh) as heredocs
# directly on the new box. See the end of this file.From the root shell of a new VPS:
# 1. Create the non-root user
adduser --disabled-password --gecos "" claude
loginctl enable-linger claude
# 2. Install Claude Code (per your preferred method — example:)
# curl -fsSL https://claude.ai/install.sh | sh # or whatever
# Make the binary available on PATH for the 'claude' user.
# 3. Drop the kit into the new user's home
mkdir -p /home/claude/claude-agent-kit
cp -r /path/to/kit/* /home/claude/claude-agent-kit/
chown -R claude:claude /home/claude/claude-agent-kit
# 4. Switch into that user's shell (machinectl preserves the user systemd bus)
machinectl shell claude@
# ...or: sudo -u claude -i
# 5. Run the installer
cd ~/claude-agent-kit && ./install.shsystemctl --user status claude-agent
journalctl --user -u claude-agent -fFrom another box (or your phone): https://claude.ai/code → you should see the environment listed.
- SSH in and out freely — the service stays up.
systemctl --user restart claude-agentif it ever gets wedged.- Update Claude Code on the host (
curl … | shor package manager), thensystemctl --user restart claude-agent.
Rerunning ./install.sh overwrites the unit, reloads, and restarts cleanly. Prerequisite checks short-circuit if already done.
systemctl --user disable --now claude-agent
rm ~/.config/systemd/user/claude-agent.service
# Optional (only if you don't use other user services):
sudo loginctl disable-linger "$USER"-
Service is in a restart loop (
Restart counter is at NN, NN climbing) — almost always means the unit'sExecStartis calling the CLI with a removed flag. The classic symptom from the 2026-04-24 incident is:Error: Input must be provided either through stdin or as a prompt argument when using --print claude-agent.service: Failed with result 'exit-code' claude-agent.service: Scheduled restart job, restart counter is at 53.Verify your unit's
ExecStartmatches the v2.1.119+ subcommand form:ExecStart=/usr/local/bin/claude remote-control --name <NAME> --permission-mode bypassPermissionsThe pre-v2.1.119 form
claude --remote-control <name> --persistwas removed (sessions persist by default;--persistno longer exists). Re-run./install.shto refresh the unit if you're on an older clone of the kit. Confirm the running CLI version withclaude --version— must be 2.1.119 or newer. -
Address already in use/ unit fails to start, but no port is involved — a manually-launchedclaude remote-control --name <NAME>may already hold the session name. Find it withpgrep -af "remote-control.*<NAME>"and either kill it or stop the conflicting unit, thensystemctl --user start claude-agent. -
"Failed to connect to bus: No medium found" when running
systemctl --useraftersu -— the session doesn't have a dbus. Fix:export XDG_RUNTIME_DIR=/run/user/$(id -u)
Or always switch users with
machinectl shell user@instead ofsu -. -
"Workspace not trusted" in the journal — the installer runs
git initin$HOME, which historically auto-trusted the directory. As of CLI 2.1.119, this is no longer reliably sufficient on first run under the systemd unit (see the Known Issue at the top of this README). Verify$HOME/.gitexists and that Claude's runtime user matches the unit'sWorkingDirectory. If the dialog still blocks, you've hit the upstream blocker — the Tmux fallback below is the working alternative for now. -
"cannot be used with root/sudo privileges" — you're running as root. Create a non-root user (see Prerequisites).
-
"Enable Remote Control? (y/n)" in the journal — the interactive consent wasn't completed. Rerun
./install.shand answer the prompt when it comes up, or runclaude remote-control --name <NAME> --permission-mode bypassPermissionsmanually and answery. -
"Remote Control eligibility" — your Claude plan may not include Remote Control. Check plan, or fall back to running plain
claudeinside tmux/screen as an alternative persistence pattern (see Tmux fallback).
When the workspace-trust blocker (or any other claude.ai/code-specific failure) makes the supervised remote-control path unusable, you can still get a persistent claude session reachable over SSH by running it inside tmux as a long-lived user service. This is not the same product — you reach it via ssh box -t tmux attach, not via claude.ai/code — but it survives SSH disconnect and reboot, which is the underlying ask in most cases.
| Feature | tmux + claude (this fallback) | claude-agent-kit (the supervised path) |
|---|---|---|
| How you reach it | ssh box -t tmux attach (terminal) |
claude.ai/code web app or mobile |
| Multi-device | One terminal at a time | Yes (browser on any device) |
| Survives box reboot (with the systemd unit) | Yes | Yes (when the trust gate clears) |
| Available today on CLI 2.1.119 | Yes | No — workspace-trust gate blocks it |
Appears in the claude.ai/code device list |
No | Yes |
The kit ships an installer for this fallback path:
cd ~/claude-agent-kit
./install-tmux.shWhat it does:
- Refuses to run as root (same constraint as the supervised path).
- Installs
tmuxviaaptif not already present. - Enables
loginctllinger so the unit starts at boot. - Walks you through
claude auth loginand one interactiveclaudelaunch (so any first-run trust prompt is cleared while a human is at the keyboard). - Drops a
claude-agent-tmux.serviceuser unit at~/.config/systemd/user/and starts it.
Override the tmux session name with CLAUDE_SESSION_NAME=foo ./install-tmux.sh (defaults to claude).
After it succeeds:
# Locally on the box:
tmux attach -t claude
# From another box / your laptop:
ssh -t user@box tmux attach -t claude
# Detach: Ctrl-b then d. Session keeps running.sudo apt-get install -y tmux # if not already present
loginctl enable-linger "$USER" # survive logout
tmux new -s claude -d 'claude --permission-mode bypassPermissions'
ssh -t user@box tmux attach -t claude # attach from another boxThis gives you the same persistent session but without the systemd unit, so it won't auto-recover after tmux kill-session or a reboot.
Archive this kit if any of these become true:
- Anthropic ships a first-party "remote-control as a service" deployment story that covers the same gotchas (auth, workspace trust, linger, unattended boot).
- Claude Code drops the
remote-controlsubcommand or replaces the systemd-friendly invocation pattern in a way that can't be patched with a one-line install.sh change. - The threat model becomes untenable for the configurations users actually run (e.g. multi-tenant becomes the default, requiring a rewrite rather than a kit update).
If scp isn't available, paste these three blocks on the target box as the non-root user:
mkdir -p ~/claude-agent-kit && cd ~/claude-agent-kitThen paste the contents of claude-agent.service.template and install.sh from this kit into files of those names (cat > filename <<'EOF' … EOF), chmod +x install.sh, and ./install.sh.