Skip to content

Install and set up

The cockpit is built from the repository; there is no signed download until a Developer ID certificate is configured for releases. A build on your own Mac takes a couple of minutes and needs no Apple account.

Requirements

On Needs
The Mac you sit at macOS 14 or later, Xcode 16 or later, XcodeGen (brew install xcodegen)
The same Mac, for the default route An SSH alias in ~/.ssh/config that reaches the runner host with a key (no password prompt)
The runner host The dashboard running (dashboard/fleetctl.sh status), version with /api/glance
For the out-of-band checks The GitHub CLI (gh) signed in on this Mac — optional, and only read when the dashboard is unreachable

Build the app

cd cockpit/App
cp Local.xcconfig.example Local.xcconfig   # optional: your signing identity and team
xcodegen generate
xcodebuild -project FleetCockpit.xcodeproj -scheme FleetCockpit -configuration Release build

The .xcodeproj is generated and never committed; re-run xcodegen generate after pulling changes to project.yml or adding files.

Signing. Without Local.xcconfig the app is signed ad hoc. That runs on the Mac that built it, but an ad-hoc build has no team, and so no app group: the desktop widgets stay empty and everything else works. To get widgets, put your own identity and team in Local.xcconfig (it is untracked):

CODE_SIGN_IDENTITY = Apple Development
DEVELOPMENT_TEAM = YOURTEAMID

A free Apple Development certificate is enough for your own Mac.

Install the app

Copy the build to /Applications, not somewhere under /tmp or DerivedData:

APP="$(xcodebuild -project FleetCockpit.xcodeproj -scheme FleetCockpit -configuration Release \
  -showBuildSettings | awk '/ BUILT_PRODUCTS_DIR /{print $3}')/Fleet Cockpit.app"
rm -rf "/Applications/Fleet Cockpit.app" && cp -R "$APP" /Applications/
open "/Applications/Fleet Cockpit.app"

LaunchServices ignores the URL schemes of apps under /private/tmp, and every Xcode build re-registers its DerivedData copy, so fleetcockpit:// links can land on a stale build. If they do, unregister the stray copies:

LSREG=/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister
$LSREG -dump | grep -o '/.*DerivedData.*/Fleet Cockpit.app' | sort -u | xargs -I{} $LSREG -u "{}"

On first launch macOS asks to allow notifications. Allow them: notifications are how the cockpit tells you about a transition while the popover is closed.

Open at login

Settings → System → Open at login registers the app with SMAppService. From a setup script, ask for it once instead; the app applies it at its next launch and clears the request:

defaults write io.github.addisdev.fleetcockpit pending.loginItem -bool YES
open "/Applications/Fleet Cockpit.app"

Check it took with sfltool dumpbtm | grep -A6 fleetcockpit (look for enabled, allowed). There is deliberately no URL for this: a web page can open fleetcockpit:// links, and it must not be able to add a login item.

No menu bar icon?

The app has no Dock icon: opening it again from Applications only brings back the menu bar item. On a Mac with a notch, a full menu bar drops the items that started most recently, and Fleet Cockpit is usually one of them. Check System Settings → Menu Bar that it is allowed, then pin it near the clock (the value is points from the right edge of the screen) and relaunch:

defaults write io.github.addisdev.fleetcockpit "NSStatusItem Preferred Position Item-0" -float 420
osascript -e 'tell application id "io.github.addisdev.fleetcockpit" to quit'
open "/Applications/Fleet Cockpit.app"

⌘-dragging the icon writes the same setting. ⌃⌥⌘F opens the panel whether or not the icon is visible.

Reach the dashboard

The dashboard binds to loopback on the runner host. The default route needs nothing changed there: the app opens an SSH tunnel through aliases from your ~/.ssh/config, trying each in turn — by default ultra, then ultra-ts for a tailnet route. A minimal entry:

Host ultra
  HostName ultra.local
  User you
  IdentityFile ~/.ssh/id_ed25519

Test it the way the app will run it — no prompt allowed:

ssh -o BatchMode=yes ultra 'curl -s 127.0.0.1:7878/api/health'

If you serve the dashboard over Tailscale Serve or the LAN instead, choose Direct URL in Settings. How the tunnel works, and why it cannot outlive the app, is under Settings and files.

Pair, to act

The cockpit is read-only until you pair it.

  1. Open Settings → Control and check Fleet root on the host (default ~/actions-runners).
  2. Press Pair this Mac. The app runs fleetctl.sh pair on the host over your first SSH alias, exchanges the six-digit code for this Mac's own device token, and keeps it in the Keychain.

open fleetcockpit://pair does the same from a terminal. Pairing needs the tunnel route; with a Direct URL, pair from a Mac that has SSH.

The master token never leaves the host. The cockpit is revocable like any paired phone:

# on the runner host
cd ~/actions-runners/dashboard
./fleetctl.sh devices          # list paired devices
./fleetctl.sh revoke <key>     # revoke one

Forget token in Settings removes it from this Mac.

Install the command line

cd cockpit
swift build -c release
mkdir -p ~/.local/lib/fleet-cockpit ~/.local/bin
rm -rf ~/.local/lib/fleet-cockpit/cockpit ~/.local/lib/fleet-cockpit/FleetCockpit_CockpitCore.bundle
cp -R .build/release/cockpit .build/release/FleetCockpit_CockpitCore.bundle ~/.local/lib/fleet-cockpit/
ln -sf ~/.local/lib/fleet-cockpit/cockpit ~/.local/bin/cockpit
cockpit status

Two things in that recipe matter:

  • Copy the binary and its resource bundle together. The bundle holds the fixtures; without it --fixture and cockpit fixtures fail.
  • Remove the old binary before copying. Copying over a signed binary in place (cp -f) leaves the kernel's cached signature for the old file on the new one, and the next run is killed on launch (exit 137).

The CLI pairs separately, with its own token: cockpit pair. See Command line.

Agent sessions

claude mcp add --scope user fleet-cockpit -- ~/.local/bin/cockpit mcp
mkdir -p ~/.claude/skills ~/.cursor/skills
ln -sfn "$PWD/cockpit/skills/fleet-cockpit" ~/.claude/skills/fleet-cockpit
ln -sfn "$PWD/cockpit/skills/fleet-cockpit" ~/.cursor/skills/fleet-cockpit

What these give a session is on Agent sessions.

Update

git pull
cd cockpit/App && xcodegen generate && xcodebuild -project FleetCockpit.xcodeproj \
  -scheme FleetCockpit -configuration Release build

Then repeat the copy to /Applications (quit the app first) and the CLI copy. Settings, the pairing token and the login item survive a reinstall: they belong to the bundle id, not the file.

Uninstall

Turn off Settings → System → Open at login first, so no login item is left pointing at a deleted app. Then:

osascript -e 'quit app "Fleet Cockpit"'
rm -rf "/Applications/Fleet Cockpit.app"
defaults delete io.github.addisdev.fleetcockpit
rm -rf ~/Library/Application\ Support/FleetCockpit
security delete-generic-password -s io.github.addisdev.fleetcockpit 2>/dev/null
rm -f ~/.local/bin/cockpit && rm -rf ~/.local/lib/fleet-cockpit
claude mcp remove fleet-cockpit -s user; rm -rf ~/.claude/skills/fleet-cockpit ~/.cursor/skills/fleet-cockpit

Then revoke the app's and the CLI's device tokens on the host with ./fleetctl.sh revoke.