Skip to content

exit 0 when accessibility is missing - #13

Open
andredezzy wants to merge 1 commit into
digimata:masterfrom
andredezzy:fix/no-relaunch-loop-without-accessibility
Open

exit 0 when accessibility is missing#13
andredezzy wants to merge 1 commit into
digimata:masterfrom
andredezzy:fix/no-relaunch-loop-without-accessibility

Conversation

@andredezzy

@andredezzy andredezzy commented Jul 30, 2026

Copy link
Copy Markdown

Problem

When Accessibility is not granted, HotkeyMonitor.start() opens the system prompt via AXIsProcessTrustedWithOptions([prompt: true]) and throws tapCreateFailed. Parrot.run() turned that into ExitCode(1).

The LaunchAgent written by parrot install --launch-at-login sets KeepAlive{SuccessfulExit: false}, so launchd relaunches the daemon on that nonzero exit. The fresh process prompts again. Each cycle also reloads the model, so the loop is slow enough to look like normal startup while it stacks up dialogs.

Real log from a machine where the grant went stale (the binary was rebuilt, so its code hash no longer matched the stored grant):

loading whisper-base.en...
✓ whisper-base.en ready
accessibility not granted — system prompt opened. Grant access, then quit and relaunch parrot.
failed to register hotkey tap: tapCreateFailed
run `parrot setup` to configure permissions.
loading whisper-base.en...
✓ whisper-base.en ready
accessibility not granted — system prompt opened. Grant access, then quit and relaunch parrot.
failed to register hotkey tap: tapCreateFailed
run `parrot setup` to configure permissions.
... (repeats)

Nine "parrot would like to control this computer" dialogs in a row before the grant landed.

Note this is reachable without any local rebuild: since the released binary is adhoc, linker-signed, TCC has no stable designated requirement to match, so a normal upgrade to a new release can also invalidate the existing grant and trigger the same loop.

Fix

Exit 0. A missing permission is not a transient failure — relaunching cannot clear it, only a user action in System Settings can. Exiting cleanly means KeepAlive{SuccessfulExit: false} leaves the daemon alone, the user grants once, and starts parrot again (or it starts at next login via RunAtLoad).

The printed guidance (run \parrot setup` to configure permissions.`) is unchanged, so the user still gets told what to do — just once instead of on a loop.

Precedent in the codebase

Setup.swift already resolves the identical situation this way — it prompts for
Accessibility, tells the user what to do, and exits 0:

print("  2. Re-run `parrot setup` — macOS only picks up the grant on a fresh process.")
throw ExitCode(0)

This change makes run consistent with setup rather than introducing a new
convention.

Alternative considered

Leaving ExitCode(1) and dropping KeepAlive from the generated plist instead.
Rejected on two counts: the plist already exists on disk for everyone who has
run parrot install, so a generator change would never reach them; and
KeepAlive is still wanted for genuine crashes, which this does not affect —
only the permission path exits 0.

Verified

Reproduced and fixed on macOS 26.4.1. Replacing the installed binary invalidates
TCC's code-hash match, which is what makes the daemon hit this path:

  • Before: nine "parrot would like to control this computer" dialogs in a row,
    each cycle reloading the model first.
  • After: one dialog, process exits 0, launchd leaves it alone
    (launchctl list shows - 0 com.digimata.parrot). Granting once and
    starting again reaches listening on fn hold normally.

No PR CI exists in this repo (release.yml runs only on v* tags), so the
branch was also built locally with swift build -c release — clean.

HotkeyMonitor.start() opens the Accessibility prompt and throws
tapCreateFailed. run() turned that into ExitCode(1), and the LaunchAgent sets
KeepAlive{SuccessfulExit: false}, so launchd relaunched the daemon, which
prompted again -- nine dialogs in a row before the grant landed.

A missing permission is not transient, so a relaunch cannot clear it. Exit 0
and let the user grant once, then start parrot again.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant