LaunchControl has been on my Mac for years. It’s the only real GUI for managing launchd agents and daemons—viewing what’s running, toggling services on and off, debugging why something won’t start. For a long time, it was great. Then it went paid.
Fair enough—it’s a solid app, the developer deserves to get paid. But I use this maybe once a month. Adobe installs a fleet of update daemons and cloud sync agents. Google drops background updaters into /Library/LaunchDaemons. Every creative app wants to phone home on a schedule. For something I reach for that infrequently, I couldn’t justify the license.
Meanwhile, LaunchControl kept growing. AI-powered job modification with support for six LLM providers. A drag-and-drop key palette. Three editor modes. It became a full IDE for launchd. I just want to see what’s running and shut the resource hogs up.
So I’d been doing it manually. launchctl list, launchctl bootout, occasionally launchctl disable when something really needed to stay gone. It works, but the syntax is awful—modern launchctl wants domain targets like gui/501/com.example.agent and you better not mix up bootout (temporary) with disable (permanent) or you’ll wonder why something came back after reboot.
This is Wintermute—a focused TUI for macOS launchd management. See everything, control what matters, skip what doesn’t. Named after the AI from Neuromancer (the one that orchestrates systems from behind the scenes, making decisions about what runs and what doesn’t). For a launchd manager, it fit perfectly.
Why This Exists
The problem: Managing launchd agents and daemons on macOS requires either a paid GUI app or memorizing launchctl syntax that actively fights you.
Existing solutions didn’t fit:
- LaunchControl: The gold standard—genuinely good app—but paid license for something I use monthly, and it’s grown into a full launchd IDE with AI integration, three editor modes, and a key palette I’ll never touch
- launchk (Rust): The only serious TUI competitor—126 stars, solid inspection, but no pause/disable distinction and no confirmation dialogs. Observer only.
- launch-tui (Go): Abandoned since 2022, user agents only, 15 commits total
- Manual terminal: Works but painful. Domain target syntax, no visual overview, easy to confuse temporary vs permanent actions
The gap: A keyboard-driven TUI that understands the difference between “shut up for now” and “stay gone forever”—without the GUI, the bloat, or the price tag.
The goal: See all my launch agents and daemons. Pause the annoying ones. Permanently disable the resource hogs from Adobe, Google, and friends. Do it from the terminal in under 10 seconds.
The Plan
This is the full implementation plan that drove the build. If you want to build your own variant—different TUI framework, different language, or just a different take on launchd management—copy this to your AI assistant and adapt. The architecture decisions and launchd domain knowledge transfer regardless of stack.
Plan: wintermute-plan.md
Wintermute Implementation Plan
Overview
A macOS TUI built on the Charmbracelet stack (Bubbletea, Bubbles, Lipgloss) for managing launchd agents and daemons across three domains.
Goals
- See everything - List all services from User Agents, Global Agents, and System Daemons with live status
- Pause vs Disable - Distinguish temporary unload (bootout) from permanent disable (bootout + disable)
- Detail pane - Full plist inspection without opening files manually
- Safety - Confirmation dialogs for destructive actions, sudo handling for system services
- Live updates - Watch plist directories with fsnotify for automatic refresh
Architecture Decisions
Framework: Charmbracelet Stack
- Bubbletea over raw termbox: Elm Architecture keeps state management clean
- Bubbles for list, viewport, help, spinner: Pre-built components, no wheel reinvention
- Lipgloss for styling: ANSI 0-15 palette adapts to any terminal theme
Package Structure: Strict Import Boundaries
service/— Pure data: plist discovery, parsing, validationlaunchctl/— Pure side effects: all launchctl commands astea.Cmdui/— Styles and keybindings onlyconfirm/— Confirmation logicmodel/— Root model, the only package that imports everything else
Plist Parsing: howett.net/plist
- howett.net/plist over manual XML: Handles binary and XML plists transparently
- Decode into
map[string]interface{}for full key access, typed struct for known fields
Implementation Tasks
Task 1: Service Discovery
- Scan three directories for
.plistfiles - Parse each plist into
LaunchServicestruct - Enrich with
launchctl listoutput (PID, exit status) - Enrich with
launchctl print-disabled(override database state)
Files:
internal/service/service.go: Discovery, parsing, enrichment, validation
Verification:
go run ./cmd/wintermute # Should list all services with correct status
Task 2: launchctl Commands
- Implement Pause (bootout), Disable (bootout + disable), Enable (enable + bootstrap), Restart (kickstart -k)
launchctl printandblamefor detail pane- Log file opening via
$PAGER - Bulk action support with
sudo -nfor non-interactive batch operations
Files:
internal/launchctl/launchctl.go: All commands astea.Cmdfunctions
Task 3: TUI Model
- Split-pane layout: service list (1/3) + detail pane (2/3)
- Domain filtering (All, User, Global, System)
- Focus management (list, detail, info viewport)
- Confirmation flow for destructive actions
- Help overlay, status bar, loading spinner
- fsnotify file watcher for live directory monitoring
Files:
internal/model/model.go: Root model with View/Updateinternal/model/delegate.go: Custom list item renderinginternal/ui/styles.go: Color palette and component stylesinternal/ui/keymap.go: All keybindingsinternal/confirm/confirm.go: Confirmation rules
Critical Files
cmd/wintermute/main.go Entry point
internal/
service/service.go Plist discovery + parsing
launchctl/launchctl.go All launchctl commands
model/model.go Root model (View/Update)
model/delegate.go List item rendering
ui/styles.go Theme and styles
ui/keymap.go Keybindings
confirm/confirm.go Confirmation logic
Trade-offs
Shelling out vs XPC
- Chose shelling out to
launchctlfor simplicity - Trade-off: Less efficient than launchk’s direct XPC approach
- Benefit: Much simpler implementation, no CGO, no private framework dependencies
ANSI 0-15 vs Custom Colors
- Chose base ANSI palette that adapts to terminal theme
- Trade-off: Less control over exact colors
- Benefit: Looks correct in any terminal color scheme—dark, light, or custom
Shared Map vs Immutable State
- Chose shared
map[string]boolbetween Model and delegate via Go reference semantics - Trade-off: Mutation visible from two places
- Benefit: No need to re-set delegate on every selection change
How Claude Code Actually Worked
The morning started with research, not code. I gave Claude the intent—“replace LaunchControl with a focused TUI”—along with references to a few existing tools I’d found (launchk, launch-tui, lctl). What came back was a 400-line research document: the full launchd domain model, every plist key that matters, modern launchctl syntax, the competitive landscape, and a recommended feature set. That doc drove everything that followed.
Then naming. I wanted something better than “launchd-tui.” We kicked around options and landed on Wintermute—the AI from Neuromancer. Once that clicked, the theme bled into everything: section headers called “ANOMALIES” and “I/O MATRIX” in the detail pane, random Neuromancer quotes on exit. A name shouldn’t matter this much. It does.
Go and the Charmbracelet stack are a well-documented combination, so Claude had plenty to work with. The tricky parts were all launchd-specific: launchctl enable must happen before bootstrap (or you get error 5), the override database takes precedence over the plist’s Disabled key, and sudo -n is the right call for bulk actions to avoid multiple password prompts. But the research doc had mapped all of this out already. The code followed the spec.
One example of research paying off early: sudo handling. Single actions on system services use tea.ExecProcess, which pauses the entire TUI and hands control to the terminal for the password prompt. Bulk actions can’t do that—you’d get prompted for every service. So those use sudo -n (non-interactive) and fail gracefully if a password is needed. Without that worked out in advance, you’d discover it the hard way when your TUI freezes mid-batch.
Total: ~4 hours. Research and planning took the first chunk. The code came together fast after that.
Results
Build time: One morning (~4 hours from research to working TUI)
First run: Pointed it at my Mac and immediately saw the usual suspects—Adobe’s com.adobe.AdobeCreativeCloud, Google’s com.google.keystone.agent, various updaters and sync daemons all happily running in the background. Filtered by domain, selected the ones I didn’t need, disabled them in bulk.
Already using it: That’s the whole point. Next time Adobe or Google drops a new background agent on my system, I open Wintermute, press / to filter, d to disable. Done. No remembering launchctl bootout gui/501/com.adobe.whatever.
What shipped vs the plan:
The research doc listed nine “nice-to-haves.” Seven shipped in the first build: launchctl print viewer, blame display, log file opening, plist validation warnings, override database view, bulk operations, and fsnotify live refresh. The only things left out are BTM/Login Items parsing and clipboard export—neither of which I actually need. When the spec is solid, you don’t leave much on the floor.
Limitations:
- No BTM database parsing (macOS 13+ login items show up in System Settings but not here)
- Shells out to
launchctlrather than using XPC directly (good enough for monthly use) - No plist editing—intentionally. Observation and control, not creation.
What’s next: Nothing. See services, silence the noisy ones, move on. LaunchControl was great when I needed more. I don’t anymore.
Tech stack: Go 1.22, Bubbletea, Bubbles, Lipgloss, fsnotify, howett.net/plist Build time: 1 morning (~4 hours) Lines of code: 2,000 across 8 files