Click a link anywhere on your Mac. If that URL is already open in a Chrome tab, that tab gets focused. If it isn't, it opens in a new tab. No duplicate tabs.
Chrome has no built-in equivalent, and no browser chooser does it either — Finicky, Velja and Choosy all pick a browser, then leave tab handling to it. This wires tab reuse in as the system default handler, so it applies to every link from every app.
Linear links are the one exception: they open in the Linear desktop app.
This sees every URL you open. It has to — it becomes your default browser handler. Everything happens locally: nothing is sent anywhere, nothing is logged, and there is no network code in this repo. The one exception is opt-in and noted under Editing the config — Finicky's
logRequestswrites every URL you visit to a local file.
- macOS with Chrome and Finicky 4.x — a v3 config has different syntax and
fails silently, so
install.shrefuses to proceed on one - Homebrew, only if Finicky isn't already installed
- Developed on macOS 26.5; no lower bound has been tested
./install.shThat checks your Chrome and Finicky versions, installs Finicky if it's missing, backs up any existing Finicky config, builds and registers the applet, and prints the two steps it can't do for you: choosing Finicky as the default browser, and approving Chrome automation on first click.
Then:
./verify.shEvery failure mode here is silent, so verify.sh exists to turn "links just
don't do anything" into a specific answer. To back out, ./uninstall.sh — read
its first message, the default-browser step has to come first.
Two pieces.
Finicky is set as the default browser, so it sees every URL the system
opens and decides where it goes. Its defaultBrowser is ChromeTabFocus, so
everything lands there unless a handler claims it first.
ChromeTabFocus is an AppleScript applet registered as an http/https handler. Given a URL it scans every Chrome window for a tab already showing it (ignoring trailing slashes), focuses that tab and window if found, and otherwise opens a new tab in the front window — or a new window, if Chrome has none open.
A click therefore crosses two processes before Chrome sees it. Naively that cost ~0.6s with a visible focus flash. Three details bring it to ~0.27s:
- The applet is built stay-open (
osacompile -s), so it stays warm instead of cold-launching per click. LSUIElementis set, so it has no Dock presence and focus never visibly leaves the app you clicked from.- Tab URLs are read one Apple event per window, not one per tab — per-tab round trips dominate once you have a lot of tabs.
Finicky's keepRunning does the same job on its side.
Both figures are wall-clock from open <url> until Chrome's active tab reports
the target URL, on one machine with ~6 tabs open. Treat them as "the difference
was obvious", not as a benchmark.
- Minimized windows don't work. If the matching tab is in a minimized window, the click appears to do nothing at all — no new tab, no visible change. The applet selects the right tab but never deminiaturizes. Confirmed, and deliberately not fixed.
- Incognito windows participate. A URL open in an incognito window will raise that window. That's intended here, but it's worth knowing before you click a link from Mail in front of someone.
- Chrome only. Safari's scripting dictionary differs, and Firefox exposes no tab-URL scripting at all, so it could never participate.
- Links clicked inside an app that opens its own webview never reach Finicky, so they can't be deduplicated.
- URLs must match exactly (modulo trailing slash). Query strings, tracking params and anchors all count as different tabs.
- The applet accepts a URL from any app, not just Finicky. Nothing is passed to a shell, so there's no injection surface; an unusual scheme just lands in a new blank tab.
~/.config/finicky/finicky.js. Finicky does not hot-reload it — an edit
does nothing until Finicky restarts, and every URL quietly falls through to the
default browser in the meantime.
pkill -x FinickyThis config sets hideIcon, so there's no Finicky menu bar item to reach for —
pkill is the control.
Stop it and stop there — do not launch it again. Finicky shows its window
on every explicit launch, and open -g -j -a Finicky is no better than
open -a Finicky; the background and hidden flags make no difference. That
window is the flash people mistake for a slow route. Launched by LaunchServices
to handle a URL, it never shows the window at all, so the next link you click
brings it back silently.
Editing chrome-tab-focus.applescript is different: re-run
./install-chrome-tab-focus.sh, or the old compiled applet keeps handling URLs
and your change appears to do nothing.
When a rule misbehaves, set options.logRequests: true, restart, and read
~/Library/Logs/Finicky/*.log — it records the exact open command issued,
which settles what a rule actually did. Turn it back off afterwards; that log
contains every URL you visit.
Handlers are tried in order; the first match wins, and anything unmatched falls
through to defaultBrowser. A handler is just match + browser:
{
match: ({ url }) => url.host === "figma.com",
browser: { name: "Figma", appType: "appName" },
}Note the { url } destructure — see the trap below before you copy this
somewhere else.
Both fail silently rather than erroring:
- A handler has no
urlfield — onlymatchandbrowser. URL rewriting belongs in the top-levelrewritearray, which runs first; handlers then match the already-rewritten URL. Confirmed in Finicky's ownfinicky.d.ts, whereBrowserHandleris{ match, browser }. - The bundled
finicky.d.tsdisagrees with the runtime aboutmatch. It declares(args_0: URL, ...), implying a bare WHATWGURL, but Finicky 4.2.2 actually passes a{ url }wrapper — which is why the config here destructures. Verified by routing rather than by reading:/issue/...reaches Linear while/pricingfalls through to Chrome, which can only happen if the destructure works. If a future version drops the wrapper, every Linear link will start opening in Chrome; switch to a bareurlparameter then.
Linear.app ships no com.apple.developer.associated-domains entitlement, so a
universal link can never reach the app directly — only the linear:// scheme
can. In practice handing Linear the plain https URL and letting it route
internally works better than rewriting the scheme, so this config doesn't
rewrite anything.
Linear matches its in-app tabs on the exact URL, so a slug-less issue link
(/issue/ES-583) opens a second tab alongside the canonical one
(/issue/ES-583/some-title-slug). Use the full URL from
linear issue view <id> --json when pasting links. Canonicalising slug-less
URLs automatically was considered and rejected: the CLI lookup costs ~0.9s per
click, and links clicked inside Linear never pass through Finicky anyway.
Don't use Linear? Delete the handler between the ---8<--- markers in
finicky.js and every URL goes to Chrome.
Kept so they don't get re-attempted:
- Routing GitHub to the GitHub PWA's shim app drops the deep link. Its
app_mode_loaderaccepts no URL argument and always lands on the app's start URL. - Launching Chrome with
--app=<url>opens the right URL, but in a fresh window every time, with no tab reuse. - GitHub Desktop registers only clone/auth schemes (
x-github-client://,github-mac://) and cannot render issues, PRs, or code.
| File | Purpose |
|---|---|
install.sh / uninstall.sh |
Everything, both directions |
verify.sh |
Diagnose a silent failure |
finicky.js |
Copied to ~/.config/finicky/finicky.js |
chrome-tab-focus.applescript |
Source for the applet |
install-chrome-tab-focus.sh |
Builds and registers the applet only; also the edit-test loop |
The applet is built rather than committed: osacompile produces the .app in
~/Applications, then the installer patches its bundle id, URL types,
LSUIElement and NSAppleEventsUsageDescription into Info.plist, ad-hoc
signs it, and registers it with LaunchServices.
Ad-hoc signing has one sharp edge worth knowing: the code identity changes on every rebuild, so macOS may re-prompt for Chrome automation after you re-run the installer — or, with a stale grant on record, silently deny it. If links stop working right after a rebuild:
tccutil reset AppleEvents io.github.gamellis.chrometabfocusMIT — see LICENSE.