Route every link to the right browser, profile, and workspace.
English | 简体中文
LinkPilot is a macOS-first (Windows / Linux to follow) link router: it sits between the OS, your browsers, and the apps that open URLs, and dispatches each link to the browser + profile that matches your rules.
v0.1 — feature-complete. Workspace, Tauri shell with menu-bar tray,
fsnotify-backed config store, route history, all five GUI pages, and an
end-to-end macOS lpt open flow. Real brand artwork shipped (full icon
matrix from docs/brand/icon.png + a single-color menu-bar template from
docs/brand/tray-template.svg); structured rule editor replaces the
JSON-textarea fallback (still available under “Advanced: raw JSON”).
Set as Default Browser (LaunchServices), Launch at Login (LaunchAgent
plist), and the daemon's Unix-socket IPC server are all wired.
See docs/linkpilot-design-v0.1.md (PRD) for the design.
Two install paths from the same release. Pick whichever (or both).
Headless — useful for terminal workflows, scripts, or alongside the
GUI from a different release. The lpt binary is a single static-ish
executable; no daemon required (lpt open does local routing when no
daemon is running, and talks to the GUI's daemon over Unix socket
when both are installed).
# From a release artifact:
curl -L https://github.com/jackerjay/LinkPilot/releases/latest/download/lpt-macos.tar.gz \
| tar -xz -C ~/.local/bin
chmod +x ~/.local/bin/lpt
# Add ~/.local/bin to PATH if it isn't already.Install the .app and the bundled lpt binary comes along. After
launching LinkPilot, open Settings → Command-line tool and click
Install to ~/.local/bin to symlink lpt onto your PATH (idempotent;
re-run after a version upgrade). The bundled binary lives at
/Applications/LinkPilot.app/Contents/MacOS/lp — you can also add
that directory to PATH directly instead of symlinking.
curl -L https://github.com/jackerjay/LinkPilot/releases/latest/download/LinkPilot_<version>_universal.dmg -o LinkPilot.dmg
hdiutil attach LinkPilot.dmg
cp -R "/Volumes/LinkPilot/LinkPilot.app" /Applications/
hdiutil detach "/Volumes/LinkPilot"
xattr -dr com.apple.quarantine /Applications/LinkPilot.app # unsigned build
open /Applications/LinkPilot.appcargo build -p linkpilot-cli
./target/debug/lp doctor # writes default config + lists browsers
./target/debug/lp rules list
./target/debug/lp open https://github.com/anthropics/anthropic-cookbook
./target/debug/lp open https://figma.com --dry-run
./target/debug/lp open https://github.com --from-app Slacklpt talks to the running daemon over a Unix socket
(~/Library/Application Support/LinkPilot/linkpilot.sock) when one is up,
and falls back to local execution otherwise. Force the local path with
--local. Writes always go through the local file; the daemon's fsnotify
watcher picks them up via the anti-echo token, so a running GUI refreshes
within a frame.
First run writes a starter config to
~/Library/Application Support/LinkPilot/linkpilot.config.json (PRD §22 demo:
github / notion → Chrome Default, figma / youtube → Arc). Edit and re-run.
The CLI mirrors everything the GUI can configure — see lpt <command> --help
for the full surface:
# Rules
lpt rules add --host "*.figma.com" --target arc --priority 20
lpt rules add --host github.com --path "/oauth/*" --keep-source --priority 50
lpt rules add --from-app Slack --ask
lpt rules list --all # include disabled rules
lpt rules disable <id-prefix> # 8-char prefix is enough
lpt rules set-priority <id-prefix> 99
lpt rules delete <id-prefix>
lpt rules add --when-json '{"op":"any","of":[...]}' --then-json '{"kind":"block"}'
# Workspaces (batch on/off groups of rules)
lpt workspaces add work --name Work
lpt workspaces disable work # all `workspace_id=work` rules skipped
# Config inspection + import/export
lpt config show # whole document as JSON
lpt config path
lpt config set-default-target arc --profile Personal
lpt config export ./backup.json
lpt config import ./backup.json
# Settings
lpt settings show
lpt settings smart-routing off # master kill-switch
lpt settings launch-at-login on
lpt settings history-retention 30 # or `clear` for unlimited
# Browsers
lpt browsers list # auto-detected + custom, merged
lpt browsers profiles chrome
lpt browsers custom add --id devbuild --name "Chrome Canary" \
--kind chromium --exec /Applications/Google\ Chrome\ Canary.app
# Default-browser registration
lpt default-browser status
lpt default-browser set # triggers the macOS confirm prompt# from repo root
cd apps/desktop
npm install # installs @tauri-apps/cli locally
npx tauri dev # starts Vite + Tauri togetherNote: use
npx tauri …(ornpm run tauri -- …). The Tauri CLI is a npm devDependency —cargo tauri …only works if you alsocargo install tauri-cli, which this repo does not require.
What to try:
- Menu bar icon stays after closing the window — daemon keeps running.
- Open https://github.com via Slack / Terminal → LinkPilot should be in the app picker (after you've set it as default in System Settings).
- Drop a fresh config in the JSON editor → atomic rewrite + the file is
reloaded if you also
vimit externally (anti-echo token prevents loops). - Inspector tab shows every decision as it happens via the
route-loggedTauri event.
cd apps/desktop
npm run bundle:mac # tauri build + patch-info-plist.sh on the .app
open ../../target/release/bundle/macos/LinkPilot.appbundle:mac runs tauri build and then patches the bundled .app's
Info.plist so the macOS "Default web browser" picker recognises LinkPilot
(see apps/desktop/scripts/patch-info-plist.sh for details).
The DMG it produces alongside is not patched — for a DMG with the
plist patch baked in, push a v*.*.* tag and use the
release.yml-produced artifact instead (see "Releases" below).
Real brand artwork is already shipped in apps/desktop/src-tauri/icons/
(generated from docs/brand/icon.png + docs/brand/tray-template.svg).
To re-generate after editing source art, see
apps/desktop/src-tauri/icons/README.md.
crates/
core/ # routing engine, rule model, config store, fsnotify,
# route history, platform traits, IPC protocol types
platform-mac/ # macOS backend (real in v0.1)
platform-win/ # Windows stub (real in v0.5)
platform-linux/ # Linux stub (real in v0.6+)
ipc/ # length-prefixed JSON over Unix socket / Named pipe
# (transport ready; server lands in a later slice)
native-host/ # NMH stdio bridge (v0.3)
cli/ # `lpt` command-line client
headless-daemon/ # reserved for a future GUI-less daemon binary
apps/
desktop/ # Tauri 2 app
src-tauri/ # Rust: tray, deep-link, commands, fsnotify wiring
src/ # React + TypeScript frontend
pages/ # menu-bar, rules, inspector, browsers, settings
lib/ # typed Tauri command wrappers
extension/ # MV3 browser extension (v0.3)
packages/
config-dsl/ # @linkpilot/config TS DSL (v0.2+)
cargo check --workspace --exclude linkpilot-desktop # core stack on any OS
cargo test -p linkpilot-core # routing + history + config
cargo check -p linkpilot-desktop # Tauri app on macOS (or
# Linux with GTK/WebKit
# dev libs installed)Cross-checking macOS-only Rust from Linux:
rustup target add x86_64-apple-darwin
cargo check --target x86_64-apple-darwin -p linkpilot-platform-mac -p linkpilot-cli(The Tauri shell can't be cross-checked from Linux because it links real Cocoa frameworks via cc; build that one on a Mac.)
Frontend:
cd apps/desktop
npm install
npm run build # tsc --noEmit + vite build → apps/desktop/dist/LinkPilot is open source under MIT OR Apache-2.0. See CONTRIBUTING.md for
local setup, PR expectations, and the release process. Please report security
issues privately; see SECURITY.md.
Maintainers publish releases by pushing a semver tag:
git tag v0.1.0
git push origin v0.1.0The tag triggers .github/workflows/release.yml, which on macos-latest:
- Builds the
lptCLI for bothx86_64-apple-darwinandaarch64-apple-darwinandlipos them into one universal binary. - Builds the Tauri shell with
--target universal-apple-darwin --bundles appso the.appis also a universal binary. - Runs
apps/desktop/scripts/patch-info-plist.shagainst the bundled.app— rewrites thetauri-plugin-deep-linkauto-injectedCFBundleURLTypesentry to Viewer/Defaultand addsCFBundleDocumentTypesfor HTML so the macOS "Default web browser" picker actually surfaces LinkPilot. - Wraps the patched
.appin a vanillaLinkPilot_<version>_universal.dmgviahdiutil(a secondtauri build --bundles dmgwould re-bundle the.appand overwrite the plist patch). - Uploads
lpt-macos,lpt-macos.tar.gz, the DMG, andchecksums.txtto a GitHub Release.
A single universal DMG works on both Apple Silicon and Intel Macs.
Every PR also runs a desktop-bundle smoke job on macos-latest
(tauri build --debug --bundles app) so packaging regressions show up
before a tag is cut.
Current release artifacts are unsigned. On macOS, unsigned builds may require removing quarantine before first launch:
xattr -dr com.apple.quarantine LinkPilot.app