A headless, content-addressable local network proxy for mobile and agent workflows.
Diffwire intercepts live HTTPS traffic via MITM TLS decryption, lets you branch API payloads like Git commits, and exposes everything to AI agents via MCP. Built for complex multi-step flows — KYC, ledger mutations, embedded banking — where staging environments are too slow and static mocks are too dumb.
Source, docs, and issues: https://github.com/iksnerd/diffwire
docker run -d --name diffwire \
-p 18081:18081 -p 13001:13001 -p 13002:13002 \
-v diffwire-data:/app/.diffwire \
iksnerd/diffwire:latest -upstream https://your-staging-api.com
Open the dashboard at http://localhost:13001. You should see the Routes/Traffic workspace with an empty traffic feed and the engine status row showing all three ports green.
Diffwire performs MITM TLS decryption with a per-workspace root CA generated on first run. Clients reject HTTPS traffic until they trust this CA. Grab the PEM first:
curl -O http://localhost:13001/api/cert/ca.pem
Then trust it per platform:
# macOS — system keychain (sudo prompts)
sudo security add-trusted-cert -d -r trustRoot \
-k /Library/Keychains/System.keychain ca.pem
# Linux (Debian/Ubuntu)
sudo cp ca.pem /usr/local/share/ca-certificates/diffwire.crt
sudo update-ca-certificates
# iOS Simulator (booted simulators)
xcrun simctl keychain booted add-root-cert ca.pem
# Android emulator / device (rooted or via Network Security Config)
adb push ca.pem /sdcard/ # then: Settings → Security → Install from storage
iOS physical devices: use the dashboard's Setup workspace (sidebar) — it generates a .mobileconfig profile and shows a QR code. Linux/macOS hosts can also click "Set as system proxy" from the same Setup workspace; the dw CLI's dw setup mirrors this on the command line but is not bundled in the Docker image.
Set both HTTP_PROXY and HTTPS_PROXY — many clients use one but not the other.
# Host process:
export HTTP_PROXY=http://localhost:18081
export HTTPS_PROXY=http://localhost:18081
# A different Docker container on the same host:
docker run --rm \
--add-host host.docker.internal:host-gateway \
-e HTTP_PROXY=http://host.docker.internal:18081 \
-e HTTPS_PROXY=http://host.docker.internal:18081 \
your-app-image
# iOS Simulator / Android emulator: configure the proxy through the
# system network settings, pointing at your Mac's LAN IP and port 18081.
curl --proxy http://localhost:18081 https://httpbin.org/get
Refresh the dashboard. A new row appears in the Traffic workspace — click it to see decoded request + response headers, body, and timing in the inspector. If the row doesn't appear, the proxy isn't being used; if it appears but shows a TLS error, the CA isn't trusted by the client running curl.
The root CA, routes, branches, and history persist in the diffwire-data named volume across restarts. Deleting the volume regenerates a new CA — clients will need to re-trust the new one.
| Port | Purpose |
|---|---|
18081 | MITM proxy — point your client traffic here |
13001 | Control Plane REST API + static web dashboard |
13002 | MCP server (Streamable HTTP, stateless) for agents |
| Flag / Env | Default | What it does |
|---|---|---|
-upstream <url> / DIFFWIRE_UPSTREAM | (required — engine warns on startup) | Default upstream for unmatched routes |
--project <name> | default | Isolate workspace under /app/.diffwire/<name>/ |
-insecure-upstream | false | Trust self-signed upstream certs (test fixtures) |
Example with env vars and an isolated project:
docker run -d --name diffwire-staging \
-p 18081:18081 -p 13001:13001 -p 13002:13002 \
-v diffwire-data:/app/.diffwire \
-e DIFFWIRE_UPSTREAM=https://api.staging.example.com \
iksnerd/diffwire:latest --project staging
.proto schema needed for gRPC v1){{uuid}}, {{now}}, {{randomInt min max}}, …)linux/amd64, linux/arm64gcr.io/distroless/static:nonroot — no shell, no package managernonroot (UID 65532)/app/.diffwire (declared VOLUME)/app/diffwire — extra args are forwarded as flags| Tag | Meaning |
|---|---|
latest | Most recent release (does not move on plain main pushes) |
X.Y.Z | Pinned semver release — also rolled up as X.Y and X (no v prefix) |
main | Tip of the main branch — rebuilt on every push |
sha-<short> | Build pinned to a specific commit (both main pushes and tag pushes) |
For reproducible deployments, pin to X.Y.Z or sha-<short>. main is a moving target; latest only changes when a new release tag is cut. Note the published tags use bare semver (e.g. 0.1.2), matching the convention of Docker Hub libraries like postgres and node — the v prefix only lives on the git tag.
The same image is mirrored at ghcr.io/iksnerd/diffwire with the same tag scheme — useful if your org standardizes on GitHub Container Registry. Both registries are updated by the same CI workflow on every push, so digests match.
security command was skipped.:18081. Confirm with curl --proxy http://localhost:18081 https://httpbin.org/get (it should appear in the feed). If that works but your app doesn't, your app is ignoring HTTP_PROXY env vars (common with Node's fetch, Go's http.DefaultClient without ProxyFromEnvironment, etc.).port already in use on docker run — another process is bound to :18081, :13001, or :13002. Either kill it or remap with -p 28081:18081 etc.-upstream configured — the engine logs a 3-line warning at boot. Explicitly matched routes still work, but unmatched HTTPS traffic has nowhere to forward to.localhost is not the host's localhost. For another container on the same host, use --add-host host.docker.internal:host-gateway and target http://host.docker.internal:18081 (shown in step 3).dw CLI not in the container — the Docker image ships only the engine binary. dw (setup helpers, named sessions, etc.) is for host-side use; install it via make build-dw from source or grab the matching tarball from the GitHub Release.MIT — see LICENSE.
Content type
Image
Digest
sha256:661c20259…
Size
14.6 MB
Last updated
3 months ago
docker pull iksnerd/diffwire