See what your app actually talks to.
A terminal-first HTTP(S) debugging proxy for mobile and web, with a real-time browser dashboard alongside it. Point a client at Detour, and every request it makes — encrypted or not — shows up live, inspectable, and editable.
Diagram: a client sends a CONNECT request to detour start, which forwards it on to the real server; in parallel, a live copy of that traffic streams to the dashboard at localhost:9080.
Why Detour
Detour treats the terminal and the browser as equally first-class:
detour start from a terminal — CI job,
SSH session, whatever — and get a live traffic view, rule engine, and breakpoint editor that a GUI
tool would give you — including a man-in-the-middle (MITM) view into HTTPS, not just plaintext HTTP —
in a browser tab it opens for you.
Streams, doesn't buffer
Bodies are throttled and forwarded chunk by chunk as they arrive, so a slow-connection simulation behaves like a slow connection, not like a fast one that pauses. Large downloads don't sit in memory waiting to be complete.
Declarative rules
Mock, route, rewrite, breakpoint, or run a JS hook — all from one rules.json, hot-reloaded the moment you save it.
Live dashboard
Every exchange streams in over WebSocket as it happens. Filter, inspect, diff two requests, replay one, or pause it mid-flight.
Setup that's actually automated
detour setup trusts the CA cert and points the system proxy at itself for mac/Android/iOS Simulator/Linux — no more hand-copying certificates.
Quickstart
Requires Node 18 or newer. Detour itself runs on macOS, Linux, and Windows.
# install (Homebrew tap, self-contained build) brew tap rwadada/detour brew install detour # run it detour start
The dashboard opens automatically at http://localhost:9080. Point any HTTP(S) client at localhost:8080 and watch its traffic show up live.
Getting started
Two ways to install, one certificate to trust, then point a client at the proxy port.
Install
Homebrew (recommended)
brew tap rwadada/detour brew install detour detour start
Ships a self-contained release build — no Node toolchain needed beyond Node itself, pulled in automatically as the formula's dependency.
From source
npm install npm run build npm start -- start
Trust the CA certificate
On first run, a local CA root is generated at ~/.detour/certs/certs/ca.pem.
Decrypting HTTPS means installing it as a trusted root on whatever you're pointing at Detour —
detour cert export [path] writes it out for scripting the install.
On macOS, Android, Linux, or the iOS Simulator, don't do this by hand —
detour setup --target mac trusts the certificate and points your system proxy
at Detour in one command. See Setup & platforms. The manual steps below are
for the platforms that can't be automated, and for when you want to know what setup
is doing.
Keychain Access → expand Trust → Always Trust. Adding it to Keychain isn't enough on its own — without the Always Trust step, macOS keeps it untrusted and HTTPS will fail.
certmgr.msc → Trusted Root Certification Authorities → Import.
Install the profile, then flip it on separately under Certificate Trust Settings. Missing that second step is the #1 reason nothing shows up.
Settings → Install a certificate. Since Android 7 (API 24), apps don't trust user-added CAs by default unless they opt in via a network_security_config — some apps still won't show decrypted traffic even once the cert is installed. A rooted device installing the cert into the system store instead is the more reliable path for those.
Usually the easiest — no pinning restrictions to fight.
A rooted device installing the cert into the system store — rather than the user store — is the more reliable path for apps with their own certificate pinning.
Start flags worth knowing
| Flag | What it does |
|---|---|
| --port <n> | Proxy port (default 8080) |
| --dashboard-port <n> | Dashboard port (default --port + 1000) |
| --rules <path> | Load a rules file; hot-reloads on save. Omit it and a passthrough.rule.json in the current directory — just a plain rules file under a conventional name — is loaded automatically, if one exists |
| --dump <level> | How much each exchange is logged. summary (default) is one line; full adds headers and body to the console; file writes that same detail to ~/.detour/dumps instead. full and file redact sensitive headers |
| --no-http2 | Falls back to HTTP/1.1-only on MITM'd HTTPS |
| --proto <path> | .proto schema for pretty-printed gRPC bodies in the CLI's full/file dumps only — the dashboard's body viewer always shows gRPC as raw bytes |
| --no-open | Don't auto-open the dashboard in a browser on startup |
Once running, point a client at your chosen port — curl -x http://localhost:8080 https://example.com, or your device's Wi-Fi proxy settings — and traffic starts logging live.
Web dashboard
Served at localhost:9080 by default, opened automatically the
moment detour start is ready.
Live log
Every request/response streams into a virtualized table over WebSocket the instant it passes through — smooth at thousands of rows, with the last 500 exchanges replayed on connect so a page refresh doesn't lose recent history. Filter by method, status class, or a URL substring; click a row to inspect headers, query params, and pretty-printed body in a resizable side panel.
The body shown in the inspector is capped at
256 KB per exchange — past that, the display is marked truncated. That cap is display-only: the full
body is still streamed to the client/server untouched, and a script rule's
beforeRequest/beforeResponse hooks always see the
complete, uncapped body regardless of what the dashboard shows.
| Time | Method | Status | URL | Size |
|---|---|---|---|---|
| 14:02:18.114 | GET | 200 | api.example.com/users/42 | 1.2 KB |
| 14:02:18.301 | POST | 302 | api.example.com/session | 0 B |
| 14:02:19.008 | GET | 404 | cdn.example.com/missing.png | 612 B |
| 14:02:19.442 | GET | 503 | api.example.com/report | 84 B |
Traffic controls
Four controls in the header. Each one changes the proxy itself, not just your view — so it takes effect immediately, for every client running through Detour, and every open dashboard tab stays in sync.
Intercept On/Off
The master switch for decryption. Turn it off and HTTPS passes through untouched — Detour can't read it, and no mock, rewrite, or breakpoint rule applies. route rules still redirect, since redirecting doesn't require decrypting.
Focus
Narrows interception to a host allowlist (*.example.com, localhost:3000) instead of an all-or-nothing switch.
Throttle
Bandwidth cap, latency, packet loss — "Fast 3G"/"Slow 3G" presets or exact numbers, for testing a client on a bad connection.
Block Hosts
Outright denies matching hosts — 403 or a connection reset — checked before every other feature.
Everything else the dashboard does
- Breakpoints — a paused exchange shows a live indicator; edit method/headers/body (or status/headers/body for a response) before resuming or aborting it.
- Compare — Ctrl/Cmd-click two rows to diff their headers and bodies side by side.
- Copy as cURL / Replay — reproduce a request in a terminal, or fire it again through the proxy as-is.
- Create Rule — generate a mock/route/rewrite/breakpoint rule straight from a captured exchange, opened in the editor for review before saving.
- Rule Profiles — save the active
rules.jsonas a named, switchable ruleset (flip betweenstagingandproductionwithout hand-editing). - Export & import — Export the log as HAR 1.2 (for other tools) or Detour's own JSON, honoring whatever filter is currently applied. "Save session" goes further: it captures the traffic and the proxy's live state — Intercept, Focus, Throttle, Block Hosts — so loading it later restores the conditions the capture happened under, not just the rows. Any HAR or JSON file can also be imported and browsed with no proxy running.
- Group by host — collapse the log into per-host sections, with Expand all / Collapse all buttons for moving between "which hosts is this app even talking to" and the detail of one of them.
- Tail — pauses auto-scroll, so incoming traffic doesn't yank you off the row you're reading.
- Settings panel — the sidebar's gear icon opens a form for everything
detour configdoes from the terminal (LAN access, dashboard password, run-detached-by-default) plus theme and the four traffic-control toggles, for anyone who'd rather not touch the CLI. - Proxy URL QR code — the sidebar's copyable Proxy URL has a "Show QR code" button next to it, for pointing a phone's Wi-Fi proxy settings at Detour by scanning instead of typing.
Rule engine
Traffic routing, rewriting, and mock substitution, declared once as JSON — evaluated top to bottom, first enabled match wins.
{
"rules": [
{
"name": "mock-users",
"match": { "method": "GET", "url": "https://api.example.com/users/*" },
"action": { "type": "mock", "status": 200, "body": { "id": 1, "name": "Mock User" } }
}
]
}
detour rules init # generate a sample rules.json detour rules validate rules.json # check its schema/consistency detour start --rules rules.json # start with rules applied
Matching
Every rule has an enabled flag, true by default —
set it to false to keep a rule in the file without it ever matching, rather
than deleting or commenting it out.
match.method matches any method if omitted. For the URL, use exactly one of:
wildcard match.url (* any run of characters,
? any single character) or a full regular expression via
match.urlRegex (with optional urlRegexFlags) for
anything a wildcard can't express.
Action types
mock Respond without touching the real server
Immediate status/headers/body — object bodies are JSON-serialized, strings sent verbatim, or point
bodyFile at a file. statusMessage overrides the
status line's reason phrase (e.g. "status": 418, "statusMessage": "I'm a teapot").
delayMs adds artificial latency before responding — including before either
simulate option below fires. simulate replaces the
response entirely, and its two values behave differently: "close" drops the
connection immediately, same as a server that crashed mid-request; "timeout"
does nothing at all — the connection is left open and the client hangs until it hits its own read/request
timeout.
route Redirect the destination
Sends the request to a different host/port. preserveHostHeader: false
also rewrites the Host header to match.
rewrite Adjust headers, query, status, or body in flight
request and response each take their own
headers/body, plus query
(request-only) and status (response-only, to override just the status code
without switching to a full mock):
{ "action": { "type": "rewrite",
"request": { "headers": { "set": { "X-Debug": "1" }, "remove": ["X-Legacy"] }, "query": { "set": { "cache": "false" } } },
"response": { "status": 503, "body": { "merge": { "maintenance": true } } } } }
request and response are independent — include either one, or both. A body rewrite takes exactly one of set (replace the body outright), replace (sequential string/regex find-and-replace), or merge (a JSON Merge Patch: each key overwrites the same key in the body, and a null value deletes that key).
breakpoint Pause and edit live
Stops a matching exchange mid-flight for the dashboard's breakpoint editor.
request/response (both default true)
pick which phase(s) pause. No timeout — only enable this on rules you're actively watching.
script A JS hook for anything else
The rule itself just points path at a CommonJS module, resolved relative to
rules.json (not the current working directory):
{ "name": "add-header", "match": { "url": "https://api.example.com/*" }, "action": { "type": "script", "path": "./rules.script.js" } }
module.exports = { beforeRequest(req) { return { headers: { ...req.headers, 'X-Detour': '1' } }; }, async beforeResponse(req, res) { return { body: res.body.toString('utf8').replace('"pending"', '"confirmed"') }; }, };
Either hook may be async; a thrown error leaves that phase
forwarded untouched rather than dropping the exchange. Reloaded automatically on change, same as
rules.json itself. The example above only touches headers/body —
beforeRequest can also return a new method, and
beforeResponse a new status/statusMessage.
Setup & platforms
detour setup --target <target> trusts the CA certificate and
points a platform's system proxy at Detour, as far as that platform allows.
--target is required for setup to change anything.
Run detour setup with no target and it just reports: every platform it
could set up from this machine, and the exact command for each. doctor and
cleanup are safer — read-only and revert-only — so those two do act on every
platform at once by default.
Trusts the cert in your login keychain, points the active network service's proxy at Detour — no sudo prompt.
Pushes the cert over adb and opens Security settings for the one required tap; sets the global HTTP proxy. No adb device? Falls back to a QR-code Wi-Fi pairing flow.
Trusts the cert on every booted Simulator — it shares the Mac's network stack, so it inherits whatever --target mac configures.
Apple's device CLI has no equivalent to Simulator's keychain command, or a way to set a device's Wi-Fi proxy.
Sets GNOME's proxy via gsettings, when available — but CA cert trust is always a manual, one-line sudo step Detour prints for you to run.
Prints the manual steps — no automation yet.
detour setup --target mac # actually configures this machine detour setup # lists every reachable target + how to set each up (no changes made) detour doctor --target mac # check without changing anything detour cleanup --target mac # revert what setup applied
detour doctor exits non-zero if anything's off — cert not trusted, proxy
pointed somewhere else — so it's scriptable. detour cleanup reverts the
proxy configuration without touching the CA cert's trust or your rules file, so a target that isn't
running Detour right now doesn't stay stuck routing through a proxy that's gone. All three accept
--host <host> to override the address they advertise/verify for a target —
useful when auto-detecting a device's LAN IP picks the wrong network interface.
LAN access & security
The proxy is reachable from your LAN by default; the dashboard is localhost-only until you opt it in — one flag, and it comes with a password gate if you want one.
Reaching Detour from another device
The proxy always binds to every network interface (0.0.0.0) — a proxy
nothing else on the network can reach isn't much of a proxy. The dashboard is the one
gated by a flag: it binds to localhost only by default, and
detour start --lan (or detour config --lan on
to make it the default) binds it to every interface too, so a phone on the same Wi-Fi can open the
dashboard UI directly instead of just proxying through this machine. Once the dashboard is LAN-bound,
the startup banner and the dashboard's sidebar list every reachable address — no need to go find this
machine's IP yourself.
The proxy is reachable from the LAN unconditionally — that's not something --lan
toggles. --lan only extends that same no-auth-by-default reach to the
dashboard itself (and, through it, decrypted traffic and rule edits).
detour config --dashboard-password 'correct horse battery staple' detour config --dashboard-password off # remove it
Takes effect for new connections immediately — no restart needed. This protects the
dashboard only; the proxy itself stays open to anything configured to use it. Need to force the
dashboard back to localhost for one run even with lanAccess on by default?
detour start --no-lan.
The gear icon in the sidebar opens a Settings panel with the same controls, so none of this
requires the CLI. One asymmetry to know about: the dashboard password (above) takes effect immediately
there too, but LAN access and the detach-by-default setting are grouped under "Startup defaults" in that
panel because they aren't live — like their CLI counterparts, both only apply on the next
detour start, not the session you're currently looking at.
CLI reference
Every top-level command Detour ships.
| Command | Does |
|---|---|
| detour start | Runs the proxy (and dashboard, unless --headless) |
| detour setup | Trusts the CA cert and configures a target's system proxy |
| detour doctor | Checks setup without changing anything; scriptable exit code |
| detour cleanup | Reverts what setup applied |
| detour cert export [path] | Writes the CA cert to a file (or stdout) |
| detour rules init / validate | Scaffolds or schema-checks a rules file |
| detour config | Persists defaults — --lan, --dashboard-password, --default-detach |
| detour status / stop | Inspects or stops a running (incl. detached) instance by port |
Daemon mode & CI
For running unattended — a CI pipeline, a long-lived background process:
--headless— proxy only, no dashboard--no-open— skip auto-opening the dashboard (see Getting started) — naturally useful here too, since a CI box usually has no browser to open one in--exit-on-idle <ms>— exits once traffic goes quiet for that long--fail-on-running— exit code3if already running on this port, distinguishable from any other startup failure--detach— background daemon; manage it withdetour status --port <n>/detour stop --port <n>(logs to~/.detour/logs/<port>.log)--foreground— the explicit opposite of--detach, for one run, ifdetour config --default-detach onmade detaching the default
Every successful start prints a single DETOUR_READY proxyPort=<n> dashboardPort=<n> pid=<n> line once it's actually ready — a script can wait on that instead of guessing at startup time. The dashboardPort segment is omitted under --headless, since there's no dashboard to report.
--port 0 (an ephemeral port) can't be combined with --detach or --fail-on-running — both need a stable, known port to reconnect to or check against later, and an ephemeral port has no fixed value until after it's already bound.
Checks (contributing)
| Script | What it checks |
|---|---|
| npm run verify | format · typecheck · lint · lint:fsd · dep-cruise · dup-check · test:coverage · test:web · test:e2e — the full CI gate |
| npm run test:e2e | Spawns the real CLI against a real HTTP server and socket |
| npm run test:coverage | Unit tests plus a ≥85% branch-coverage floor on the rule engine |
| npm run test:mutation | Stryker — confirms the suite catches real bugs, not just runs lines |
The dashboard's own source (React 19 + Vite + Tailwind + Zustand) lives in web/ — run npm run dev:dashboard alongside detour start for hot-reloading UI changes.
The proxy engine itself (CONNECT tunneling, on-the-fly per-host TLS certs, HTTP/1.1 +
HTTP/2) is built directly on Node's http/https/http2/tls/net
and node-forge — deliberately not a third-party MITM library, so the
request/response pipeline can genuinely stream and throttle chunk-by-chunk.
Known limitations
- WebSocket-over-HTTP/2 (RFC 8441 extended CONNECT) isn't supported — a WebSocket connection to a host that also uses HTTP/2 for its regular traffic still works, but negotiates plain HTTP/1.1 for the WebSocket connection itself, same as most browsers do anyway.
- The proxy→upstream leg is always HTTP/1.1, even when the client negotiated HTTP/2 against Detour's MITM'd TLS server —
--no-http2only affects that client-facing side. - Certificate pinning defeats interception the same way it defeats any MITM proxy — see Troubleshooting for what that looks like and the app/device-side workarounds.
- Captured body display cap — the dashboard's inspector holds up to 256 KB of a body per exchange; see Web dashboard's Live log section for what that does and doesn't affect.
Detour is licensed under the Apache License 2.0. Source and issues: github.com/rwadada/Detour.
Detour is at 0.x — rules.json's schema and the CLI's flags may still change between minor versions.
Nothing is showing up
Traffic isn't appearing in the log or the console. In rough order of how often each one turns out to be it:
Checklist
- Intercept is off, or the host is outside Focus. While Intercept is off, HTTPS passes straight through undecrypted — nothing to show. If Focus has an allowlist set, a host not on it gets the same treatment. See Web dashboard → Traffic controls.
- The CA certificate isn't installed, or isn't trusted. See
Getting started → Trust the CA certificate — or run
detour doctor --target <target>, which checks this without changing anything. - On iOS, the profile is installed but trust wasn't flipped on separately. Installing the profile alone leaves it untrusted for TLS — it still needs a second, separate step under Settings → General → About → Certificate Trust Settings. Missing this is the single most common reason nothing shows up on iOS.
- On Android, the app targets API 24+ and doesn't trust user-added CAs. Since Android 7
(API 24), apps don't trust user-added CAs by default unless they opt in via a
network_security_config. A rooted device installing the cert into the system store instead is the more reliable path for those apps. - The app pins its own certificates. See below.
Certificate pinning
An app's own defense, not something Detour — or any MITM proxy — can see through from the network side. It's enforced inside the app process. A production build that stopped showing decrypted traffic between two of its own versions, with nothing changed on the proxy side, is a strong signal the newer build added pinning, not a Detour regression.
The fix has to live on the app/device side: a debug- or QA-only build variant that skips pinning, or — for a build whose source you don't control — a rooted device with a runtime bypass.