splitroute is a lightweight split-routing tool for macOS.
You keep normal internet traffic on one interface (for example Ethernet), and route selected services (for example OpenAI/ChatGPT/Codex) through another interface (for example iPhone hotspot).
This project is intentionally simple: Bash scripts plus service config files. No VPN, no heavy daemon.
docs/case-study-openai.md- real troubleshooting history (symptoms -> root cause -> fix)docs/faq.md- practical Q&Adocs/migration.md- migration from older home-directory scriptsdocs/roadmap-watchlist.md- future ideasCONTRIBUTING.md- contribution guideSECURITY.md- security policyDISCLAIMER.md- policy and liability notes
Typical setup:
en7Ethernet is your default routeen0hotspot is used only for selected services
splitroute does two things:
- Per-host routes (route by IP)
- Resolve hostnames from
services/<service>/hosts.txt - Add host routes so those IPs use the hotspot gateway
- Per-domain DNS override (optional but default ON)
- Write
/etc/resolver/<domain>files for domains inservices/<service>/dns_domains.txt - Helps bypass DNS-based blocking (for example
146.112.61.xblocked-page answers)
This is not a VPN. It does not tunnel all traffic.
- No MITM/proxy logic
- No certificate installation
- No TLS verification disable
- HTTPS remains end-to-end encrypted
The tool changes:
- host routes in the routing table
- managed resolver files in
/etc/resolver
- macOS
- admin permissions (
sudo) forrouteand/etc/resolver - standard tools:
route,netstat,ifconfig,ipconfig,dscacheutil,curl - optional tools:
dig,lsof
bin/splitroute- CLI entrypointscripts/splitroute_on.sh- enable routing for a servicescripts/splitroute_off.sh- disable routing and clean statescripts/splitroute_check.sh- diagnosticsservices/<service>/hosts.txt- hosts (one per line)services/<service>/dns_domains.txt- resolver domains (base domains only)services/_template/- service template
Default service: openai.
-
Connect both links:
- Ethernet (default route)
- hotspot on Wi-Fi
-
Enable split-routing:
cd /path/to/splitroute
./bin/splitroute on openai- Verify routing:
./bin/splitroute check openai -- --host chatgpt.com --control- Refresh if needed:
./bin/splitroute refresh openai- Turn it off:
./bin/splitroute off openaiDefaults:
WIFI_IF=en0ETH_IF=en7
Override example:
WIFI_IF=en0 ETH_IF=en7 ./bin/splitroute check openai -- --controlEdit:
services/openai/hosts.txt
Modes:
DNS_OVERRIDE=on- always use per-domain resolversDNS_OVERRIDE=auto- enable only when blocked DNS is detectedDNS_OVERRIDE=off- never write/etc/resolver
Default:
DNS_OVERRIDE=on
Examples:
DNS_OVERRIDE=on DNS_SERVERS="1.1.1.1 8.8.8.8" ./bin/splitroute on openaiDomain source:
services/openai/dns_domains.txt
Routing check:
./bin/splitroute check openai -- --no-curlControl host check:
./bin/splitroute control openai -- --control-host youtube.com --no-curlPID-based check:
pgrep -n codex
./bin/splitroute check openai -- --pid <PID> --no-curl- Host routes are runtime-only and disappear after reboot.
- Resolver files in
/etc/resolverare on-disk and can persist until cleaned. ./bin/splitroute off <service>removes both routes and managed resolver files.splitroute_off.shhas fallback cleanup even when/tmpstate files are gone.
Menu bar app behavior:
- Auto-OFF is intentionally removed.
- ON/OFF are manual only.
- At app launch, stale splitroute state from a previous session is detected and reset (requires admin auth).
- Service host files are never auto-deleted. They stay in
services/<service>/until you remove them manually.
To guarantee reset after reboot/login, add the app to macOS Login Items so startup cleanup runs automatically.
- Routing is by IP, not by domain.
- CDN IPs can rotate; use
refreshwhen needed. - IPv6 may be unavailable on hotspot.
- If hotspot disconnects while ON, service traffic can fail until
offorrefresh.
- Better service editor UX
- Optional blocked-service watchlist
- Optional helper/daemon model for stricter lifecycle cleanup
Build and run:
bash scripts/build_menubar_app.sh
open build/SplitrouteMenuBar.appVersioning:
CFBundleShortVersionString= release version (fromInfo.plistorAPP_VERSIONenv).CFBundleVersion= numeric build number, auto-generated per build (timestamp), can be overridden byAPP_BUILD.- If HEAD is exactly on tag
vX.YorvX.Y.Z, build uses that tag asCFBundleShortVersionString(unlessAPP_VERSIONis provided).
Example:
APP_VERSION=0.2.1 APP_BUILD=20260303104500 bash scripts/build_menubar_app.shWorkflow (official install target: /Applications only):
sudo bash scripts/workflow_menubar_app.shThe workflow:
- builds the app,
- installs to
/Applications/SplitrouteMenuBar.app, - verifies installed app matches local build (
scripts/verify_menubar_install.sh), - launches
/Applications/SplitrouteMenuBar.app, - packages DMG/ZIP.
Package:
bash scripts/package_menubar_app.sh
open build/SplitrouteMenuBar.dmgIf DMG creation fails, the script creates build/SplitrouteMenuBar.zip.
- Build/install/package:
sudo APP_VERSION=0.2.1 bash scripts/workflow_menubar_app.sh- Commit and tag:
git commit -am "release: v0.x"
git tag -a v0.x -m "v0.x"- Push:
git push origin master --tags- Create release:
gh release create v0.x build/SplitrouteMenuBar.dmg -t "v0.x" -n "Release v0.x"If DMG is missing:
gh release create v0.x build/SplitrouteMenuBar.zip -t "v0.x" -n "Release v0.x"- If repo auto-detection fails, use
Settings -> Set Repo Path.... Servicessupports multi-select.Check connectionsverifies route behavior without changing config.Add Service...can create a basic service or run Smart Host Discovery (explicit opt-in).Touch ID (sudo)requirespam_tid.soin/etc/pam.d/sudo.- Developer signing/notarization is intentionally separate from this local build workflow.