feat: macOS menu bar app for remote CLI management#1
feat: macOS menu bar app for remote CLI management#1gldc wants to merge 10 commits intobuckle42:masterfrom
Conversation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Also remove unused signal import from menubar.py. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add Tailscale MagicDNS hostname to the menu bar app so users can see their .ts.net domain alongside the IP. Also fix start-remote-cli.sh to prepend Homebrew Python paths so voice-wrapper launches correctly when started from launchd or the menubar app. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Calling launchctl load immediately starts a second instance of the menubar app, and launchctl unload kills the running one. Just write/remove the plist file instead — it takes effect on next login. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Falls back to Tailscale IP if MagicDNS is not available. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
buckle42
left a comment
There was a problem hiding this comment.
Hey @gldc — thanks for putting this together! The menu bar app is a great idea and fills a real usability gap. The UX thinking (three-state icon, quit dialog, MagicDNS fallback) is solid.
I found a few issues that need to be addressed before I can merge. Splitting them into critical (must fix) and important (should fix).
Critical
1. TCC violation in auto-start plist (menubar.py:228-237)
The generated plist points to os.path.abspath(__file__), which resolves to ~/Documents/.... macOS launchd agents can't access ~/Documents/ without Full Disk Access — the existing CLAUDE.md calls this out explicitly. The "Auto-start on Login" feature will silently fail after reboot.
The existing remote-cli.plist handles this by requiring scripts be copied to ~/.local/bin/remote-cli/. The menubar plist needs a similar approach — either copy the script to a safe location, or detect and warn when running from a TCC-protected path.
2. XML injection in plist template (menubar.py:26-42)
The plist is built with .format() string interpolation. If the repo path contains &, <, or >, the plist becomes malformed XML. Recommend using plistlib from the standard library instead, which handles escaping automatically.
3. Zombie processes on repeated start/stop (menubar.py:196-200)
start-remote-cli.sh runs a blocking watchdog loop that never returns. Each "Start Services" click spawns a Popen child that's never tracked or cleaned up. Repeated start/stop cycles will accumulate orphaned processes. Either track the Popen handle and terminate on stop, or use start_new_session=True to fully detach.
Important
4. Expensive polling (menubar.py:118-158)
Two tailscale subprocess calls every 5 seconds is heavy for a menu bar app. Tailscale IP/DNS rarely changes mid-session. Consider polling Tailscale info every 60 seconds — the PID-based health checks are fine at 5s since they're just file reads and signal checks.
5. Race condition in toggle (menubar.py:187-192)
Toggle logic compares the button title string to decide start vs stop, but the health check timer updates that title asynchronously. Could cause double-starts. A boolean flag for intent (separate from display state) would be more reliable.
6. No guard against double-start (menubar.py:196-200)
Nothing prevents calling _start_services() when already running. The start script does pkill -f "ttyd" which kills the running instance — brief outage that wasn't the user's intent.
7. Dead config code (menubar.py:106-108)
auto_start_services is never set to True anywhere. The "Auto-start on Login" toggle controls the launchd plist (different concept). This config path is unused and confusing — either wire it to a menu item or remove it.
8. Incomplete launchctl handling (menubar.py:241-243)
Uninstall deletes the plist but doesn't unload from launchd — the agent stays loaded until logout. Install doesn't call launchctl load either. The toggle doesn't take effect immediately.
Minor (can address later)
- Missing
rumpsimport guard — users get a rawModuleNotFoundError import sysinside a method instead of at the top of the file- Doesn't use
shutil.which("tailscale")with fallback likevoice-wrapper.pydoes - Hardcoded
/opt/homebrew/opt/python@3.11/in the PATH fix is specific to your machine — other users may have different Python versions - No user feedback when log files don't exist yet
Happy to discuss any of these if you have questions. The core concept is great — just needs these fixes to be solid for other users cloning the repo. Thanks again for the contribution!
Summary
scripts/menubar.py) for one-click control of remote CLI servicesstart-remote-cli.shso voice-wrapper launches correctly from launchd/menubarTest plan
rumps(pip3 install rumps) and launch withnohup python3 scripts/menubar.py &>/dev/null &🤖 Generated with Claude Code