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).
This commit is contained in:
@@ -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__<server>` 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
|
||||
```
|
||||
@@ -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}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"negsplit": {
|
||||
"command": "negsplit",
|
||||
"args": ["mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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": []
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"theme": "dark",
|
||||
"sandbox": {
|
||||
"network": {
|
||||
"strictAllowlist": true,
|
||||
"allowedDomains": []
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user