@reopt-ai/dev-proxy
v2.0.0
Published
Subdomain-based reverse proxy and HTTP/WS traffic inspector TUI
Maintainers
Readme
dev-proxy
A development proxy built for agentic, multi-project workflows.
Develop multiple projects and Git worktrees in parallel with your coding agents — one proxy routes every project's services by subdomain, and one terminal shows their HTTP/WS traffic together.
Routes *.{domain}:3000 requests to local services by subdomain and displays all traffic in a terminal dashboard. Think of it as a lightweight, terminal-native alternative to tools like Charles or Proxyman — purpose-built for local multi-service development.

With wildcard DNS pointing to your proxy, a trusted TLS certificate, and httpsPort: 443, each project gets a clean URL like https://shop.dev.example.com while its services keep their own internal ports. Open the same URL on a phone or tablet on the same LAN to test on real devices — no per-device /etc/hosts edits.

Why dev-proxy?
When coding agents work across multiple projects and branches at once, each project brings its own frontend, API, auth, and other services. dev-proxy gives them distinct subdomains and a shared traffic inspector, so you can develop and debug them together from one terminal.
dev-proxy is:
- Multi-project by design — Route services from multiple registered projects through one proxy and inspect their traffic together
- Zero-config start — Works out of the box with
localhostand auto-generated TLS certs - Terminal-native — No browser windows to manage; lives where you already work
- Vim-style navigation —
j/kto browse,/to search,rto replay - Worktree-aware — Routes
branch--app.domainto per-worktree ports automatically - Real-device testing — With wildcard DNS configured, phones and tablets on the same LAN use the same URLs without per-device
/etc/hostsedits - Team-ready — One root proxy serves the whole LAN; another machine takes over a subdomain with
peer run, approved from the root's TUI - Lightweight — Two runtime dependencies (
ink+react), ~10fps throttled rendering
Features
- Real-time HTTP request/response monitoring (method, status, size, latency)
- WebSocket connection tracking (OPEN / CLOSED / ERROR)
- Request/response header, cookie, and query parameter inspection
- Noise filter (
_next/,favicon), error-only mode, URL/method search - Request replay with original headers and curl copy to clipboard
- Upstream
http/httpsandws/wsstarget support - Git worktree-based dynamic routing via project config
- Peers: serve a subdomain from another machine on the LAN —
peer join <domain>pairs with one approval on the root (no token copying),peer runclaims the subdomain while the dev server runs and releases it on exit - Auto-generated TLS certificates via mkcert
- Project-based config: global (
~/.dev-proxy/config.json) + per-project (dev-proxy.config.mjsfor routes +worktreeConfig,.dev-proxy.worktrees.jsonfor the CLI-managed worktree map). Legacy.dev-proxy.jsonis read as a fallback.
Prerequisites
- Node.js >= 20.11
- mkcert (optional, for HTTPS) —
brew install mkcert && mkcert -install
Quick Start
For Humans
npm install -g @reopt-ai/dev-proxy
dev-proxy init
dev-proxyPress Enter to arm the inspector, then open http://www.example.dev:3000 in your browser.
For AI Agents
Option 1 — Claude Code skill (recommended):
npx skills add reopt-ai/dev-proxyThen tell Claude Code: "setup dev-proxy" — it will detect your project structure, suggest routes, and generate config files automatically.
Option 2 — Any AI agent:
Paste this to your AI coding agent (Claude Code, Cursor, Copilot, etc.):
Install and configure dev-proxy by following the instructions here: https://raw.githubusercontent.com/reopt-ai/dev-proxy/main/docs/guide/installation.md
Install
# npx (no install)
npx @reopt-ai/dev-proxy
# Global install
npm install -g @reopt-ai/dev-proxy
# From source
git clone https://github.com/reopt-ai/dev-proxy.git
cd dev-proxy && pnpm install && pnpm proxyConfiguration
Config lives in three files for new setups:
~/.dev-proxy/config.json— Global settings (domain, ports, TLS, project list)<project>/dev-proxy.config.mjs— Per-project routes + (optional)worktreeConfig. Hand-edited.<project>/.dev-proxy.worktrees.json— CLI-managed worktree instance map (dev-proxy initwrites an empty{ "worktrees": {} }placeholder; the worktree CLI commands keep it in sync — do not hand-edit)
Easiest path: run
dev-proxy initfrom your project directory and the wizard creates these files for you. The sections below cover the file format if you'd rather author them by hand.
Legacy
<project>/.dev-proxy.jsonis a read-only fallback for older projects (pre-mjs layouts). Rundev-proxy migrateonce and it goes away.
Global Config (~/.dev-proxy/config.json)
{
"domain": "example.dev",
"port": 3000,
"httpsPort": 3443,
"projects": ["/path/to/your/project"]
}port and httpsPort can be 80 and 443 so URLs need no port suffix (dev-proxy config set httpsPort 443). macOS lets an unprivileged process bind them; on Linux, grant Node the capability once instead of running the proxy as root:
sudo setcap 'cap_net_bind_service=+ep' "$(readlink -f "$(command -v node)")""port": false (dev-proxy config set port off) turns the plain-HTTP listener off so the proxy serves HTTPS only. It needs a certificate; dev-proxy refuses to start without one. Keep HTTP on if local tools need it. Remote peer control always requires HTTPS, even when the HTTP listener is enabled.
Optional keys:
certPath/keyPath— use your own certificate instead of the mkcert default (relative paths resolve from~/.dev-proxy/). See Access from other devices.timeouts— upstream deadlines in seconds,{ "connect": 10, "responseHeaders": 60 }by default.falseturns one off. RaiseresponseHeadersfor cold compiles or long-poll endpoints that hold headers longer. Read at startup.
Project Routes (dev-proxy.config.mjs)
Place a dev-proxy.config.mjs in each project root registered in projects. Routes are defined here.
/** @type {import('@reopt-ai/dev-proxy').Config} */
export default {
routes: {
"@": "http://localhost:3005",
www: "http://localhost:3001",
studio: "http://localhost:3001",
api: "http://localhost:4000",
"*": "http://localhost:3001",
},
};"@"matches the apex (bare) domain — e.g.example.devitself, with no subdomain. Optional; falls back to"*"if absent."*"is a wildcard — unmatched subdomains of the configured domain route here. Requests with anotherHost(including a bare LAN IP) are rejected before any route or peer lookup.- When multiple projects register the same subdomain, the first one wins
certPath/keyPathare set in the global config, resolved relative to~/.dev-proxy/dev-proxy.config.jsis also accepted (used whenpackage.jsonhas"type": "module");.mjstakes precedence if both exist- Resolution order at runtime:
dev-proxy.config.mjs→dev-proxy.config.js→.dev-proxy.json(legacy fallback for bothroutesandworktreeConfig) - The CLI-managed
worktreesinstance map always lives in.dev-proxy.worktrees.json.worktreeConfig(if present) belongs in the samedev-proxy.config.mjsdefault export asroutes— see Worktree Routing.
JS configs are evaluated in a fresh Worker on every load, including their relative imports, and the Worker is then terminated. Export plain data in routes and worktreeConfig; configuration evaluation must finish within 10 seconds. Module globals and process changes do not persist between reloads.
HTTP upstream requests have a queue/connection/TLS deadline (10 seconds) and a response-header deadline after the request body is sent (60 seconds); both are set by timeouts. WebSocket upgrades share the connection deadline. Client disconnects release the upstream connection immediately. Upload time is not counted toward the header deadline, and responses such as SSE have no overall duration limit. If the upstream fails after the response has started, the client connection is reset, so a truncated body is never mistaken for a complete one.
Migrating from .dev-proxy.json
Earlier versions stored everything (routes, worktreeConfig, and the worktrees instance map) in .dev-proxy.json. The format still works at read time as a fallback, but new setups use dev-proxy.config.mjs (routes + worktreeConfig) + .dev-proxy.worktrees.json (CLI-managed instance map). To consolidate every registered project at once:
dev-proxy migrateThe command moves routes and worktreeConfig into dev-proxy.config.mjs, moves the worktrees instance map into .dev-proxy.worktrees.json, and deletes .dev-proxy.json once it is empty. It is idempotent — projects already migrated are skipped.
HTTPS
Certificates are stored in ~/.dev-proxy/certs/. If missing, they are auto-generated using mkcert.
brew install mkcert
mkcert -installWhen mkcert is installed, dev-proxy automatically generates wildcard certificates on first run. No manual steps needed.
Access from other devices
The proxy listens on every interface, but only routes requests whose hostname is the configured domain or one of its subdomains. On another device, configure DNS or hosts so those names resolve to the proxy's LAN IP, and establish certificate trust for HTTPS. Pick one of these setups:
| | DNS | TLS | Works on phones |
| -------------------------------------------- | ---------------------------------------------------------------------------- | ----------------------------------------- | ---------------------- |
| Public DNS + Let's Encrypt (recommended) | wildcard A record on a domain you own → your LAN IP | real certificate via DNS-01 challenge | yes, zero device setup |
| dnsmasq on your machine | run dnsmasq bound to your LAN IP, point devices (or the router's DHCP) at it | install the mkcert root CA on each device | yes, after CA install |
| hosts file per device | edit /etc/hosts on each device | install the mkcert root CA on each device | no |
Public DNS + Let's Encrypt
- Reserve a fixed LAN IP for your machine in your router (DHCP reservation).
- Add
*.dev.example.comanddev.example.comArecords pointing at that private IP. Some routers block DNS answers that contain private IPs (rebinding protection) — allow the domain in the router ifnslookup app.dev.example.comfails on another device. - Issue a wildcard certificate with the DNS-01 challenge, e.g.
certbot certonly --manual --preferred-challenges dns -d '*.dev.example.com' -d dev.example.com, and pointcertPath/keyPathin~/.dev-proxy/config.jsonat the issued files. - Set
domaintodev.example.com.
Keep in mind the record is public: anyone can look up that *.dev.example.com points at a private address, although only devices on your network can reach it.
Renewal. Let's Encrypt certificates last 90 days. Automate the DNS-01 challenge with certbot's --manual-auth-hook / --manual-cleanup-hook (a script that adds and removes the _acme-challenge TXT record through your DNS provider's CLI or API) and run certbot renew from cron or launchd. dev-proxy watches the certificate files and swaps a renewed certificate in without a restart — including when certbot repoints the live/*.pem symlinks.
Sharing one service across machines. Because the public record points at one machine, other developers get that machine's services by default. A developer who runs some apps locally overrides only those subdomains in their own /etc/hosts (hosts entries win over DNS) and leaves the rest alone. For example, to run studio locally but use the shared id server:
127.0.0.1 studio.dev.example.com
# no entry for id.dev.example.com → DNS → the shared machineHostnames stay the same everywhere, so cookies, OAuth redirect URIs and CORS allow-lists do not change between the shared and the local setup.
Sharing the mkcert CA instead
mkcert -CAROOT prints the directory holding rootCA.pem. Install that file as a trusted root on each device (iOS: AirDrop → install profile → Settings › General › About › Certificate Trust Settings; Android: Settings › Security › Install a certificate). The existing wildcard certificate already covers every subdomain, so nothing needs to be re-issued.
In every setup, run dev-proxy doctor: the Network section shows the address other devices can use, DNS reports whether each hostname resolves to loopback (this machine only) or to your LAN address (reachable from the network), and TLS tells you whether the certificate is mkcert-issued or publicly trusted.
Peers: serve a subdomain from another machine
With public DNS pointing at one machine (the root), a second machine can take over a subdomain without running a proxy and without anyone touching hosts files. The root routes the subdomain to the peer's app port; TLS, the inspector and every hostname stay on the root, so cookies, OAuth redirect URIs and CORS allow-lists do not change.
Remote peers require HTTPS with a certificate they trust and a matching hostname. There is no plaintext fallback.
| Root setup | How peers join |
| ---------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| public wildcard DNS + publicly trusted certificate + httpsPort 443 or 3443 | peer join example.dev |
| trusted certificate on another HTTPS port | peer join https://root.example.dev:8443 |
| mkcert certificate | trust the root's CA in Node.js, then use the HTTPS hostname |
| no wildcard DNS | add root.example.dev to the peer's DNS or hosts file, matching the certificate |
For mkcert, copy only the root's rootCA.pem to the peer (never rootCA-key.pem) and set NODE_EXTRA_CA_CERTS=/path/to/rootCA.pem when running peer commands. Keep this setting for peer run, claim, release and list, not just join. The root can turn its HTTP listener off. dev-proxy doctor → Peers reports the required HTTPS origin and trust setup.
On the peer:
dev-proxy peer join example.dev # once — prints a code and waits
dev-proxy peer run studio --port 3001 -- pnpm dev # claim studio while pnpm dev runspeer join <domain> reaches the root at https://root.<domain> (the wildcard record already resolves it, so nobody needs the root's IP) and asks to pair. The root's TUI shows the machine's name, address and the same code; press A then Y there to approve, or D to deny. No secret is copied by hand: the peer generates its own token and the root stores only its hash. On the root, dev-proxy peer devices lists paired machines and dev-proxy peer revoke <name> unpairs one and releases the subdomains it had claimed.
peer join <domain> tries https://root.<domain> on port 443, then on this machine's httpsPort (3443 unless configured). For any other port or hostname, pass the full origin (peer join https://root.example.dev:8443). --token <token> skips approval using the root's own token from ~/.dev-proxy/peer-token, and still requires HTTPS remotely. A guessed root.<domain> must accept the token before it is saved; an explicit origin is saved as given, so it works while the root is down. Existing joins stored as remote http://… in ~/.dev-proxy/peer.json must be replaced by joining the HTTPS origin again; a stored http://localhost:<port> keeps working as http://127.0.0.1:<port>. Commands on the root itself can continue to use literal loopback HTTP. dev-proxy doctor on a joined machine checks that the root still accepts its token.
peer run claims studio.<domain> → http://<this machine's LAN IP>:3001 when the command starts and releases it when the command exits (including Ctrl+C). Put it in the app's dev script and developers keep typing pnpm dev. The app must listen on 0.0.0.0, not only localhost — Next.js does by default, Vite needs vite --host, and most other dev servers take --host 0.0.0.0 or HOST=0.0.0.0. peer claim / peer release do the same without wrapping a command, and peer list shows every claim with its owner and reachability.
Rules: a claim beats the root's local route for the same subdomain; the last claim wins (the previous owner is reported); the root probes claim targets every 30s and drops a claim that has been unreachable for 10 minutes. The control API (/_dev-proxy/… on the proxy ports) only answers callers from loopback or private networks, requires TLS for non-loopback callers, refuses requests sent by a browser, and requires a bearer token for everything except the pair request itself. Repeated pairing requests from one address are limited to one every 5 seconds.
Worktree Routing
dev-proxy supports git worktree-based dynamic routing. When you use branch--app.domain as the hostname, it routes to a per-worktree port.
Automatic lifecycle management:
Routes and worktreeConfig go in the same dev-proxy.config.mjs default export (you author this); the live worktrees instance map goes in .dev-proxy.worktrees.json (the CLI manages this). Use services to define per-subdomain port mappings — dev-proxy allocates ports automatically and generates a .env.local file so your dev servers know which port to listen on:
// dev-proxy.config.mjs
/** @type {import('@reopt-ai/dev-proxy').Config} */
export default {
routes: {
www: "http://localhost:3001",
data: "http://localhost:4001",
"*": "http://localhost:3001",
},
worktreeConfig: {
portRange: [4101, 5000],
directory: "../myproject-{branch}",
services: {
www: { env: "PORT" },
data: { env: "DATA_PORT" },
},
envFile: ".env.local",
hooks: {
"post-create": "pnpm install",
"post-remove": "echo cleanup done",
},
},
};// .dev-proxy.worktrees.json (CLI-managed — do not hand-edit)
{
"worktrees": {
"main": { "ports": { "www": 3001, "data": 4001 } }
}
}Then create and destroy worktrees with a single command:
dev-proxy worktree create feature-auth
# → git worktree add
# → allocates ports: www=4101, data=4102
# → writes .env.local: PORT=4101, DATA_PORT=4102
# → runs post-create hook (pnpm install)
dev-proxy worktree destroy feature-auth
# → runs post-remove hook, git worktree remove, releases portsHow routing works:
feature-auth--www.example.dev:3000→ routes to port 4101 (www service)feature-auth--data.example.dev:3000→ routes to port 4102 (data service)- Config file is watched live — routing updates instantly on changes
- Unregistered worktrees show an offline error page instead of silently falling back
Manual mode (without worktreeConfig):
dev-proxy worktree add feature-auth 4001 # Register single port (no git operations)
dev-proxy worktree remove feature-auth # Unregister onlyUsage
# If installed globally or via npx
dev-proxy
# From source
pnpm proxy
# Debug mode (tsx, no build step)
pnpm proxy:srcpnpm proxy builds dist/ first, then runs with NODE_ENV=production to avoid Ink/React dev-mode memory leaks.
UI States
The TUI has three states:
- Splash — Shows configured routes and listening ports. Press Enter to arm.
- Inspect — Live traffic dashboard with list + detail panels.
- Standby — Auto-sleeps after 60s of no interaction to reduce memory pressure. Press I or Enter to resume.
Keybindings
Navigation
| Key | Action |
| --------- | --------------------------- |
| ← / → | Switch list / detail focus |
| j / ↓ | Next request |
| k / ↑ | Previous request |
| g | Jump to first |
| G | Jump to last |
| Enter | Open detail panel |
| Esc | Back to list / clear search |
Detail Panel
| Key | Action |
| --------- | ------ |
| ↑ / ↓ | Scroll |
Focusing the detail panel automatically disables Follow mode so your selection stays put.
Filters & Actions
| Key | Action |
| --- | ------------------------------------------ |
| / | Search mode (filter by URL or method) |
| f | Toggle Follow mode |
| n | Toggle noise filter (_next, favicon) |
| e | Toggle error-only mode |
| x | Clear all traffic and filters |
| r | Replay selected request (with headers) |
| y | Copy selected request as curl to clipboard |
Mouse
- Scroll in list or detail panel
- Click a row to select it
- Click header filter badges to toggle them
Security
This is a development tool and makes deliberate trade-offs for local development convenience:
rejectUnauthorized: false— The proxy accepts self-signed certificates from upstream targets. This is intentional so that dev services using mkcert or self-signed certs work without extra configuration. Do not use this proxy in production.- No authentication — The proxy listens on all interfaces with no auth layer, so anything that can reach your machine on the proxy ports can use it. Keep it on trusted networks.
Troubleshooting
Port already in use
Error: port 3000 is already in use (another dev-proxy instance may already be running)Kill the existing process or use a different port in your config:
# Find and kill
lsof -ti :3000 | xargs kill
# Or change the proxy port in ~/.dev-proxy/config.json
# ("port": 3080)peer join cannot reach the root
The error names the failure; the usual causes, in order:
| Message | Cause | Fix |
| ----------------------------------------- | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------- |
| … does not resolve on this machine | typo in the domain, or the router drops public DNS answers that point at private IPs | check spelling; fix DNS or add the certificate's hostname to the peer's hosts file |
| … presented a certificate … not trust | the root uses a mkcert certificate | set NODE_EXTRA_CA_CERTS to the root's rootCA.pem, or use a publicly trusted certificate |
| … certificate issued for another name | joined by IP address or a name the certificate does not cover | join by root.<domain> or another hostname on the certificate |
| … does not speak HTTPS | the address points at the root's plain-HTTP port | use the root's httpsPort (3443 unless configured) |
| … refused the connection | dev-proxy is not running on the root, or its httpsPort is neither 443 nor this machine's | start it; dev-proxy config set httpsPort 443 on the root, or pass https://root.<domain>:<httpsPort> |
| … did not answer | different network, VPN, guest Wi-Fi or a firewall in between | put both machines on the same LAN |
| the root proxy does not support pairing | the root runs a dev-proxy older than 1.11 | upgrade it, or pass --token |
On the root, dev-proxy doctor → Peers reports the same from the other side.
mkcert not found
HTTPS disabled — mkcert not found.HTTPS is optional. Install mkcert for TLS support:
brew install mkcert # macOS
mkcert -installNext.js HMR / origin errors
Next.js >= 15.0 validates the Origin header on dev server requests. When accessing via dev-proxy (e.g., http://web.localhost:3000), the origin differs from what Next.js expects, causing blocked requests or HMR failures.
Add allowedDevOrigins to your next.config.mjs:
const nextConfig = {
allowedDevOrigins: ["http://web.localhost:3000"],
};
export default nextConfig;Add one entry per subdomain that routes to a Next.js service. If using HTTPS, also add the https:// variant.
Blank screen / Raw mode error
If you see Raw mode is not supported, you're running in a non-TTY context (e.g., piped output, CI). dev-proxy requires an interactive terminal.
Request not routing to expected target
- Check the splash screen — it lists all configured routes
- Verify your
Hostheader matchessubdomain.domain:port - Ensure the target service is actually running on the configured port
CLI Reference
| Command | Description |
| ---------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| dev-proxy | Start proxy and open traffic inspector |
| dev-proxy init | Interactive setup wizard |
| dev-proxy migrate | Move legacy .dev-proxy.json content into .mjs + .dev-proxy.worktrees.json, then delete it |
| dev-proxy status | Show configuration and routing table |
| dev-proxy doctor | Run environment diagnostics |
| dev-proxy config | View global settings |
| dev-proxy config set <key> <value> | Modify global settings (domain, port, httpsPort; port off for HTTPS only) |
| dev-proxy project add [path] | Register a project (default: cwd) |
| dev-proxy project remove <path> | Unregister a project |
| dev-proxy project list | List registered projects |
| dev-proxy worktree create <branch> | Create worktree with auto port + hooks |
| dev-proxy worktree destroy <branch> | Destroy worktree with hooks + cleanup |
| dev-proxy worktree add <name> <port> | Register worktree manually (no git operations) |
| dev-proxy worktree remove <name> | Unregister worktree manually |
| dev-proxy worktree list | List all worktrees |
| dev-proxy peer join <root> | Pair this machine with a root proxy (approved on the root) |
| dev-proxy peer run <sub> --port <n> -- <cmd> | Claim a subdomain while <cmd> runs |
| dev-proxy peer claim <sub> --port <n> | Claim a subdomain on the root |
| dev-proxy peer release <sub> | Release a claim |
| dev-proxy peer list | List claims with owner and reachability |
| dev-proxy peer devices | On the root: list paired machines |
| dev-proxy peer revoke <name> | On the root: unpair a machine and release its claims |
| dev-proxy --help | Show help |
| dev-proxy --version | Show version |
Architecture
src/
├── cli.ts # Subcommand router
├── index.tsx # TUI dashboard (Ink render + proxy lifecycle)
├── store.ts # External store (useSyncExternalStore)
├── commands/ # CLI subcommands (Ink components)
│ ├── init.tsx # Interactive setup wizard
│ ├── status.tsx # Configuration overview
│ ├── doctor.tsx # Environment diagnostics
│ ├── config.tsx # Config view/set
│ ├── project.tsx # Project management
│ ├── worktree.tsx # Worktree management
│ ├── help.tsx # Help output
│ └── version.tsx # Version output
├── bootstrap.ts # Startup bootstrapper (config load, proxy init)
├── cli/ # Shared CLI components
│ ├── config-io.ts # Config I/O helpers and port allocation
│ └── output.tsx # Display components (Header, Section, Check, etc.)
├── proxy/
│ ├── config.ts # Config loader (~/.dev-proxy + .mjs / .dev-proxy.json / .dev-proxy.worktrees.json)
│ ├── server.ts # HTTP/WS reverse proxy
│ ├── routes.ts # Subdomain → target routing
│ ├── certs.ts # TLS certificate resolution (mkcert)
│ ├── worktrees.ts # Dynamic worktree port registry
│ └── types.ts # Event types
├── components/
│ ├── app.tsx # Root (resize, keyboard, state machine)
│ ├── splash.tsx # Splash screen
│ ├── status-bar.tsx # Top status bar
│ ├── request-list.tsx # Request list (viewport slicing)
│ ├── detail-panel.tsx # Detail panel (scrollable)
│ └── footer-bar.tsx # Bottom keybinding hints
├── hooks/
│ └── use-mouse.ts # SGR mouse event parser
└── utils/
├── format.ts # Color palette, formatters
└── list-layout.ts # Responsive column layoutContributing
See CONTRIBUTING.md for development setup and guidelines.
