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

  1. See everything - List all services from User Agents, Global Agents, and System Daemons with live status
  2. Pause vs Disable - Distinguish temporary unload (bootout) from permanent disable (bootout + disable)
  3. Detail pane - Full plist inspection without opening files manually
  4. Safety - Confirmation dialogs for destructive actions, sudo handling for system services
  5. 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, validation
  • launchctl/ — Pure side effects: all launchctl commands as tea.Cmd
  • ui/ — Styles and keybindings only
  • confirm/ — Confirmation logic
  • model/ — 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 .plist files
  • Parse each plist into LaunchService struct
  • Enrich with launchctl list output (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 print and blame for detail pane
  • Log file opening via $PAGER
  • Bulk action support with sudo -n for non-interactive batch operations

Files:

  • internal/launchctl/launchctl.go: All commands as tea.Cmd functions

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/Update
  • internal/model/delegate.go: Custom list item rendering
  • internal/ui/styles.go: Color palette and component styles
  • internal/ui/keymap.go: All keybindings
  • internal/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 launchctl for 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]bool between 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 launchctl rather 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