Always-on Claude Code sessions with Remote Control and systemd

Run Claude Code on a Linux box as systemd services, and drive it from claude.ai or your phone — even after a reboot.

Claude Code’s Remote Control lets you drive a session running on your own machine from claude.ai/code or the mobile app. The code, the tools and the credentials stay on your machine; the browser is just a window onto it.

The catch: a Remote Control session only lives as long as the claude process behind it. Close the terminal, lose the SSH connection or reboot, and it’s gone. This post is how I turned mine into always-on systemd services that survive all of that. It’s verified on Ubuntu with tmux 3.4 and Claude Code 2.1.28x (September 2026).

There are two kinds of service:

  1. A generic claude-rc@<project>.service: a Remote Control server for a project directory. It spawns new sessions on demand, whenever you start one from claude.ai/code or the app.
  2. A dedicated claude-<name>.service: keeps one existing conversation always available, reusing its Remote Control URL when it already had one.

Plus a logrotate rule, because these services write debug logs that grow forever otherwise.

Before you start

Unit files can’t use $HOME or ~, so every path in them must be absolute. Collect these values on the target machine, as the user who will own the services (not root):

PlaceholderMeaningHow to get it
<user>account that runs claude and owns ~/.claudeid -un
<group>its primary group (for logrotate)id -gn
<home>its home directorygetent passwd "$(id -un)" | cut -d: -f6
<claude-bin-dir>directory holding the claude binarydirname "$(command -v claude)"
<projects-root>parent directory of your project foldersyour choice
<host>short machine name, used in session nameshostname -s

You also need tmux, systemctl and logrotate installed, sudo, and to be logged in to Claude Code (~/.claude/.credentials.json exists).

If a machine already has some of this set up, look before you change anything:

systemctl list-unit-files 'claude*' --no-pager      # installed units
systemctl list-units 'claude*' --all --no-pager     # and their state
cat /etc/logrotate.d/claude-cache 2>/dev/null
ls ~/.cache/claude-rc/ 2>/dev/null

The common design

Both kinds of service share the same shape:

  • System units in /etc/systemd/system/, running as User=<user>. systemd sets HOME, so claude finds its credentials. No user lingering needed.
  • claude runs inside a detached tmux server with its own socket name (tmux -L <socket>). That way you can attach to it from any shell with tmux -L <socket> attach and see exactly what the session shows (detach with Ctrl-b d). Hence Type=forking, ExecStop=… kill-server and Restart=always.
  • Never set PrivateTmp=true. The tmux socket lives in /tmp/tmux-<uid>/, and a private /tmp makes it impossible to attach from a normal shell.
  • Every claude gets a --debug-file, because tmux output never reaches the journal. Everything else the service writes (logs of spawned sessions, bridge transcripts, a latest symlink) lands in the same directory as that file, so I point them all at <home>/.cache/claude-rc/: one directory, one logrotate rule.
  • claude doesn’t create that directory, so each unit has an ExecStartPre that does.
  • An explicit PATH, so claude resolves.

1. The generic service

/etc/systemd/system/claude-rc@.service:

[Unit]
Description=Claude Code Remote Control (%i)
After=network-online.target
Wants=network-online.target

[Service]
Type=forking
User=<user>
WorkingDirectory=<projects-root>/%i
Environment=PATH=<claude-bin-dir>:/usr/local/bin:/usr/bin:/bin
ExecStartPre=/usr/bin/mkdir -p <home>/.cache/claude-rc
ExecStart=/usr/bin/tmux -L rc-%i new-session -d -s rc 'claude remote-control --name %i@<host> --spawn worktree --capacity 6 --permission-mode auto --debug-file <home>/.cache/claude-rc/rc-%i.log'
ExecStop=/usr/bin/tmux -L rc-%i kill-server
Restart=always
RestartSec=10
NoNewPrivileges=true

[Install]
WantedBy=multi-user.target

It’s a template: enable one instance per project, named after its folder under <projects-root>:

sudo systemd-analyze verify /etc/systemd/system/claude-rc@.service
sudo systemctl daemon-reload
sudo systemctl enable --now claude-rc@my-project.service

A few things I tripped on:

  • Use %i, not %I, and don’t run the instance name through systemd-escape. Escaping turns - into \x2d, and %I turns - into /. A plain claude-rc@my-project with %i gives the right path.
  • --spawn worktree gives each new session its own git worktree, so parallel sessions don’t step on each other. It requires the directory to be a git repo.
  • --capacity 6 caps how many sessions the server runs at once.

The other spawn modes are same-dir (the default: every session works in the directory itself) and session (a single session; the server exits when it ends).

For a directory that isn’t a git repo — say <projects-root> itself, to start sessions that work across several projects — worktree is impossible. I use a standalone claude-rc-<name>.service for that: the same unit with a fixed WorkingDirectory, its own socket, and --spawn same-dir. Just remember that concurrent sessions then share the directory and can collide.

To check it works: systemctl status claude-rc@my-project should show active (running) with a claude remote-control child, and the debug log should contain Created initial session session_01… and work/poll -> 200:

systemctl status claude-rc@my-project
tail ~/.cache/claude-rc/rc-my-project.log

The project then shows up in claude.ai/code and the app, ready for new sessions.

2. A dedicated service for an existing conversation

Sometimes you don’t want a new session, but to keep one particular conversation reachable — one with a lot of context you don’t want to lose.

