Skip to content

Latest commit

 

History

History
178 lines (121 loc) · 8.83 KB

File metadata and controls

178 lines (121 loc) · 8.83 KB

Upgrading to skhd.zig 0.0.21 (macOS Tahoe compatibility)

0.0.21 fixes the actual root cause of "skhd doesn't start on reboot". The 0.0.18 rework was correct on packaging and signing but missed the gatekeeper: macOS Background Tasks Manager (BTM) silently marked any hand-installed LaunchAgent as disallowed. 0.0.21 switches to SMAppService, which gets a proper [enabled, allowed, notified] BTM entry that auto-loads at every login.

If you upgraded to 0.0.18–0.0.20 and skhd still doesn't always start after reboot, 0.0.21 is the fix you want.

Migrating from 0.0.20 → 0.0.21

brew upgrade skhd-zig

# Re-register via SMAppService. The `skhd` shim brew puts on PATH is a
# symlink into the bundle's inner binary, so SMAppService still sees the
# correct calling bundle and BTM gets a clean managed entry:
skhd --install-service

# Verify
skhd --status
# Expect: Registration status: enabled
#         Daemon running: Yes (PID …)
#         Hotkeys functional: Yes (event tap active)

That's it. Your accessibility grant carries over (TCC entry is bundle-ID-keyed, the cert hasn't changed), and BTM now has a proper managed entry that auto-loads on every reboot.

The old disallowed legacy BTM entry from previous versions is harmless once the new managed entry is in place — but you can clean it up via System Settings → General → Login Items & Extensions if you like.

What brew upgrade does for you (post-0.1.1)

The formula's post_install hook now runs skhd --start-service automatically after every install and upgrade. That means:

  • Legacy ~/Library/LaunchAgents/com.jackielii.skhd.plist gets booted out and deleted. If you ever installed a pre-0.0.21 version, this orphan plist shadowed the SMAppService registration on Tahoe — same Label, two definitions, launchd refused to spawn either (EX_CONFIG, 108: Invalid path: Contents/MacOS/skhd). It's now cleaned up on every upgrade.
  • SMAppService rebinds to the current Cellar bundle path. The previous version's binding pointed at the now-deleted Cellar/<old-version>/skhd.app; the post_install re-registration picks up the new path.
  • The daemon still self-heals the cdHash/TCC mismatch on the first event-tap attempt after the new binary spawns (see the section below). You may still need to re-toggle the Accessibility entry in System Settings once per upgrade — the daemon will print a single short instruction line in the log when this happens, instead of the previous wall of text repeated every 10 seconds.

If something looks wrong after a brew upgrade, the canonical recovery is still one command:

skhd --start-service   # idempotent — same as what post_install just ran
skhd --status          # verify

If keys stop working after brew upgrade (macOS Tahoe)

On macOS 15+ (Tahoe), TCC stores Input Monitoring grants with a csreq anchored to the binary's cdHash rather than the signing cert root. Every rebuild produces a new cdHash, so a brew upgrade silently invalidates the grant — the System Settings entry still shows as granted, but no events flow into the daemon.

Symptoms:

  • skhd --status shows the daemon running, but hotkeys don't fire.
  • System Settings → Privacy & Security → Accessibility (and/or Input Monitoring) shows skhd as enabled.
  • ~/Library/Logs/skhd.log shows the event tap was created but no key activity.

Since 0.1.2 the daemon does this automatically when it's launchd-managed: on the first event-tap creation failure it calls tccutil reset itself, writes a marker at ~/Library/Caches/com.jackielii.skhd/tcc_auto_reset_at to avoid reset loops on subsequent respawns, and emits a single short "go re-toggle in Settings" line in the log. You only need to perform step 2 below.

If you're on an older release or the auto-reset failed, the manual fix is:

# 1. Drop the stale grants so macOS will store a fresh, cert-root-anchored csreq
tccutil reset ListenEvent com.jackielii.skhd
tccutil reset Accessibility com.jackielii.skhd
skhd --restart-service

# 2. Re-toggle the entry in System Settings → Privacy & Security → Accessibility
#    (or accept the prompt if one appears). The first hotkey press triggers the
#    Input Monitoring prompt — approve it.

This issue recurs on every brew upgrade until Apple loosens the anchor policy; skhd --status includes the same fix in its remediation output when the tap is detected as denied.

