From 3a2d2b7c3755139da719f1f96b52b68419fbaf16 Mon Sep 17 00:00:00 2001 From: Daniele Fucini Date: Sun, 1 Nov 2026 11:06:58 +0100 Subject: [PATCH] Add the shared Claude Remote Control setup Units, settings and setup README for the claude-rc user, which serves my projects' read-only MCP servers to the Claude app (negsplit for now). --- .config/claude-rc/README.md | 75 +++++++++++++++++++++ .config/claude-rc/claude-rc-notify.service | 13 ++++ .config/claude-rc/claude-rc-recover.service | 8 +++ .config/claude-rc/claude-rc-recover.timer | 9 +++ .config/claude-rc/claude-rc.service | 39 +++++++++++ .config/claude-rc/mcp.json | 8 +++ .config/claude-rc/project-settings.json | 36 ++++++++++ .config/claude-rc/user-settings.json | 9 +++ 8 files changed, 197 insertions(+) create mode 100644 .config/claude-rc/README.md create mode 100644 .config/claude-rc/claude-rc-notify.service create mode 100644 .config/claude-rc/claude-rc-recover.service create mode 100644 .config/claude-rc/claude-rc-recover.timer create mode 100644 .config/claude-rc/claude-rc.service create mode 100644 .config/claude-rc/mcp.json create mode 100644 .config/claude-rc/project-settings.json create mode 100644 .config/claude-rc/user-settings.json diff --git a/.config/claude-rc/README.md b/.config/claude-rc/README.md new file mode 100644 index 0000000..b047e8e --- /dev/null +++ b/.config/claude-rc/README.md @@ -0,0 +1,75 @@ +# Always-on Remote Control + +Reference copy of a machine setup: Claude Code Remote Control running as its own system user `claude-rc`, so my +projects' read-only MCP servers (now: negsplit) can be used from the Claude app on the phone. One user and one +session (`data`) for all projects. The user holds only read-only database logins; Bash runs sandboxed without +network and can't read them (only the MCP servers, which run outside the sandbox, do). Nothing secret in this +folder: the ntfy topic lives only in `/etc/claude-rc-notify.env`. + +Needs `claude-code`, `bubblewrap`, `socat` and each project's MCP server installed. Per project: + +- negsplit: the installed `negsplit` (with `python-mcp`) and the `garmin_ro` grants from its + `sql/garmin_ro_grants.sql`. A per-user query limit is a useful safety net (the session can lift it): + + ``` + mariadb -u root -p -e "ALTER USER 'garmin_ro'@'localhost' WITH MAX_STATEMENT_TIME 30;" + ``` + +## Setup + +Run from this folder. User and folders: + +``` +doas useradd --system --create-home --home-dir /var/lib/claude-rc --shell /usr/bin/nologin claude-rc +doas chmod 700 /var/lib/claude-rc +doas install -d -o claude-rc -g claude-rc -m 700 /var/lib/claude-rc/.config /var/lib/claude-rc/.claude /var/lib/claude-rc/data /var/lib/claude-rc/data/.claude +doas install -o claude-rc -g claude-rc -m 644 mcp.json /var/lib/claude-rc/data/.mcp.json +doas install -o claude-rc -g claude-rc -m 644 project-settings.json /var/lib/claude-rc/data/.claude/settings.json +doas install -o claude-rc -g claude-rc -m 644 user-settings.json /var/lib/claude-rc/.claude/settings.json +``` + +`strictAllowlist` must stay in the user settings: in project settings it has no effect. + +Each project's config holds only a `[readonly]` section. negsplit (`garmin_ro`): + +``` +doas install -d -o claude-rc -g claude-rc -m 700 /var/lib/claude-rc/.config/negsplit +doas install -o claude-rc -g claude-rc -m 600 config.toml /var/lib/claude-rc/.config/negsplit/config.toml +``` + +Log in, trust the folder and enable Remote Control once, interactively: + +``` +doas -u claude-rc sh -c 'cd /var/lib/claude-rc/data && exec claude auth login' +doas -u claude-rc sh -c 'cd /var/lib/claude-rc/data && exec claude' +doas -u claude-rc sh -c 'cd /var/lib/claude-rc/data && exec claude remote-control --name data --spawn session' +``` + +ntfy topic for the failure alert (subscribe to it on ntfy.sh), then the units: + +``` +doas sh -c 'umask 077; printf "NTFY_TOPIC=claude-rc-%s\n" "$(openssl rand -hex 16)" > /etc/claude-rc-notify.env' +doas install -m 644 claude-rc.service claude-rc-notify.service claude-rc-recover.service claude-rc-recover.timer -t /etc/systemd/system/ +doas systemctl daemon-reload +doas systemctl enable --now claude-rc.service claude-rc-recover.timer +``` + +The service restarts with a growing delay (30 s to 10 min); after about 50 minutes of failures it gives up and +sends the alert. The timer restarts it every 3 hours if it gave up, so a long network outage doesn't stop it for +good. After upgrading a project's MCP server, or changing `mcp.json` or the settings, restart `claude-rc.service` +so phone sessions use the new version. + +## Adding a project + +Add its server to `mcp.json` and `mcp__` to the `allow` list in `project-settings.json` (plus a `Read` rule +for its chart folder, if any), install its `[readonly]` config as above, reinstall the changed files and restart +the service. + +## Dead login + +``` +doas systemctl stop claude-rc.service +doas -u claude-rc sh -c 'cd /var/lib/claude-rc/data && exec claude auth login' +doas systemctl reset-failed claude-rc.service +doas systemctl start claude-rc.service +``` diff --git a/.config/claude-rc/claude-rc-notify.service b/.config/claude-rc/claude-rc-notify.service new file mode 100644 index 0000000..0ba7573 --- /dev/null +++ b/.config/claude-rc/claude-rc-notify.service @@ -0,0 +1,13 @@ +[Unit] +Description=Notify via ntfy that Claude Remote Control stopped + +[Service] +Type=oneshot +DynamicUser=yes +# NTFY_TOPIC=...: root-only (0600), the topic is the only secret +EnvironmentFile=/etc/claude-rc-notify.env +TimeoutStartSec=10min +ExecStart=/usr/bin/curl -fsS -m 30 --retry 5 --retry-delay 60 --retry-all-errors \ + -H "Title: Claude Remote Control stopped" -H "Tags: warning" \ + -d "%H: claude-rc.service keeps failing (dead login?). Retried every 3 h; fix at home: claude auth login as claude-rc." \ + https://ntfy.sh/${NTFY_TOPIC} diff --git a/.config/claude-rc/claude-rc-recover.service b/.config/claude-rc/claude-rc-recover.service new file mode 100644 index 0000000..6109ddc --- /dev/null +++ b/.config/claude-rc/claude-rc-recover.service @@ -0,0 +1,8 @@ +[Unit] +Description=Restart Claude Remote Control if it gave up (e.g. after a long network outage) + +[Service] +Type=oneshot +ExecCondition=/usr/bin/systemctl is-failed --quiet claude-rc.service +ExecStart=/usr/bin/systemctl reset-failed claude-rc.service +ExecStart=/usr/bin/systemctl start --no-block claude-rc.service diff --git a/.config/claude-rc/claude-rc-recover.timer b/.config/claude-rc/claude-rc-recover.timer new file mode 100644 index 0000000..0ed94d1 --- /dev/null +++ b/.config/claude-rc/claude-rc-recover.timer @@ -0,0 +1,9 @@ +[Unit] +Description=Every 3 h, restart Claude Remote Control if it gave up + +[Timer] +OnBootSec=1h +OnUnitActiveSec=3h + +[Install] +WantedBy=timers.target diff --git a/.config/claude-rc/claude-rc.service b/.config/claude-rc/claude-rc.service new file mode 100644 index 0000000..05f83e5 --- /dev/null +++ b/.config/claude-rc/claude-rc.service @@ -0,0 +1,39 @@ +[Unit] +Description=Claude Code Remote Control (read-only MCP servers) +Wants=network-online.target +After=network-online.target mariadb.service +# about 50 min of failed starts (back-off below): give up and notify +StartLimitIntervalSec=1h +StartLimitBurst=8 +OnFailure=claude-rc-notify.service + +[Service] +User=claude-rc +Group=claude-rc +WorkingDirectory=/var/lib/claude-rc/data +# strips credentials from subprocess environments; on Linux also sandboxes ! shell commands +Environment=CLAUDE_CODE_SUBPROCESS_ENV_SCRUB=1 +ExecStart=/usr/bin/claude remote-control --name data --spawn session +Restart=always +RestartSec=30s +RestartSteps=4 +RestartMaxDelaySec=10min +ProtectSystem=strict +ProtectHome=yes +ReadWritePaths=/var/lib/claude-rc +PrivateTmp=yes +NoNewPrivileges=yes +ProtectKernelTunables=yes +ProtectKernelModules=yes +ProtectControlGroups=yes +ProtectKernelLogs=yes +ProtectClock=yes +ProtectHostname=yes +CapabilityBoundingSet= +RestrictSUIDSGID=yes +RestrictRealtime=yes +LockPersonality=yes +# no RestrictNamespaces/MemoryDenyWriteExecute: the Bash sandbox (bubblewrap) needs user namespaces + +[Install] +WantedBy=multi-user.target diff --git a/.config/claude-rc/mcp.json b/.config/claude-rc/mcp.json new file mode 100644 index 0000000..f2743bf --- /dev/null +++ b/.config/claude-rc/mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "negsplit": { + "command": "negsplit", + "args": ["mcp"] + } + } +} diff --git a/.config/claude-rc/project-settings.json b/.config/claude-rc/project-settings.json new file mode 100644 index 0000000..bb8f9a0 --- /dev/null +++ b/.config/claude-rc/project-settings.json @@ -0,0 +1,36 @@ +{ + "permissions": { + "defaultMode": "default", + "disableBypassPermissionsMode": "disable", + "disableAutoMode": "disable", + "allow": [ + "mcp__negsplit", + "Read(~/.local/share/negsplit/charts/**)" + ], + "deny": [ + "Read(~/.config/**)", + "Read(~/.claude/**)", + "Read(~/.claude.json)", + "Edit(~/.config/**)", + "Edit(~/.claude/**)", + "Edit(~/.claude.json)", + "Edit(./.claude/**)", + "Edit(./.mcp.json)", + "WebFetch", + "WebSearch" + ] + }, + "sandbox": { + "enabled": true, + "failIfUnavailable": true, + "allowUnsandboxedCommands": false, + "autoAllowBashIfSandboxed": true, + "filesystem": { + "denyRead": ["~/.config", "~/.claude", "~/.claude.json"], + "denyWrite": ["./.claude", "./.mcp.json"] + }, + "network": { + "allowedDomains": [] + } + } +} diff --git a/.config/claude-rc/user-settings.json b/.config/claude-rc/user-settings.json new file mode 100644 index 0000000..9f36be4 --- /dev/null +++ b/.config/claude-rc/user-settings.json @@ -0,0 +1,9 @@ +{ + "theme": "dark", + "sandbox": { + "network": { + "strictAllowlist": true, + "allowedDomains": [] + } + } +}