Detour
v0.1.13 Apache-2.0
GitHub
Detour · HTTP debugging proxy

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.

terminal
# 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.

02 · Setup

Getting started

Two ways to install, one certificate to trust, then point a client at the proxy port.

Install

Homebrew (recommended)

terminal
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

terminal
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.

TIP

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.

macOS

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.

Windows

certmgr.msc → Trusted Root Certification Authorities → Import.

iOS (device)

Install the profile, then flip it on separately under Certificate Trust Settings. Missing that second step is the #1 reason nothing shows up.

Android

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.

Simulators / emulators

Usually the easiest — no pinning restrictions to fight.

NOTE

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

FlagWhat 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-http2Falls 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-openDon'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.

03 · Interface

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.

NOTE

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.

TimeMethodStatusURLSize
14:02:18.114GET200api.example.com/users/421.2 KB
14:02:18.301POST302api.example.com/session0 B
14:02:19.008GET404cdn.example.com/missing.png612 B
14:02:19.442GET503api.example.com/report84 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.json as a named, switchable ruleset (flip between staging and production without 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 config does 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.
04 · rules.json

Rule engine

Traffic routing, rewriting, and mock substitution, declared once as JSON — evaluated top to bottom, first enabled match wins.

rules.json
{
  "rules": [
    {
      "name": "mock-users",
      "match": { "method": "GET", "url": "https://api.example.com/users/*" },
      "action": { "type": "mock", "status": 200, "body": { "id": 1, "name": "Mock User" } }
    }
  ]
}
terminal
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):

rules.json (excerpt)
{ "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):

rules.json (excerpt)
{ "name": "add-header", "match": { "url": "https://api.example.com/*" }, "action": { "type": "script", "path": "./rules.script.js" } }
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.

05 · detour setup / doctor / cleanup

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.

macOS automated

Trusts the cert in your login keychain, points the active network service's proxy at Detour — no sudo prompt.

Android mostly

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.

iOS Simulator automated

Trusts the cert on every booted Simulator — it shares the Mac's network stack, so it inherits whatever --target mac configures.

iOS device manual

Apple's device CLI has no equivalent to Simulator's keychain command, or a way to set a device's Wi-Fi proxy.

Linux mostly

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.

Windows manual

Prints the manual steps — no automation yet.

terminal
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.

06 · Network exposure

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.

NO AUTH BY DEFAULT

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).

terminal
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.

07 · Reference

CLI reference

Every top-level command Detour ships.

CommandDoes
detour startRuns the proxy (and dashboard, unless --headless)
detour setupTrusts the CA cert and configures a target's system proxy
detour doctorChecks setup without changing anything; scriptable exit code
detour cleanupReverts what setup applied
detour cert export [path]Writes the CA cert to a file (or stdout)
detour rules init / validateScaffolds or schema-checks a rules file
detour configPersists defaults — --lan, --dashboard-password, --default-detach
detour status / stopInspects 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 code 3 if already running on this port, distinguishable from any other startup failure
  • --detach — background daemon; manage it with detour status --port <n> / detour stop --port <n> (logs to ~/.detour/logs/<port>.log)
  • --foreground — the explicit opposite of --detach, for one run, if detour config --default-detach on made 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)

ScriptWhat it checks
npm run verifyformat · typecheck · lint · lint:fsd · dep-cruise · dup-check · test:coverage · test:web · test:e2e — the full CI gate
npm run test:e2eSpawns the real CLI against a real HTTP server and socket
npm run test:coverageUnit tests plus a ≥85% branch-coverage floor on the rule engine
npm run test:mutationStryker — confirms the suite catches real bugs, not just runs lines
TIP

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-http2 only 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.

08 · First run

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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.