Skip to content

macOS 27 blocks Finicky from reading browser profile data — all profile routing silently opens the default profile #556

Description

@dadatuputi

Full disclosure: troubleshooting performed with Claude, issue authored by Claude.

Describe the bug

On macOS 27 ("Golden Gate"), the OS refuses non-browser apps access to browser data directories under ~/Library/Application Support. Finicky reads those directories to resolve a configured profile name, so every read now fails with EPERM ("operation not permitted"). The profile list comes back empty, no name matches, and LaunchBrowser falls through to launching the browser with no profile argument at all — so the URL opens in whichever profile was last used.

There is no error, no dialog, and nothing in the default log. The handler looks like it simply routed to the wrong browser profile, so it reads as a config mistake. It took me a while to work out that my config was fine and the files were still there.

Confirmed on my machine for both browser families:

$ ls ~/Library/Application\ Support/Firefox
ls: /Users/…/Library/Application Support/Firefox: Operation not permitted

$ ls ~/Library/Application\ Support/Google/Chrome >/dev/null && echo readable || echo DENIED
DENIED

Note this is EPERM, not ENOENT — the files are all still present. stat() is permitted while open()/readdir() are refused, so existence tests pass and reads fail:

$ [ -f ~/Library/Application\ Support/Firefox/profiles.ini ] && echo exists
exists
$ cat ~/Library/Application\ Support/Firefox/profiles.ini
(nothing — refused)

This is the same protection that breaks Playwright's bundled Firefox on the identical directory (microsoft/playwright#42768). Related but not the same: #552 reports macOS 27.0 dropping Finicky's Automation permission for Chrome, which is a different TCC service. Nobody seems to have reported the app-data read denial yet.

Your configuration

export default {
  defaultBrowser: "Firefox:Personal",
  handlers: [
    { match: ["titanfile.com/*"], browser: "Firefox:Work" },
  ],
};

Any config using browser: "Browser:Profile" or profile: reproduces it.

To Reproduce

  1. On macOS 27, confirm the denial: ls ~/Library/Application\ Support/Firefox → Operation not permitted.
  2. Configure a handler with a browser profile, e.g. browser: "Firefox:Work".
  3. Open a matching URL.
  4. The URL opens in the last-used profile, not Work. No error is shown.
  5. The profile dropdown in the Finicky window is empty for that browser.
  6. Grant Finicky Full Disk Access in System Settings → Privacy & Security, restart it, and everything works again.

Environment


Where it happens

All in apps/finicky/src/browser/launcher.go on main:

Line What
181 Chromium Local State path — read refused
193 Firefox profiles.ini path — read refused
209 slog.Info("Error reading profiles.ini", …) — the denial, logged below the default level
230 slog.Warn("Could not find profile in Firefox profiles.", …) — reports an empty list as "not found"
237 slog.Info("Error reading Local State file", …) — same, Chromium side
370, 373 GetProfilesForBrowser — why the window's profile dropdown is empty

And LaunchBrowser (~line 69): when resolveBrowserProfileArgs returns false, -n, --args and the profile flag are all skipped, so the browser is launched with just the URL.

Two things make it hard to notice

  1. A denial and a genuine absence are indistinguishable in the code — both end up as an empty profile list.
  2. The fallback for an unresolved profile is "launch with no profile", which is silent and plausible-looking.

Possible fixes — happy to PR any of these if they'd be useful

  1. Distinguish EPERM from ENOENT and say so, with the Full Disk Access remedy, at Warn. Cheapest fix; turns a silent misroute into a one-line answer.
  2. Let the browser do the lookup when the list can't be read. Firefox's -P <name> is resolved by Firefox itself — SelectStartupProfile passes it to GetProfileByName, which searches the profiles.ini Firefox reads directly. Finicky's own read only ever confirmed a lookup Firefox was about to do anyway. Passing the name through unresolved restores name-based routing for classic profiles with no permission at all. Same shape for Chromium: --profile-directory= takes the directory name and Chrome resolves it. An unknown Firefox name returns NS_ERROR_SHOW_PROFILE_MANAGER, so the worst case is a visible dialog rather than a silent misroute.
  3. Accept a profile directory path and pass it through, which needs no access to anything.
  4. Surface it in the UI — the profile dropdown could say "macOS is blocking access to this browser's profiles" instead of being empty.

Fixes 2 and 3 do not cover profiles created with Firefox's newer profile manager: their name→directory mapping exists only in the profile group store, which is inside the blocked directory, and Firefox has no command-line flag that selects one by name. Those need either the permission or an explicit path.

The proper "ask the user" route is an NSOpenPanel grant — a non-sandboxed app that has the user pick a folder gets com.apple.macl on it, persistable via a security-scoped bookmark. That avoids Full Disk Access entirely, but it's Swift work in the app shell and I haven't prototyped whether a folder selection grants recursive access. Worth noting Full Disk Access itself has no request API, so nothing can prompt for it — and because the grant is keyed to the code signature, it has to be re-added after each rebuild if you build Finicky yourself.

I have 1, 2 and 3 implemented for the Firefox path already, on top of #546. Glad to split them into a standalone PR against main instead if that's easier to review — they're independent of that PR's profile-group work.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions