Docs

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

ArgumentRequiredDescription
portYesLocal port to forward to (e.g., 3000, 8080)

Flags

FlagTypeDefaultDescription
--namestringRandomCustom subdomain. Lowercase, 3–50 chars, no leading/trailing hyphens
--domainstringworkslocal.exposedTunnel domain (currently only workslocal.exposed)
--jsonbooleanfalseOutput tunnel info as JSON, suppress colored logs
--inspectbooleantrueStart 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

CodeMeaning
0Clean shutdown (Ctrl+C or workslocal stop)
1Connection error (relay server unreachable)
2Subdomain error (taken, invalid, or reserved)

What happens on Ctrl+C

  1. Sends close_tunnel to the relay for each active tunnel
  2. Closes the WebSocket connection cleanly (code 1000)
  3. Stops the web inspector server
  4. 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

FlagTypeDefaultDescription
--namestringRandomCustom subdomain
--portnumberPort for the catch mode URL display (cosmetic)
--statusnumber200HTTP status code to return to callers
--bodystring""Response body to return to callers
--jsonbooleanfalseOutput as JSON

Behavior

  • Creates a tunnel like workslocal http but does NOT forward to localhost
  • Every incoming request gets a static response (default: 200 OK with 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:

FlagDescription
--versionPrint version and exit
--helpPrint help for the command
--jsonMachine-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"
}
FieldDescription
anonymousTokenRandom 32-byte hex token, generated on first run. Gives anonymous users a consistent identity for subdomain reservation on reconnect
sessionTokenClerk JWT, set after workslocal login. Enables persistent subdomains
userIdClerk user ID
serverUrlRelay server URL. Override with WORKSLOCAL_SERVER_URL env var

Environment variables

VariableDescriptionDefault
WORKSLOCAL_SERVER_URLWebSocket URL of the relay serverwss://api.workslocal.dev/ws
WORKSLOCAL_API_KEYAPI key for authentication (alternative to workslocal login)

Auto-reconnect

When the WebSocket connection drops (laptop sleep, network change, relay restart), WorksLocal automatically reconnects:

  1. Exponential backoff: 1s → 2s → 4s → 8s → 16s → 30s (capped)
  2. Up to 10 attempts by default
  3. On successful reconnect, all tunnels are re-created with the same subdomains
  4. 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:

ScopeLimitWindow
Per tunnel1,000 requests1 hour
Per IP (anonymous)200 requests1 minute
Per user (authenticated)5,000 requests1 hour
Tunnel creation10 tunnels1 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.exposed only (.io, .run coming 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 --password and --allow-ip flags are planned
  • macOS/Linux/Windows — requires Node.js 20+. Standalone binaries (Homebrew, winget) coming later