CLI Reference
Complete reference for the workslocal command-line tool.
Installation
npm install -g workslocal
Verify:
workslocal --version
Commands
workslocal http <port>
Start an HTTP tunnel forwarding to your local server.
workslocal http 3000 workslocal http 3000 --name myapp workslocal http 8080 --name api --json
Arguments
| Argument | Required | Description |
|---|---|---|
| port | Yes | Local port to forward to (e.g., 3000, 8080) |
Flags
| Flag | Type | Default | Description |
|---|---|---|---|
| --name | string | Random | Custom subdomain. Lowercase, 3–50 chars, no leading/trailing hyphens |
| --domain | string | workslocal.exposed | Tunnel domain (currently only workslocal.exposed) |
| --json | boolean | false | Output tunnel info as JSON, suppress colored logs |
| --inspect | boolean | true | Start web inspector at localhost:4040 |
Behavior
- Creates a WebSocket connection to the relay server
- Registers a subdomain (random or custom)
- Forwards incoming HTTP requests to
localhost:<port> - Prints live request log with method, path, status, and latency
- Opens web inspector at
http://localhost:4040 - Auto-reconnects on disconnect (up to 10 attempts with exponential backoff)
- Re-creates tunnels with the same subdomain after reconnect
Exit codes
| Code | Meaning |
|---|---|
| 0 | Clean shutdown (Ctrl+C or workslocal stop) |
| 1 | Connection error (relay server unreachable) |
| 2 | Subdomain error (taken, invalid, or reserved) |
What happens on Ctrl+C
- Sends
close_tunnelto the relay for each active tunnel - Closes the WebSocket connection cleanly (code 1000)
- Stops the web inspector server
- Exits with code 0
workslocal catch
Start a tunnel in catch mode — captures incoming requests without forwarding to a local server. Like webhook.site in your terminal.
workslocal catch workslocal catch --name stripe workslocal catch --port 8080 --name github
Flags
| Flag | Type | Default | Description |
|---|---|---|---|
| --name | string | Random | Custom subdomain |
| --port | number | — | Port for the catch mode URL display (cosmetic) |
| --status | number | 200 | HTTP status code to return to callers |
| --body | string | "" | Response body to return to callers |
| --json | boolean | false | Output as JSON |
Behavior
- Creates a tunnel like
workslocal httpbut does NOT forward to localhost - Every incoming request gets a static response (default:
200 OKwith empty body) - Requests are captured in the RequestStore and displayed in the terminal + inspector
- The response includes an
x-workslocal-mode: catchheader so callers know it's catch mode
How it works internally
The CLI creates a TunnelClient with a proxyOverride function instead of the normal LocalProxy. When an http_request message arrives from the relay, the override returns the static response immediately. The request is still captured in the RequestStore and pushed to the inspector via SSE.
Stripe → tunnel URL → Cloudflare → Durable Object → WebSocket → CLI ↓ CatchProxy returns 200 RequestStore captures it Inspector shows it via SSE ↓ 200 OK → back to Stripe
Typical workflow
# Step 1: Start catching workslocal catch --name stripe-test # → https://stripe-test.workslocal.exposed (catching) # Step 2: Paste URL in Stripe webhook dashboard, trigger test event # Step 3: See the payload in terminal + localhost:4040 inspector # Step 4: When your handler is ready, switch to tunnel mode: workslocal http 3000 --name stripe-test # → Same URL, now forwarding to localhost:3000
workslocal login
Authenticate with WorksLocal via browser-based OAuth (GitHub via Clerk).
workslocal login
Opens your default browser to complete the login flow. On success, the session token is stored in ~/.workslocal/config.json.
Why authenticate
- Persistent subdomains that survive restarts permanently
- Up to 5 simultaneous tunnels
- Required for future features: teams, custom domains, API keys
workslocal logout
Clear stored credentials.
workslocal logout
Deletes ~/.workslocal/config.json. After logout, you're back to anonymous mode with random subdomains.
workslocal whoami
Show current authentication status.
workslocal whoami
Authenticated:
Logged in as chandan@example.com User ID: user_abc123
Anonymous:
Not logged in. Run 'workslocal login' to authenticate. Anonymous token: a1b2c3...
workslocal status
List all active tunnels.
workslocal status workslocal status --json
Output:
Active tunnels: myapp https://myapp.workslocal.exposed → localhost:3000 42 requests stripe https://stripe.workslocal.exposed → catch mode 7 requests
workslocal stop <name>
Stop a specific tunnel by subdomain name.
workslocal stop myapp
workslocal stop --all
Stop all active tunnels.
workslocal stop --all
workslocal domains
List available tunnel domains.
workslocal domains
Output:
Available domains: workslocal.exposed (default)
workslocal config
Get and set configuration values.
workslocal config set default-domain workslocal.exposed workslocal config get default-domain
Global flags
These flags work with every command:
| Flag | Description |
|---|---|
| --version | Print version and exit |
| --help | Print help for the command |
| --json | Machine-readable JSON output (no colors, no spinners) |
Config file
Located at ~/.workslocal/config.json:
{
"anonymousToken": "hex-string-64-chars",
"sessionToken": "eyJ...",
"userId": "user_xxx",
"serverUrl": "wss://api.workslocal.dev/ws"
}| Field | Description |
|---|---|
| anonymousToken | Random 32-byte hex token, generated on first run. Gives anonymous users a consistent identity for subdomain reservation on reconnect |
| sessionToken | Clerk JWT, set after workslocal login. Enables persistent subdomains |
| userId | Clerk user ID |
| serverUrl | Relay server URL. Override with WORKSLOCAL_SERVER_URL env var |
Environment variables
| Variable | Description | Default |
|---|---|---|
| WORKSLOCAL_SERVER_URL | WebSocket URL of the relay server | wss://api.workslocal.dev/ws |
| WORKSLOCAL_API_KEY | API key for authentication (alternative to workslocal login) | — |
Auto-reconnect
When the WebSocket connection drops (laptop sleep, network change, relay restart), WorksLocal automatically reconnects:
- Exponential backoff:
1s → 2s → 4s → 8s → 16s → 30s(capped) - Up to 10 attempts by default
- On successful reconnect, all tunnels are re-created with the same subdomains
- Subdomain reservation (30 minutes for anonymous, permanent for authenticated) prevents hijacking during reconnect
If all 10 attempts fail, the CLI exits with a reconnect_failed event.
Rate limits
The relay server enforces these limits:
| Scope | Limit | Window |
|---|---|---|
| Per tunnel | 1,000 requests | 1 hour |
| Per IP (anonymous) | 200 requests | 1 minute |
| Per user (authenticated) | 5,000 requests | 1 hour |
| Tunnel creation | 10 tunnels | 1 hour |
When rate limited, the relay returns 429 Too Many Requests.
Limitations
- HTTP only — no TCP, UDP, or raw socket tunneling
- Single tunnel domain —
workslocal.exposedonly (.io,.runcoming later) - 10 MB body limit — request bodies over 10 MB are rejected with 413
- 30-second timeout — local server must respond within 30 seconds or 504 is returned
- No SSE/streaming — responses are fully buffered (streaming support coming next release)
- In-memory request store — captured requests lost on exit (1,000 max per tunnel, ring buffer)
- No password protection yet —
--passwordand--allow-ipflags are planned - macOS/Linux/Windows — requires Node.js 20+. Standalone binaries (Homebrew, winget) coming later