Fleet changelogs · dev.ecs0.net
rdmsm4x-changelog-20260825-1319-claude-remote-control-boot-service

rdmsm4x-changelog-20260825-1319-claude-remote-control-boot-service

Claude Code Remote Control now runs as an always-on LaunchAgent on rdmsm4x, so a session is reachable from claude.ai/code and the Claude mobile app with nobody at the terminal. Installed, self-heal tested, and verified end to end.

What changed on rdmsm4x

Path Change
~/scripts/claude_rc_service.sh NEW v1.1.1, mode 755. Self-contained: install / status / restart / uninstall / logs + internal run (launchd entry point).
~/Library/LaunchAgents/net.dataroo.claude-rc.plist NEW. RunAtLoad, KeepAlive, ThrottleInterval 60, WorkingDirectory ~/dev.
~/Library/Logs/claude-rc/ NEW. claude-rc.log (rotates at 4 MB, one .1 kept) + tiny launchd.log.
~/dataroo.net/wiki/claude-remote-control-service-research.html NEW. Published research/design page, v0.2.
~/Desktop/claude_remote_research_ver0.2_20260825.html NEW. Copy for Rich. v0.1 removed.

Not changed, deliberately: ~/.claude/settings.json was NOT touched. permissions.defaultMode stays bypassPermissions — that key is Rich's alone and no agent writes it. No credential was written to disk and no long-lived token was created.

Service definition

claude remote-control \
  --remote-control-session-name-prefix rdmsm4x \
  --spawn same-dir \
  --capacity 4

Run from ~/dev. The wrapper unsets ANTHROPIC_BASE_URL, ANTHROPIC_API_KEY, DISABLE_TELEMETRY, DO_NOT_TRACK, CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC and DISABLE_GROWTHBOOK — each silently disables Remote Control and a LaunchAgent can inherit them. --verbose is deliberately omitted: it dumps entire SessionStart hook payloads as JSON.

Why a LaunchAgent and not a LaunchDaemon

Claude Code on macOS keeps its live OAuth credentials in the login keychain — verified by watching security find-generic-password -s "Claude Code-credentials" refresh its mdat on use (rdmbair15m5 2026-08-25 16:58Z, rdmsm4x 11:31Z). ~/.claude/.credentials.json is a stale leftover on both hosts (rdmsm4x copy mtime Aug 24 07:36, its token already expired Aug 24 15:36) and must not be designed around. The login keychain is locked until a GUI login, so a system-domain daemon cannot authenticate. That, not launchd, was the real pre-login blocker.

Verification evidence

Check Command Result
Server mode runs headless launched with stdin a pipe, stdout a file, no TTY Registered, printed a session URL. No tmux/pty needed.
Install preflight bash claude_rc_service.sh install binary, execute bit, workdir, workspace trust, RC consent all PASS
Registers a session log scrape session_01VLWyy76x9A9G9G3xzFUrBe
Self-heals kill -9 80639, wait 75s new pid 81711, runs = 3, state = running
Restart does not orphan compared session URL before/after kill same session reattached (~4h recovery window)
Reachable cross-host listed + messaged from rdmbair15m5 appears as rdmsm4x-ethereal-squirrel
End to end asked the session to run 4 commands replied rdmsm4x / /Users/richh/dev / 13:18:49 EDT / pid = 81711

Decisions Rich made (2026-08-25)

  1. Boot resilience: keep FileVault, use a LaunchAgent. Accepted trade — a cold boot needs a human at the unlock screen. Use sudo fdesetup authrestart for planned reboots.
  2. Topology: ~/dev with --spawn same-dir. ~/dev is trusted but is NOT a git repo, so --spawn worktree was unavailable there.
  3. Permissions: inherit bypassPermissions.
  4. Concurrency: --capacity 4 (default is 32; the box hit load 71 during a build).

Outstanding / known limits

Post-install fix: log volume (v1.1.1, 13:39 EDT)

The first run produced a 471 KB log in 20 minutes with no --verbose. Cause: server mode repaints its status block roughly once a second. On a TTY that repaints in place; to a FILE every repaint is appended. Measured 1099 copies of "space to show QR code" in 20 minutes, ~1.2 MB/hour of noise burying anything diagnostic.

Two things made naive fixes fail, both caught by testing before deploying:

  1. macOS /usr/bin/awk is BWK awk and has no strftime() — the first filter died on every line. Rewritten in python3.
  2. Consecutive-dedup is insufficient. With two sessions live the status block lists each on its own line, so the lines ALTERNATE and no two identical lines are ever adjacent. Replaced with time-windowed suppression (300s TTL, reports how many repeats were swallowed).
  3. Session ids live inside OSC-8 hyperlinks, so stripping OSC removed the id too and the filter reported zero session URLs. Fixed by matching the id on the RAW line first.

Regression-tested against two real captures before shipping: 3387 lines -> 11, and the alternating two-session case 128 lines -> 11.

Result measured in production: 454 bytes in 90 seconds, versus 471 KB for a comparable window before. status still finds the session URL, which is what it greps for.

Also changed: the wrapper no longer execs claude, so it survives to log the child's exit status (claude exited rc=N). launchd still restarts on wrapper exit via KeepAlive.

How to undo

bash ~/scripts/claude_rc_service.sh uninstall     # bootout + remove the plist
rm -f ~/scripts/claude_rc_service.sh
rm -rf ~/Library/Logs/claude-rc
rm -f ~/dataroo.net/wiki/claude-remote-control-service-research.html

Nothing else was modified; there is no other state to reverse.