Migrating from 0.0.17 or earlier (the original Tahoe rework)

Version 0.0.18 introduced the .app bundle structure and ~/Library/Logs/skhd.log. If you're coming from 0.0.17 or earlier you also need to perform the steps below before the SMAppService re-register above.

What changed and why

Area Before After
Distribution layout bare Mach-O at bin/skhd .app bundle (skhd.app/Contents/MacOS/skhd) with bare-binary symlink kept for CLI use
TCC entries path-keyed (/opt/homebrew/Cellar/.../bin/skhd) bundle-ID-keyed (com.jackielii.skhd)
LaunchAgent commands launchctl load -w / unload -w launchctl bootstrap / bootout (no persistent disable flag)
Plist ProgramArguments version-pinned Cellar path stable /opt/homebrew/opt/skhd-zig/... symlink
Plist log path /tmp/skhd_$USER.log (wiped at boot) ~/Library/Logs/skhd.log
Plist ThrottleInterval 30 s 10 s
CGEventTapCreate failures exit immediately, wait full throttle, repeat retry up to 10× at 500 ms before giving up

The combined effect: the daemon comes up reliably after a cold reboot, accessibility permissions persist across brew upgrade and rebuilds, and a previous --stop-service no longer prevents auto-load on the next login.

Required actions

These steps assume you installed the previous version via Homebrew. The order matters.

1. Stop the old service

skhd --stop-service

2. Upgrade

brew upgrade jackielii/tap/skhd-zig

The new formula installs skhd.app to <prefix>/skhd.app (e.g. /opt/homebrew/opt/skhd-zig/skhd.app) and symlinks the CLI into bin/skhd. macOS will prompt for Accessibility on first launch via the popup deep-link — no manual +/navigate step in System Settings.

3. Clear the legacy disable flag (if you ever ran the old --stop-service)

The old unload -w set a flag in launchd's persistent disable list. The new --start-service clears it automatically, but it's worth confirming it's gone:

launchctl print-disabled gui/$(id -u) | grep com.jackielii.skhd
# Expect: "com.jackielii.skhd" => enabled

If it shows disabled, run:

launchctl enable gui/$(id -u)/com.jackielii.skhd

4. Drop stale TCC entries from previous installs

The path-keyed accessibility entries from previous Cellar versions will silently shadow the new bundle-ID entry until removed:

sudo sqlite3 "/Library/Application Support/com.apple.TCC/TCC.db" \
  "DELETE FROM access WHERE client LIKE '%skhd-zig%' AND client_type=1;"

This deletes only path-keyed (client_type=1) rows. The new bundle-ID-keyed (client_type=0) entry created when you grant in step 6 below is left untouched on future runs.

5. Install the new LaunchAgent plist

skhd --install-service

The plist now points at /opt/homebrew/opt/skhd-zig/skhd.app/Contents/MacOS/skhd (stable across brew upgrade).

6. Grant Accessibility for skhd.app

skhd --install-service triggers the macOS Accessibility prompt with a deep-link to the right pane. In the dialog, click Open System Settings and toggle the skhd entry on — no manual +/navigate step needed.

You will only need to do this once. The bundle-ID-keyed TCC entry now persists across rebuilds and Homebrew upgrades.

7. Start

skhd --start-service

Watch the log at ~/Library/Logs/skhd.log. You should see:

info(skhd): Starting event tap
info(skhd): Event tap created successfully. skhd is now running.

Or if the daemon hits the early-boot WindowServer race once or twice, the new retry loop handles it:

warning(event_tap): Event tap creation failed (attempt 1/10), retrying in 500ms...
info(event_tap): Event tap created on attempt 2/10

Notes for source builds

If you build from source rather than installing via Homebrew:

zig build sign-app                          # produces a signed zig-out/skhd.app
ln -sfn "$(pwd)/zig-out/skhd.app" /Applications/skhd.app
/Applications/skhd.app/Contents/MacOS/skhd --install-service
/Applications/skhd.app/Contents/MacOS/skhd --start-service

zig build (without app) still produces the bare binary at zig-out/bin/skhd for quick development iteration — it just won't be visible in the Accessibility picker.

Troubleshooting

If anything goes sideways, see docs/CODE_SIGNING.md — the troubleshooting section there covers stale TCC entries, picker rejections, the misleading --status output, and signing problems.