Documentation

Junction is a rule-based link router for macOS. It sits as your default browser and sends every link you click to the right browser, browser profile, or native app — by rules you write once.

macOS allows exactly one default browser. If you juggle work and personal Chrome profiles, want Zoom links in the Zoom app instead of a tab, or want links from Terminal to open somewhere other than links from Mail, you need a router in between. Junction takes the default-browser slot: it receives the link, checks it against your ordered rule list, and hands it off to wherever it belongs. First match wins; anything unmatched opens in your fallback browser.

It's a menu-bar app — no windows, no Dock icon. Open source (MIT), with a GUI and a human-readable config file you can check into your dotfiles, plus a CLI. Zero telemetry.

NoteA link is never lost. Every failure path — a missing browser, an app that isn't installed, a deleted Firefox profile, an unknown rewriter, an invalid rule — degrades to your fallback browser rather than dropping the link or opening the wrong thing.

Installation

Requirements: macOS 13 Ventura or later. Builds are universal — Apple silicon and Intel.

Heads upJunction is not notarized by Apple yet, so macOS quarantines it and refuses the first launch. Whichever way you install, clear the quarantine flag afterward (or right-click Junction.app → Open → Open).

Homebrew (recommended)

The repo is its own tap, so point Homebrew straight at it. The cask lives outside Homebrew's official taps, so trust it before installing:

Terminal
brew tap jnahian/junction https://github.com/jnahian/junctionbrew trust --cask jnahian/junction/junctionbrew install --cask junctionxattr -dr com.apple.quarantine /Applications/Junction.app

Homebrew also symlinks the bundled junction CLI onto your PATH.

Manual DMG

Download Junction.dmg from the latest release, open it, and drag Junction into Applications. Then run the xattr command above. With a manual install, link the CLI yourself if you want it:

Terminal
ln -sf "/Applications/Junction.app/Contents/Helpers/junction" /usr/local/bin/junction

Uninstall

Terminal
brew uninstall --zap --cask junction     # Homebrew: also removes config + prefsrm -rf /Applications/Junction.app        # manual install

Your rules live at ~/.config/junction/config.json — delete that too on a manual uninstall if you want a clean slate.

First launch

Junction opens a short setup the first time you run it.

1 · Pick a fallback browser

Where anything your rules don't match will open. This is the safety net that makes routing non-destructive — you can add rules gradually and nothing breaks in the meantime.

2 · Make Junction your default browser

The one step it can't work without: being the default browser is how Junction sees every clicked link. Setup offers to do it, or set it yourself under System Settings → Desktop & Dock → Default web browser. Junction hands each link off to a real browser afterward — you never browse "inside" it.

3 · Enable a few starter rules

Setup only offers rules for apps and browsers you actually have installed. After that Junction lives in the menu bar; click the icon for recent links, settings, and updates.

TipHold ⌥ Option while clicking any link to force the picker, whatever your rules say.

The config file

Rules live in ~/.config/junction/config.json (honors $XDG_CONFIG_HOME). Edit them in the GUI or in your editor — it's plain text you own, so commit it to your dotfiles, sync it, or share it with your team.

~/.config/junction/config.json
{
  "version": 1,
  "fallback": { "app": "com.apple.Safari" },
  "stripTrackingParams": true,
  "rules": [
    {
      "name": "Work links → Chrome work profile",
      "match": { "patterns": ["*.atlassian.net/*", "app.clickup.com/*"] },
      "action": { "app": "com.google.Chrome", "profile": "Profile 1" }
    },
    {
      "name": "Zoom → native app",
      "match": { "patterns": ["*.zoom.us/j/*", "*.zoom.us/w/*"] },
      "action": { "deepLink": "zoom" }
    },
    {
      "name": "Links from Terminal → Chrome",
      "match": { "sourceApps": ["com.googlecode.iterm2", "com.apple.Terminal"] },
      "action": { "app": "com.google.Chrome" }
    }
  ]
}

Top-level keys:

  • rules — the ordered rule list, checked top to bottom.
  • fallback — where unmatched links go, named by bundle ID.
  • stripTrackingParams — drop tracking parameters before routing.
  • enabledRewriters, customRewriters, slackTeams — see Deep links.
NoteThe file is watched and hot-reloads with validation. An invalid edit keeps the last-good rules live and shows a menu-bar warning rather than breaking routing. Comments and trailing commas are accepted on read; the GUI writes strict, stably-sorted JSON so git diffs stay clean.

Rule syntax

A rule is a name, a match, and an action:

one rule
{
  "name": "Personal → Firefox profile",
  "match": { "patterns": ["reddit.com/*", "news.ycombinator.com/*"] },
  "action": { "app": "org.mozilla.firefox", "profile": "personal" }
}

Match patterns

Patterns are matched against the link's host/path. The scheme is ignored unless you write it, the host is case-insensitive, the path is case-sensitive, and the query string is ignored — use regex when the query matters.

PatternMatches
*.atlassian.net/*Any subdomain of atlassian.net — and the apex domain itself — at any path depth
app.clickup.com/*That exact host, any path below it
reddit.comA bare host: matches all of its subpaths
*.zoom.us/j/** stays inside one path segment; ** or a trailing * crosses segments
*Everything — but you don't need it; unmatched links already go to your fallback

Match on the source app

A rule can also match where the click came from — the bundle ID of the app that opened the link. Combine it with patterns to narrow a rule to one app's links:

source-app rule
{
  "name": "Meeting links from Calendar → picker",
  "match": {
    "patterns": ["meet.google.com/*"],
    "sourceApps": ["com.apple.iCal"]
  },
  "action": { "prompt": true }
}

Precedence

Order is the logic. Junction walks the list from the top and stops at the first rule that matches, so put specific patterns above broad ones. If two rules could match the same link, the one higher in the file wins.

Fallback

If no rule matches, the link opens in your fallback browser. So does any rule that can't be carried out — a browser that isn't installed, a profile that was deleted. Junction never drops a click.

TipNot sure which rule wins? junction test <url> prints the matching rule and the decision without opening anything. See Command line.

Actions

The action decides what happens to a matched link:

ActionWhat it does
{ "app": "com.google.Chrome" }Open in a browser or app, named by its bundle ID.
{ "app": "com.google.Chrome", "profile": "Profile 1" }Open in a specific browser profile — a Chromium profile directory (Default, Profile 1) or a Firefox profile name (what firefox -P takes, listed in Settings → Browsers).
{ "deepLink": "zoom" }Rewrite the link to a native app deep link. Works whether or not the rewriter is enabled in Settings → Deep Links.
{ "prompt": true }Show the picker and let you choose at click time.
{ "clipboard": true }Copy the link instead of opening it.
NoteA profile is a Chromium profile directory or a Firefox profile name. If a rule names a profile that no longer exists, the link degrades to your fallback browser rather than opening in the wrong one.

Not supported:

  • Firefox containers are an extension feature with no launch-flag equivalent — Firefox profiles are supported.
  • Arc spaces have no public API.

Command line

The app bundle ships a companion CLI at Contents/Helpers/junction (Homebrew links it onto your PATH automatically).

Terminal
junction test "https://mycorp.atlassian.net/browse/X-1" # which rule wins, and why
junction open "https://example.com"             # route a link now
junction config path                            # print the config path
junction config validate                        # check the file before saving

junction test is the fastest way to reason about precedence — it prints which rule matched and what Junction would do, without opening anything. Add --source <bundle-id> to test a source-app rule:

Terminal
junction test "https://mycorp.atlassian.net/browse/X-1" --source com.tinyspeck.slackmacgap

junction config validate runs the same validation the GUI and the file watcher use, so a config that passes here is a config the app will load.

Troubleshooting

macOS refuses to open the app

Junction isn't notarized yet, so macOS quarantines it. Clear the flag with xattr -dr com.apple.quarantine /Applications/Junction.app, or right-click Junction.app → Open → Open.

Nothing routes at all

Junction only sees links when it is the default browser — that's the one step it can't work without. Re-select it under System Settings → Desktop & Dock → Default web browser.

Links open in the wrong place

Almost always precedence: a broader rule above is catching the link first. Run junction test <url> — it prints the matching rule and the decision without opening anything.

A rule opens the fallback instead of its app

Junction degrades to the fallback rather than opening the wrong thing — a deleted browser profile, an app that isn't installed, or an unknown rewriter all land there. The menu-bar icon flags an invalid config, and the last-good rules stay live until you fix it.

Slack deep links open the browser

Slack permalinks don't carry the team ID the app needs. Map each workspace subdomain to its team ID in Settings → Deep Links (or slackTeams in the config); unmapped workspaces open in the browser rather than on the wrong screen.

Privacy

Zero telemetry, zero analytics. Junction makes exactly one kind of network request: the Sparkle update check against this repository's release feed. The links you route are never sent anywhere and never logged to disk — the recent-links list is in memory, capped at 10, and cleared on quit.

Being the default browser is a high-trust position. That's the argument for open source, for CI that builds and tests every commit, and for updates that only install if they carry a valid EdDSA signature from the maintainer's key.

StatusMVP under active development. Planned for v1.1+: redirect and shortener unwrapping (opt-in), mailto: routing, time- and network-based conditions, and Shortcuts actions. Track it on GitHub, or read the changelog.