What I learned the hard way

  • The Remote Control URL (https://claude.ai/code/session_01…) is assigned by Anthropic’s server. It isn’t derived from the local session id.
  • claude remote-control --session-id session_01… looks like the obvious tool, but it only reattaches sessions created by a claude remote-control server. For a normal session it fails with Session … has no environment_id. It may never have been attached to a bridge.
  • What works is resuming the conversation with Remote Control on: claude --resume <local-uuid> --remote-control "<Display Name>". If the session already had a Remote Control URL, it reconnects to the same URL, history intact. If it never had one, a new URL is created, and it then stays the same across restarts.
  • Exception: a session that had already stopped got a new URL, even though its old one was still on record. There’s no way to force the old one back, so expect the link to change once.
  • Only one process may run a given conversation. Stop any other instance first.

Find the conversation

Conversations live under ~/.claude/projects/<project-slug>/<uuid>.jsonl, and their titles in a sibling <uuid>/custom-title.json. Titles may contain typos, so search loosely on one distinctive word:

grep -rli "<word>" ~/.claude/projects --include=custom-title.json \
  | xargs -I{} sh -c 'echo "{}: $(cat {})"'

The directory name is the local session UUID. Running sessions each have a ~/.claude/sessions/<pid>.json with the sessionId, the working directory, and bridgeSessionId: the current Remote Control URL (cse_X ↔ session_X), empty if it has never been on Remote Control. Note the model and effort it used too, so the service can pin them.

Stop it, and back it up

Copy the transcript somewhere safe first. Then stop whatever runs it: exit it if it’s in a terminal. If it’s a background session, claude daemon stop --any stops it — but it stops every background session the daemon runs, so check what else is listed in ~/.claude/daemon/roster.json first.

Try it by hand

Before writing a unit, run the exact command in tmux and check it comes up:

mkdir -p ~/.cache/claude-rc
cd <cwd> && tmux -L rc-<name> new-session -d -s rc -x 160 -y 47 \
  'claude --resume <uuid> --remote-control "<Display Name>" --permission-mode auto --model <model> --debug-file <home>/.cache/claude-rc/<name>.log'

# Wait for Remote Control, then look at the pane.
timeout 90 bash -c 'until tmux -L rc-<name> capture-pane -p -t rc | grep -q "remote-control is active"; do sleep 2; done'
tmux -L rc-<name> capture-pane -p -t rc | grep -E 'remote-control is active|auto mode'

Check the URL (the same as before, if it had one), auto mode on, and the model. Pin the full model id (e.g. claude-opus-5-5) rather than an alias, and pass --effort explicitly if it matters. Then kill the test — tmux -L rc-<name> kill-server — since the service uses the same socket and conversation.

The unit

/etc/systemd/system/claude-<name>.service:

[Unit]
Description=Claude Code session "<Display Name>" (Remote Control)
After=network-online.target
Wants=network-online.target

[Service]
Type=forking
User=<user>
WorkingDirectory=<cwd>
Environment=PATH=<claude-bin-dir>:/usr/local/bin:/usr/bin:/bin
ExecStartPre=/usr/bin/mkdir -p <home>/.cache/claude-rc
ExecStart=/usr/bin/tmux -L rc-<name> new-session -d -s rc -x 160 -y 47 'claude --resume <uuid> --remote-control "<Display Name>" --permission-mode auto --model <model> --debug-file <home>/.cache/claude-rc/<name>.log'
ExecStop=/usr/bin/tmux -L rc-<name> kill-server
Restart=always
RestartSec=10
NoNewPrivileges=true

[Install]
WantedBy=multi-user.target
sudo systemd-analyze verify /etc/systemd/system/claude-<name>.service
sudo systemctl daemon-reload
sudo systemctl enable --now claude-<name>.service

--remote-control "<Display Name>" sets the name shown in claude.ai/code. Every restart resumes the same conversation on the same URL.

3. Rotating the logs

Debug logs and bridge transcripts grow without limit. /etc/logrotate.d/claude-cache:

# Claude Code debug logs and bridge transcripts written by the claude-rc services.
# claude keeps these files open, so copytruncate instead of moving them.
<home>/.cache/claude-rc/*.log <home>/.cache/claude-rc/*.jsonl {
    su <user> <group>
    daily
    maxsize 50M
    rotate 7
    maxage 14
    compress
    delaycompress
    copytruncate
    missingok
    notifempty
}

The system logrotate.timer picks it up daily, and new services need no change as long as their --debug-file is a .log in that directory. Rotated copies (*.log.1, *.log.2.gz) don’t match the globs, so they’re never re-rotated.

Test it with a dry run, then a forced rotation:

sudo logrotate -d /etc/logrotate.d/claude-cache      # dry run
sudo logrotate -v -f /etc/logrotate.d/claude-cache   # real rotation
tr -dc '\000' < ~/.cache/claude-rc/<name>.log | wc -c   # must print 0

That last line checks the truncated log contains no NUL bytes, which proves claude writes in append mode and copytruncate is safe.

One rule above all: never rotate ~/.claude/projects/**/*.jsonl. Those are the actual conversations, and the dedicated services resume from them on every restart.

Restarting without losing work

Before any systemctl restart — a unit edit, an upgrade — look at the session first:

tmux -L rc-<name> capture-pane -p -t rc

Don’t restart it while it’s working, while its input box holds an unsent draft, or while it has scheduled jobs created in the session: those only live in the running process and are lost on restart.

A restart briefly drops the Remote Control connection and interrupts any turn in progress. Dedicated sessions come back on the same URL; a generic server re-registers and re-adopts its sessions.

To change a flag (model, effort, name), edit the unit, then:

sudo systemd-analyze verify /etc/systemd/system/claude-<name>.service
sudo systemctl daemon-reload
sudo systemctl restart claude-<name>.service

A word on permissions

These units run Claude with --permission-mode auto, around the clock, on a machine you care about. That’s the point — you want it to get on with work while you’re away — but it’s worth taking seriously: whoever can open your claude.ai account can drive this machine. Protect that account, and keep the services to directories you’re happy for it to work in.

If you ask Claude Code itself to set this up, expect it to ask for explicit permission before writing unit files or running systemctl. That’s by design; say yes, or run the commands yourself.


That’s it: a few unit files, one logrotate rule, and Claude Code is waiting in my pocket whenever I need it — reboots included.

claude-codelinuxsystemd