Skip to main content
skill Claude Code CX Codex Cursor GitHub Copilot

portless — Claude Code skill: what it does, how to install

Assigns local dev servers stable names like myapp.localhost instead of port numbers.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add vercel-labs/portless --skill portless -a claude-code
Open on GitHub

What portless does

Starts an HTTPS reverse proxy on port 443 and assigns each server a random port in the 4000–4999 range, while the browser gets a name. Agent and developer work with myapp.localhost, api.myapp.localhost — port conflicts, browser history confusion, and agent port guessing are gone.

Supports monorepos, git worktrees (each branch gets its own subdomain), and turborepo. Install globally with npm install -g portless or add as a dev dependency. Do NOT use npx or pnpm dlx for one-off runs — it breaks the workflow. Bypass the proxy with PORTLESS=0.

On macOS and Linux, the proxy elevates with sudo; if unavailable, it falls back to port 1355.

What it helps with

  • avoid port conflicts when two projects want the same number
  • test the right service when the agent checks multiple endpoints
  • keep cookies and localStorage isolated between localhost apps
  • run several monorepo services with clear names
  • switch between git worktrees without URL collisions

Example prompts for the agent

  • set up portless for this project with name myapp
  • start the dev server through portless
  • add a proxy for api.myapp and run the backend

How to install portless

1Skills CLI (Vercel)

npx skills add vercel-labs/portless --skill portless -a claude-code

2Manually: put the files in place

~/.claude/skills/portless/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/portless/

Similar

memory-leak-debugging ChromeDevTools Finds memory leaks in JavaScript and Node.js with Chrome DevTools MCP. Use it to investigate high memory use, OOM errors, and heap snapshots. Claude Code CX Codex Cursor +1 3,200installs claude-certification rohitg00 Walks you through one of four Claude certification tracks: teaches the lesson, runs the lab, checks your artifacts. Useful when you are preparing for an exam and want a tutor instead of a reading list. Claude Code CX Codex Cursor +1 3,300installs prompt-master nidhinjs This skill writes and improves prompts for a named AI tool. Use it when a rough request needs to become a ready-to-paste prompt for Claude Code, Cursor, or another supported tool. Claude Code CX Codex Cursor +1 3,900installs mem0 mem0ai Guides AI coding agents in adding persistent memory to Python and TypeScript applications using the Mem0 Platform SDK or open-source Memory class. Claude Code CX Codex Cursor +1 2,300installs plugin-creator official openai Scaffolds a Codex plugin with its manifest, optional folders, and marketplace entry. Useful when you need a local plugin ready for editing or testing. Claude Code CX Codex Cursor +1 2,500installs plannotator-compound backnotprop Reads a Plannotator plan archive, pulls out denial patterns and feedback, then builds an HTML dashboard with findings. Claude Code CX Codex Cursor +1 11,400installs setup-matt-pocock-skills Matt Pocock This skill configures a repository for Matt Pocock's engineering skills by setting its issue tracker, triage labels, and domain docs layout. Run it before using the other skills. Claude Code 1,052,851installs to-tickets Matt Pocock Turns a plan, spec, or conversation into small end-to-end tickets with explicit blockers. Use it to agree on the work breakdown before tickets go to a tracker. Claude Code 711,230installs

Questions about portless

How to install portless in Claude Code?

Run npx skills add vercel-labs/portless --skill portless -a claude-code — the skill installs to ~/.claude/skills/portless/.

What does portless do?

Replaces port numbers with stable .localhost names for local dev servers. Agent and browser see myapp.localhost; the proxy forwards to a randomly assigned port in the 4000–4999 range.

Can I install portless via npx without a global install?

No. Do not use npx or pnpm dlx for one-off execution — install globally or add to the project's devDependencies.

Source text

SKILL.md · Apache-2.0
---
name: portless
description: Set up and use portless for named local dev server URLs (e.g. https://myapp.localhost instead of http://localhost:3000). Use when integrating portless into a project, configuring dev server names, setting up the local proxy, working with .localhost domains, or troubleshooting port/proxy issues.
---

# Portless

Replace port numbers with stable, named .localhost URLs. For humans and agents.

## Why portless

- **Port conflicts**: `EADDRINUSE` when two projects default to the same port
- **Memorizing ports**: which app is on 3001 vs 8080?
- **Refreshing shows the wrong app**: stop one server, start another on the same port, stale tab shows wrong content
- **Monorepo multiplier**: every problem scales with each service in the repo
- **Agents test the wrong port**: AI agents guess or hardcode the wrong port
- **Cookie/storage clashes**: cookies on `localhost` bleed across apps; localStorage lost when ports shift
- **Hardcoded ports in config**: CORS allowlists, OAuth redirects, `.env` files break when ports change
- **Sharing URLs with teammates**: "what port is that on?" becomes a Slack question
- **Browser history is useless**: `localhost:3000` history is a mix of unrelated projects

## Installation

Install globally (recommended) or as a project dev dependency. Do NOT use `npx` or `pnpm dlx` for one-off execution.

```bash
# Global (available everywhere)
npm install -g portless

# Or per-project dev dependency
npm install -D portless
```

When installed per-project, invoke via package.json scripts or `npx portless` (since the package is local, npx will not download anything).

## Quick Start

```bash
# Install globally (or add -D to a project)
npm install -g portless

# Run your app (auto-starts the HTTPS proxy on port 443)
portless run next dev
# -> https://<project>.localhost

# Or with an explicit name
portless myapp next dev
# -> https://myapp.localhost
```

The proxy auto-starts when you run an app. You can also start it explicitly with `portless proxy start`. Auto-start reuses the configuration (port, TLS, TLDs) from the most recent proxy run, so a restart or reboot does not silently revert to defaults. Explicit env vars always take priority.

In non-interactive environments (no TTY, or `CI=1`), portless exits with a descriptive error instead of prompting. Task runners like turborepo should pre-start the proxy.

## Integration Patterns

### Zero-config (recommended)

Bare `portless` works out of the box. It runs the `"dev"` script from `package.json` through the proxy, inferring the app name from the package name, git root, or directory:

```bash
portless        # -> runs "dev" script, https://<project>.localhost
pnpm dev        # -> works without portless, plain "next dev"
```

Use an optional `portless.json` to override defaults (name, script, port):

```json
{ "name": "myapp" }
```

```bash
portless        # -> runs "dev" script, https://myapp.localhost
```

### Monorepo

One `portless.json` at the repo root. Portless discovers packages from `pnpm-workspace.yaml`, or the `"workspaces"` field in `package.json` (npm, yarn, bun):

```json
{
  "apps": {
    "apps/web": { "name": "myapp" },
    "apps/api": { "name": "api.myapp" }
  }
}
```

```bash
portless                  # from repo root: start all packages with a "dev" script
cd apps/web && portless   # start just one package
portless --script start   # run "start" instead of "dev"
```

The `apps` map is optional and only provides name overrides. Unlisted packages auto-discover with inferred names.

Without an `apps` map, hostnames follow `<package>.<project>.localhost`. The project name comes from the most common npm scope (e.g. `@myorg/web` and `@myorg/api` produce `myorg`), falling back to the workspace root directory name. If a package's short name matches the project name, it uses the bare `<project>.localhost`.

### Turborepo

For turborepo projects, use portless as the `dev` script with the real command in a separate script:

```json
{
  "scripts": { "dev": "portless", "dev:app": "next dev" },
  "portless": { "name": "myapp", "script": "dev:app" }
}
```

`pnpm dev` runs turbo, which runs `portless` in each package. Portless detects the package manager and runs `pnpm run dev:app` through the proxy.

When `portless` runs from a workspace root, it uses the existing Turbo integration to preserve task ordering when either `turbo.json` or `turbo.jsonc` is readable. Set `"turbo": false` in the root portless configuration to use direct spawning instead.

### package.json scripts

You can still use portless directly in scripts:

```json
{
  "scripts": {
    "dev": "portless run next dev"
  }
}
```

The proxy auto-starts when you run an app. Or start it explicitly: `portless proxy start`.

### Multi-app setups with subdomains

```bash
portless myapp next dev          # https://myapp.localhost
portless api.myapp pnpm start    # https://api.myapp.localhost
portless docs.myapp next dev     # https://docs.myapp.localhost
```

By default, only explicitly registered subdomains are routed (strict mode). Start the proxy with `--wildcard` to allow any subdomain of a registered route to fall back to that app (e.g. `tenant1.myapp.localhost` routes to the `myapp` app). Exact matches always take priority over wildcards. When several registered routes are parents of the host, the most specific one wins (`admin.api.myapp.localhost` routes to `api.myapp`, not `myapp`).

### Git worktrees

`portless run` automatically detects git worktrees. In a linked worktree, the branch name is prepended as a subdomain prefix so each worktree gets a unique URL:

```bash
# Main worktree (no prefix)
portless run next dev   # -> https://myapp.localhost

# Linked worktree on branch "fix-ui"
portless run next dev   # -> https://fix-ui.myapp.localhost
```

No config changes needed. Put `portless run` in `package.json` once and it works in all worktrees.

### Bypassing portless

Set `PORTLESS=0` to run the command directly without the proxy:

```bash
PORTLESS=0 pnpm dev   # Bypasses proxy, uses default port
```

When a proxied command is stopped with Ctrl+C, portless waits for its process tree to exit. A second
Ctrl+C forwards another interrupt, and remaining descendants are terminated after a short grace
period.

## How It Works

1. `portless proxy start` starts an HTTPS reverse proxy on port 443 as a background daemon. Auto-elevates with sudo on macOS/Linux; falls back to port 1355 if sudo is unavailable. Use `--no-tls` for plain HTTP on port 80. Configurable with `-p` / `--port` or the `PORTLESS_PORT` env var. The proxy also auto-starts when you run an app.
2. `portless <name> <cmd>` assigns a random free port (4000-4999) via the `PORT` env var and registers the app with the proxy
3. The browser hits `https://<name>.localhost`; the proxy forwards to the app's assigned port

Outside LAN mode, the proxy and its HTTP redirect listener bind only to the IPv4 and IPv6 loopback addresses, `127.0.0.1` and `::1`. They do not accept connections through LAN, VPN, or other network interfaces.

When portless itself returns an error (404 for an unknown host, 421 for a misdirected request, 502 when the app is unreachable, 508 for a request loop), the body depends on the `Accept` header. Browsers (`text/html`) get an HTML page. curl, `fetch`, and other clients get JSON such as `{"error":"bad_gateway","upstream":"myapp.localhost","message":"..."}`, with `error` set to `not_found`, `misdirected_request`, `bad_gateway`, or `loop_detected`. Parse that JSON instead of scraping HTML. Responses from the app itself are forwarded unchanged. Hostnames outside the portless TLDs, for example a site a DNS blocklist points at 127.0.0.1, get 421 Misdirected Request instead of the app list.

`.localhost` domains resolve to `127.0.0.1` natively in Chrome, Firefox, and Edge. Safari relies on the system DNS resolver, which may not handle `.localhost` subdomains on all configurations. Run `portless hosts sync` to add entries to `/etc/hosts` if needed.

Use `portless proxy start --tld localhost --tld test` to serve the same app names under multiple TLDs from one proxy. `PORTLESS_URL` uses the first configured TLD. When configured TLDs overlap (e.g. `example.com` and `dev.example.com`), hostnames are matched against the longest TLD first, regardless of configuration order. `PORTLESS_TLD` accepts the same comma separated list format, e.g. `PORTLESS_TLD=localhost,test`.

TLDs can be multi-segment DNS names such as `dev.example.com`, so local URLs can mirror production structure (`myapp.dev.example.com`). Each label follows DNS rules: lowercase letters, digits, interior hyphens, 63 characters per label, 253 total. Strict OAuth providers that reject `.localhost` redirect URIs accept a real domain like `https://myapp.dev.example.com/api/auth/callback/google`.

Most frameworks (Next.js, Express, Nuxt, etc.) respect the `PORT` env var automatically. For frameworks that ignore `PORT` (Vite, VitePlus, VitePress, Astro, React Router, Angular, Expo, React Native, Wrangler), portless auto-injects the correct `--port` flag and, when needed, a matching `--host` CLI flag (`--ip` for Wrangler). Injection reaches through a package script whose command starts with the framework or a known runner (`"dev": "vite"`, `"dev": "bunx vite"`). Only the framework's server commands get the flags (`dev`, `serve`, `preview`, `start`, a bare `vite`, or `vite [root]`); a command that does not serve, such as `vite build`, `vite optimize`, `vp test` or `astro check`, rejects them and is left alone. Expo connection modes (`--localhost`, `--lan`, `--tunnel`) are preserved while the assigned port is still injected. A script portless cannot classify is left alone too: a flag before the subcommand on a CLI whose flag grammar it does not track (`vp --mode dev build`). Portless also leaves a script alone when appending flags to it would not work: a compound command (`&&`, `|`, `;`), a trailing `#` comment, its own `--` option terminator, an env prefix (`NODE_ENV=production vite`), delegation to another script (`"dev": "npm run dev:vite"`), or runner flags before the script name (`bun run --bun dev`). Those keep their own port, so set it in the script yourself.

### State directory

Portless stores its state (routes, PID file, port file) in `~/.portless`. When the proxy runs under sudo, this remains the invoking user's home directory so unprivileged apps and the proxy share route registrations. Override with the `PORTLESS_STATE_DIR` environment variable.

### Environment variables

| Variable              | Description                                                                    |
| --------------------- | ------------------------------------------------------------------------------ |
| `PORTLESS_PORT`       | Override the default proxy port (default: 443 with HTTPS, 80 without)          |
| `PORTLESS_APP_PORT`   | Use a fixed port for the app (skip auto-assignment)                            |
| `PORTLESS_HTTPS`      | HTTPS on by default; set to `0` to disable (same as `--no-tls`)                |
| `PORTLESS_LAN`        | Set to `1` to always enable LAN mode (auto-detects LAN IP)                     |
| `PORTLESS_LAN_IP`     | Pin a specific LAN IP for LAN mode                                             |
| `PORTLESS_TLD`        | Use one or more TLDs, single or multi-segment (e.g. localhost,dev.example.com) |
| `PORTLESS_WILDCARD`   | Set to `1` to allow unregistered subdomains to fall back to parent             |
| `PORTLESS_SYNC_HOSTS` | Set to `0` to disable auto-sync of /etc/hosts (on by default)                  |
| `PORTLESS_TAILSCALE`  | Set to `1` to share apps on your Tailscale network (same as `--tailscale`)     |
| `PORTLESS_FUNNEL`     | Set to `1` to share apps publicly via Tailscale Funnel (same as `--funnel`)    |
| `PORTLESS_NGROK`      | Set to `1` to share apps publicly via ngrok (same as `--ngrok`)                |
| `PORTLESS_CLOUDFLARE` | Set to `1` to share apps publicly via Cloudflare (same as `--cloudflare`)      |
| `PORTLESS_STATE_DIR`  | Override the state directory                                                   |
| `PORTLESS=0`          | Bypass the proxy, run the command directly                                     |

### HTTP/2 + HTTPS

HTTPS with HTTP/2 is enabled by default (faster page loads for dev servers with many files). WebSockets work over both HTTP/1.1 (Upgrade) and HTTP/2 (RFC 8441 extended CONNECT), so dev server HMR works through the proxy. First run generates a local CA and adds it to the system trust store. After that, no prompts and no browser warnings.

```bash
portless proxy start --cert ./c.pem --key ./k.pem  # Use custom certs
portless proxy start --no-tls                       # Disable HTTPS (plain HTTP)
portless trust                                      # Add CA to trust store later
```

On Linux, `portless trust` supports Debian/Ubuntu, Arch, Fedora/RHEL/CentOS, and openSUSE (via `update-ca-certificates` or `update-ca-trust`). On Windows, it uses `certutil` to add the CA to the system trust store. On WSL, it updates both the Linux trust store and the Windows current-user Root store so Windows browsers trust portless HTTPS certificates. Linux containers on a WSL2 host use only the Linux trust store.

### LAN mode

```bash
portless proxy start --lan
portless proxy start --lan --https
portless proxy start --lan --ip 192.168.1.42
```

`--lan` explicitly binds the proxy to the IPv4 and IPv6 unspecified addresses, `0.0.0.0` and `::`, and advertises `<name>.local` hostnames over mDNS so devices on the same Wi-Fi can reach your apps. Portless auto-detects your LAN IP and follows network changes automatically, but you can pin a specific address with `--ip <address>` or the `PORTLESS_LAN_IP` environment variable. Set `PORTLESS_LAN=1` to default to LAN mode every time the proxy starts.

Portless remembers LAN mode via `proxy.lan`, so if you stop a LAN proxy and start again, it stays in LAN mode. All proxy settings (port, TLS, TLDs, LAN) are persisted and reused on auto-start unless overridden by explicit flags or env vars. Use `PORTLESS_LAN=0` for one start to switch back to `.localhost` mode. If a proxy is already running with different explicit LAN/TLS/TLD settings, portless warns and asks you to stop it first.

LAN mode depends on the system mDNS helpers that portless launches: macOS includes `dns-sd`, while Linux uses `avahi-publish-address` from `avahi-utils` (install via `sudo apt install avahi-utils` or your distro’s tooling).

- **Next.js**: add your `.local` hostnames to `allowedDevOrigins`:

  ```js
  // next.config.js
  module.exports = {
    allowedDevOrigins: ["myapp.local", "*.myapp.local"],
  };
  ```

- **Expo / React Native**: portless always injects `--port`. React Native also gets `--host 127.0.0.1`. Expo gets `--host localhost` outside LAN mode, but in LAN mode portless leaves Metro on its default LAN host behavior instead of forcing `--host` or `HOST`.

### Tailscale sharing

Share dev servers with teammates on your Tailscale network using `--tailscale`, or expose to the public internet with `--funnel`:

```bash
portless myapp --tailscale next dev
# -> https://myapp.localhost           (local)
# -> https://devbox.yourteam.ts.net    (tailnet)

portless myapp --funnel next dev
# -> https://myapp.localhost           (local)
# -> https://devbox.yourteam.ts.net    (public internet)
```

Tailscale HTTPS certificates must be enabled before `--tailscale` or `--funnel` can register HTTPS URLs. Funnel must also be enabled for the tailnet and node before `--funnel` can register the public URL. If either setting is missing, portless exits before starting the child process.

Each `--tailscale` app is root-mounted on its own Tailscale HTTPS port (443, then 8443, 8444, etc.) so no framework `basePath` configuration is needed. Set `PORTLESS_TAILSCALE=1` to share every app by default. `portless list` shows both local and tailnet URLs. Tailscale serve registrations are cleaned up when the app exits. Requires `tailscale` CLI installed and connected, with Tailscale HTTPS certificates enabled.

### ngrok sharing

Expose a dev server to the public internet with ngrok using `--ngrok`:

```bash
portless myapp --ngrok next dev
# -> https://myapp.localhost           (local)
# -> https://abc123.ngrok.app          (public internet)
```

Set `PORTLESS_NGROK=1` to enable ngrok by default when portless runs an app. `portless list` shows both local and ngrok URLs. The ngrok tunnel is cleaned up when the app exits. Requires the `ngrok` CLI to be installed and authenticated with `ngrok config add-authtoken <token>`.

### Cloudflare sharing

```bash
portless myapp --cloudflare next dev
# -> https://<random>.trycloudflare.com (public internet)
```

`--cloudflare` opens a Cloudflare quick tunnel; no Cloudflare account is needed. Set `PORTLESS_CLOUDFLARE=1` to enable it by default. The app gets `PORTLESS_CLOUDFLARE_URL`, `portless list` shows a `cloudflare:` line, and the tunnel stops when the app exits. It combines with `--ngrok`, `--tailscale` and `--funnel`. Quick tunnels do not support Server-Sent Events, allow at most 200 in-flight requests, and get a new URL every run. Requires the `cloudflared` CLI on PATH (https://developers.cloudflare.com/tunnel/downloads/).

## OS startup service

Use the service command when users want the proxy to start automatically after reboot:

```bash
portless service install
portless service install --lan
portless service install --wildcard
PORTLESS_STATE_DIR=~/.portless-lan PORTLESS_LAN=1 portless service install
portless service status
portless service uninstall
```

The service uses portless defaults unless install options or `PORTLESS_*` environment variables are provided: HTTPS on port 443 with `.localhost` names. `service install` accepts proxy options including `--port`, `--no-tls`, `--lan`, `--ip`, `--tld`, `--wildcard`, `--cert`, and `--key`. Use `--state-dir <path>` or `PORTLESS_STATE_DIR=<path>` to choose where service state and logs are written.

The chosen service configuration is written into launchd, systemd, or Task Scheduler and reused after reboot. `portless service status` reports the installed port, HTTPS mode, TLDs, LAN mode, wildcard mode, and state directory. macOS and Linux install a root-owned service so port 443 can bind at boot. Windows installs a Task Scheduler startup task that runs as SYSTEM. Installation and removal may require administrator privileges. `portless clean` automatically removes the service.

## CLI Reference

| Command                                           | Description                                                    |
| ------------------------------------------------- | -------------------------------------------------------------- |
| `portless`                                        | Run dev script through proxy                                   |
| `portless`                                        | From monorepo root: run all workspace packages                 |
| `portless --script <name>`                        | Run a specific package.json script (default: dev)              |
| `portless run [cmd] [args...]`                    | Infer name from project, run through proxy (auto-starts)       |
| `portless run --name <name> <cmd>`                | Override inferred base name (worktree prefix still applies)    |
| `portless <name> <cmd> [args...]`                 | Run app at `https://<name>.localhost` (auto-starts proxy)      |
| `portless get <name>`                             | Print URL for a service (for cross-service wiring)             |
| `portless get <name> --no-worktree`               | Print URL without worktree prefix                              |
| `portless list`                                   | Show active routes and uptime                                  |
| `portless doctor`                                 | Check proxy, routes, DNS, CA trust, and LAN prerequisites      |
| `portless trust`                                  | Add local CA to system trust store (for HTTPS)                 |
| `portless clean`                                  | Remove state, CA trust entry, and /etc/hosts block             |
| `portless prune`                                  | Kill orphaned dev server groups; `run` also does it on start   |
| `portless prune --force`                          | Kill orphans with SIGKILL instead of SIGTERM                   |
| `portless proxy start`                            | Start HTTPS proxy as a daemon (port 443, auto-elevates)        |
| `portless proxy start --no-tls`                   | Start without HTTPS (plain HTTP on port 80)                    |
| `portless proxy start --lan`                      | Start in LAN mode (mDNS `.local`, auto-follows LAN IP changes) |
| `portless proxy start -p <number>`                | Start the proxy on a custom port                               |
| `portless proxy start --tld test`                 | Use .test instead of .localhost                                |
| `portless proxy start --tld localhost --tld test` | Serve both TLDs from one proxy                                 |
| `portless proxy start --tld dev.example.com`      | Use a multi-segment TLD for production-parity URLs             |
| `portless proxy start --foreground`               | Start the proxy in foreground (for debugging)                  |
| `portless proxy start --wildcard`                 | Allow unregistered subdomains to fall back to parent route     |
| `portless proxy stop`                             | Stop the proxy                                                 |
| `portless service install`                        | Start the HTTPS proxy when the OS starts                       |
| `portless service install --lan`                  | Start the service in LAN mode                                  |
| `portless service install --wildcard`             | Persist wildcard routing in the startup service                |
| `portless service status`                         | Show service and proxy status                                  |
| `portless service uninstall`                      | Remove the startup service                                     |
| `portless alias <name> <port>`                    | Register a static route (e.g. for Docker containers)           |
| `portless alias <name> <port> --force`            | Overwrite an existing route                                    |
| `portless alias --remove <name>`                  | Remove a static route                                          |
| `portless hosts sync`                             | Reconcile routes with /etc/hosts (fixes Safari)                |
| `portless hosts clean`                            | Remove portless entries from /etc/hosts                        |
| `portless <name> --app-port <n> <cmd>`            | Use a fixed port for the app instead of auto-assignment        |
| `portless <name> --tailscale <cmd>`               | Share the app on your Tailscale network (tailnet)              |
| `portless <name> --funnel <cmd>`                  | Share the app publicly via Tailscale Funnel                    |
| `portless <name> --ngrok <cmd>`                   | Share the app publicly via ngrok                               |
| `portless <name> --cloudflare <cmd>`              | Share the app publicly via a Cloudflare quick tunnel           |
| `portless <name> --force <cmd>`                   | Kill the existing process and take over its route              |
| `portless --name <name> <cmd>`                    | Force `<name>` as app name (bypasses subcommand dispatch)      |
| `portless <name> -- <cmd> [args...]`              | Stop flag parsing; everything after `--` is passed to child    |
| `portless --help` / `-h`                          | Show help                                                      |
| `portless run --help`                             | Show help for a subcommand (also: alias, hosts, clean)         |
| `portless --version` / `-v`                       | Show version                                                   |

**Reserved names:** `run`, `get`, `alias`, `hosts`, `list`, `doctor`, `trust`, `clean`, `prune`, `proxy`, `service`, and `mcp` are subcommands and cannot be used as app names directly. Use `portless run <cmd>` to infer the name, or `portless --name <name> <cmd>` to force any name including reserved ones.

## MCP server

`portless mcp [--tools <profiles>]` starts a stdio MCP server. It defaults to `core`, which provides route listing, URL lookup, static alias management, and profile discovery. Add comma-separated `proxy`, `diagnostics`, or `all` profiles as needed. `default` aliases `core`, and `full` aliases `all`. The `tools_profiles` tool lists available profiles. Shared tool arguments are `cwd`, `extraArgs`, and `timeoutMs`; bad arguments (unknown or wrong type) and calls to a tool that is not enabled return a tool result with `isError: true` that explains the fix, so the model can correct its call; unknown arguments list the tool's own arguments. MCP does not run dev servers; start them with `portless run <cmd>` in a shell.

```json
{ "mcpServers": { "portless": { "command": "portless", "args": ["mcp"] } } }
```

## portless.json

Optional config file. Portless looks for it in the current directory.

| Field     | Type    | Default                    | Description                                              |
| --------- | ------- | -------------------------- | -------------------------------------------------------- |
| `name`    | string  | inferred from package.json | Base app name (worktree prefix still applies)            |
| `script`  | string  | `"dev"`                    | Name of a package.json script to run                     |
| `appPort` | number  | auto-assigned              | Fixed port for the child process                         |
| `proxy`   | boolean | auto-detected              | Whether to route through the proxy (`false` for tasks)   |
| `apps`    | object  |                            | Overrides for workspace packages, keyed by relative path |
| `turbo`   | boolean | `true`                     | Set `false` to use direct spawning instead of turborepo  |

Each `apps` entry has the same shape (`name`, `script`, `appPort`, `proxy`). When `apps` is present, top-level fields apply only in single-app mode.

### package.json "portless" key

Instead of a separate `portless.json`, you can add a `"portless"` key to your `package.json`. A string value is shorthand for setting the name:

```json
{ "portless": "myapp" }
```

An object supports all per-app fields (`name`, `script`, `appPort`, `proxy`):

```json
{ "portless": { "name": "myapp", "script": "dev:app" } }
```

Precedence (closest wins): CLI flags > package.json `"portless"` key > portless.json app entry > defaults.

## Troubleshooting

### Run diagnostics

Use `portless doctor` first when local routing or HTTPS behavior looks wrong. It is read-only and checks Node.js, state directory permissions, proxy liveness, route entries, hostname resolution, local CA trust, and LAN mode prerequisites.

### Proxy not running

The proxy auto-starts when you run an app with `portless <name> <cmd>`. If it doesn't start (e.g. port conflict), start it manually:

```bash
portless proxy start
```

### Port already in use

Another process is bound to the proxy port. Either stop it first, or use a different port:

```bash
portless proxy start -p 8080
```

### Framework not respecting PORT

Portless auto-injects the right `--port` flag and, when needed, a matching `--host` flag for frameworks that ignore the `PORT` env var: **Vite**, **VitePlus** (`vp`), **VitePress**, **Astro**, **React Router**, **Angular**, **Expo**, **React Native**, and **Wrangler** (which gets `--ip` instead of `--host`). SvelteKit uses Vite internally and is handled automatically. Injection reaches through a package script whose command starts with the framework or a known runner, and only for the framework's server commands (`dev`, `serve`, `preview`, `start`, or a bare `vite`) — `vite build`, `vite optimize`, `vp test` and other non-serving commands reject the flags, so they are left untouched, as is any invocation portless cannot classify (`vp --mode dev build`). It is also skipped for a compound command (`&&`, `|`, `;`), a trailing `#` comment, its own `--` option terminator, an env prefix (`NODE_ENV=production vite`), delegation to another script, and runner flags before the script name (`bun run --bun dev`) — each of those keeps its own port and the app returns 502, so set the port in the script yourself.

For other frameworks that don't read `PORT`, pass the port manually:

- **Webpack Dev Server**: use `--port $PORT`
- **Custom servers**: read `process.env.PORT` and listen on it

### Permission errors

The default ports (80 for HTTP, 443 for HTTPS) require `sudo` on macOS and Linux. Portless auto-elevates with sudo when needed. If sudo is unavailable, it falls back to port 1355 (no sudo needed). On Windows, no elevation is required.

```bash
portless proxy start --https           # Auto-elevates with sudo for port 443
portless proxy start -p 1355 --https   # No sudo needed (URLs include :1355)
portless proxy stop                    # Stop (use sudo if started with sudo)
```

### Safari can't find .localhost URLs

Safari relies on the system DNS resolver for `.localhost` subdomains, which may not resolve them on all macOS configurations. Chrome, Firefox, and Edge have built-in handling.

Fix:

```bash
portless hosts sync    # Reconcile current routes with /etc/hosts
portless hosts clean   # Remove entries later
```

Auto-syncs `/etc/hosts` for route hostnames by default. Set `PORTLESS_SYNC_HOSTS=0` to disable. If a route hostname will not resolve, the command that registered it warns and points you to `portless hosts sync`.

Manual sync reconciles portless-managed entries with current routes and rem...

© vercel-labs · Apache-2.0 · vercel-labs/portless

See also

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor