{"owner":"vercel-labs","repo":"portless","hasSkills":true,"totalSkillsCount":5,"totalTokensCount":11890,"categories":["root-instruction","anthropic-skill"],"hasMcp":false,"mcpConfig":null,"found":["AGENTS.md","packages/portless/tsconfig.json","packages/portless/package.json","skills/oauth/SKILL.md","skills/portless/SKILL.md"],"skills":{"AGENTS.md":"# Agent Rules\n\n## Package Manager\n\nUse `pnpm` for all package management commands (not npm or yarn).\n\nException: End-user install instructions should use `npm install -g` (global) or `npm install -D` (project dev dependency) since npm is universal.\n\n## Dependencies\n\nAlways check for the latest npm version when adding dependencies. Use `pnpm add <package>` (without version) to get the latest, or verify with `npm view <package> version` first.\n\n## No Emojis\n\nDo not use emojis anywhere in this repository (code, comments, output, docs).\n\n## Dashes\n\nNever use `--` or em dashes as dashes in prose, comments, or user-facing output. Rephrase the sentence instead. The only exception is CLI flags (e.g. `--port`).\n\n## Boolean Environment Variables\n\nDocument boolean env vars using only `0` and `1` in CLI help, SKILL.md, docs pages, and README. Code accepts `true`/`false` as well (and `skip` for `PORTLESS`), but these alternatives are not documented.\n\n## Docs Updates\n\nWhen a change affects how humans or agents use portless (new/changed/removed commands, flags, behavior, or config), update all of these:\n\n1. `README.md` (user-facing documentation)\n2. `skills/portless/SKILL.md` (agent skill for using portless)\n3. `packages/portless/src/cli.ts` (`--help` output)\n4. `packages/portless/src/mcp.ts` (MCP tools and `portless mcp --help` output)\n\nUse the `technical-writer` skill (`.agents/skills/technical-writer/SKILL.md`) when you write or edit prose for this repo, including docs pages, READMEs, changelog entries, commit messages, PR descriptions, and issues.\n\n## MCP Tools\n\nAny change to a CLI command, flag, output, or environment variable must update the MCP tools in `packages/portless/src/mcp.ts` in the same change. `mcp.test.ts` reads `portless --help` and each tool command's `--help`, and fails when a command or flag is neither handled by a tool nor listed as omitted.\n\n## Releasing\n\nReleases are manual, single-PR affairs. The maintainer controls the changelog voice and format.\n\nTo prepare a release:\n\n1. Create a branch (e.g. `prepare-v1.2.0`)\n2. Bump the version in `packages/portless/package.json`\n3. Write the changelog entry in `CHANGELOG.md`, wrapped in `<!-- release:start -->` and `<!-- release:end -->` markers\n4. Remove the `<!-- release:start -->` and `<!-- release:end -->` markers from the previous release entry (only the latest release should have markers)\n5. Add a matching entry to `apps/docs/content/docs/changelog.mdx`\n6. Open a PR and merge to `main`\n\nCI compares the version in `packages/portless/package.json` to what's on npm. If it differs, it builds, publishes, and creates the GitHub release automatically. The release body is extracted from the content between the markers.\n\n## Windows Debugging\n\nA remote Windows Server 2022 EC2 instance is available for debugging Windows-specific issues. It uses AWS Systems Manager (SSM) with no SSH or open ports. Commands run via `aws ssm send-command` and return stdout/stderr.\n\nAll scripts require `AWS_PROFILE=portless-debug` (or the profile must be set as default). Prefix every command with it or export it for the session:\n\n```bash\nexport AWS_PROFILE=portless-debug\n```\n\n### Prerequisites\n\nThe instance must be provisioned first (one-time, by a human):\n\n```bash\n./scripts/windows-debug/provision.sh\n```\n\nRequires: AWS CLI v2 configured with `ec2:*`, `iam:CreateRole`, `iam:AttachRolePolicy`, `ssm:SendCommand`, `ssm:GetCommandInvocation` permissions and a default VPC.\n\n### Usage\n\nStart the instance (if stopped):\n\n```bash\n./scripts/windows-debug/start.sh\n```\n\nRun a command on Windows:\n\n```bash\n./scripts/windows-debug/run.sh \"<powershell-command>\"\n```\n\nSync the current git branch and rebuild:\n\n```bash\n./scripts/windows-debug/sync.sh\n```\n\nStop the instance when done (avoids cost):\n\n```bash\n./scripts/windows-debug/stop.sh\n```\n\n### Important notes\n\n**SSM agent takes a long time to come online.** After starting or restarting the instance, the SSM agent can take 5 to 10 minutes before it accepts commands. If `run.sh` returns `InvalidInstanceId`, wait and retry. Do not assume the instance is broken; poll with increasing intervals.\n\n**PowerShell uses `;` not `&&`.** The `run.sh` wrapper executes PowerShell, which does not support `&&` as a command separator. Use `;` instead:\n\n```bash\n./scripts/windows-debug/run.sh \"cd C:\\portless; pnpm test\"\n```\n\n**OpenSSL may not be at the expected path.** The bootstrap installs OpenSSL to `C:\\Program Files\\OpenSSL-Win64\\bin`, but this can fail silently. Git bundles its own OpenSSL at `C:\\Program Files\\Git\\mingw64\\bin`. If `openssl` is not found, add Git's path:\n\n```bash\n./scripts/windows-debug/run.sh '$env:PATH = \"C:\\Program Files\\Git\\mingw64\\bin;$env:PATH\"; openssl version'\n```\n\n**SSM runs as SYSTEM.** Commands execute as the SYSTEM account, not a normal user. This affects user-specific operations (e.g., `certutil -addstore -user Root` targets SYSTEM's trust store, not a real user's). Keep this in mind when testing user-facing features.\n\n### Common Workflows\n\nRun unit tests on Windows:\n\n```bash\n./scripts/windows-debug/run.sh \"cd C:\\portless; pnpm test\"\n```\n\nRun e2e tests on Windows:\n\n```bash\n./scripts/windows-debug/run.sh \"cd C:\\portless; pnpm test:e2e\"\n```\n\nCheck bootstrap progress (first boot only):\n\n```bash\n./scripts/windows-debug/run.sh \"Get-Content C:\\bootstrap.log\"\n```\n\nThe repo lives at `C:\\portless` on the instance. Node.js 24, pnpm 11, Git, and OpenSSL are pre-installed. The `run.sh` wrapper automatically adds these tools to PATH.\n\n<!-- opensrc:start -->\n\n## Source Code Reference\n\nSource code for dependencies is available in `opensrc/` for deeper understanding of implementation details.\n\nSee `opensrc/sources.json` for the list of available packages and their versions.\n\nUse this source code when you need to understand how a package works internally, not just its types/interface.\n\n### Fetching Additional Source Code\n\nTo fetch source code for a package or repository you need to understand, run:\n\n```bash\nnpx opensrc <package>           # npm package (e.g., npx opensrc zod)\nnpx opensrc pypi:<package>      # Python package (e.g., npx opensrc pypi:requests)\nnpx opensrc crates:<package>    # Rust crate (e.g., npx opensrc crates:serde)\nnpx opensrc <owner>/<repo>      # GitHub repo (e.g., npx opensrc vercel/ai)\n```\n\n<!-- opensrc:end -->\n","packages/portless/tsconfig.json":"{\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"esModuleInterop\": true,\n    \"strict\": true,\n    \"skipLibCheck\": true,\n    \"outDir\": \"dist\",\n    \"rootDir\": \"src\",\n    \"declaration\": true,\n    \"resolveJsonModule\": true\n  },\n  \"include\": [\"src/**/*\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n","packages/portless/package.json":"{\n  \"name\": \"portless\",\n  \"version\": \"0.15.13\",\n  \"description\": \"Replace port numbers with stable, named .localhost URLs. For humans and agents.\",\n  \"type\": \"module\",\n  \"main\": \"./dist/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"import\": \"./dist/index.js\",\n      \"types\": \"./dist/index.d.ts\"\n    }\n  },\n  \"bin\": {\n    \"portless\": \"./dist/cli.js\"\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"engines\": {\n    \"node\": \">=24\"\n  },\n  \"os\": [\n    \"darwin\",\n    \"linux\",\n    \"win32\"\n  ],\n  \"scripts\": {\n    \"build\": \"tsup\",\n    \"dev\": \"tsup --watch\",\n    \"lint\": \"eslint src/\",\n    \"lint:fix\": \"eslint src/ --fix\",\n    \"prepublishOnly\": \"cp ../../README.md . && pnpm build\",\n    \"test\": \"vitest run\",\n    \"test:coverage\": \"vitest run --coverage\",\n    \"test:watch\": \"vitest\",\n    \"type-check\": \"tsc --noEmit\"\n  },\n  \"keywords\": [\n    \"local\",\n    \"development\",\n    \"proxy\",\n    \"localhost\"\n  ],\n  \"author\": \"Vercel Labs\",\n  \"license\": \"Apache-2.0\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/vercel-labs/portless.git\"\n  },\n  \"homepage\": \"https://portless.sh\",\n  \"bugs\": {\n    \"url\": \"https://github.com/vercel-labs/portless/issues\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"^24.12.4\",\n    \"@vitest/coverage-v8\": \"^4.0.18\",\n    \"eslint\": \"^9.39.2\",\n    \"tsup\": \"^8.0.1\",\n    \"typescript\": \"^5.3.3\",\n    \"vitest\": \"^4.0.18\"\n  }\n}\n","skills/oauth/SKILL.md":"---\nname: oauth\ndescription: Configure OAuth providers (Google, Apple, Microsoft, Facebook, GitHub, etc.) to work with portless local dev URLs. Use when setting up OAuth redirect URIs, fixing \"redirect_uri_mismatch\" or \"invalid redirect\" errors, configuring sign-in providers for local development, or when a provider rejects .localhost subdomains. Triggers include \"OAuth not working with portless\", \"redirect URI mismatch\", \"Google/Apple/Microsoft sign-in fails locally\", \"configure OAuth for local dev\", or any task involving OAuth callback URLs with portless domains.\n---\n\n# OAuth with Portless\n\nOAuth providers validate redirect URIs against domain rules. `.localhost` subdomains fail on most providers because they are not in the Public Suffix List or are explicitly blocked. Portless fixes this with `--tld` to serve apps on real, valid domains.\n\n## The Problem\n\nWhen portless uses the default `.localhost` TLD, OAuth providers reject redirect URIs like `http://myapp.localhost:1355/callback`:\n\n| Provider  | `localhost` | `.localhost` subdomains | Reason                         |\n| --------- | ----------- | ----------------------- | ------------------------------ |\n| Google    | Allowed     | Rejected                | Not in their bundled PSL       |\n| Apple     | Rejected    | Rejected                | No localhost at all            |\n| Microsoft | Allowed     | Allowed                 | Permissive localhost handling  |\n| Facebook  | Allowed     | Varies                  | Must register each URI exactly |\n| GitHub    | Allowed     | Allowed                 | Permissive                     |\n\nGoogle and Apple are the strictest. Microsoft and GitHub are more lenient with localhost.\n\n## The Fix\n\nUse a valid TLD so the redirect URI passes provider validation:\n\n```bash\nportless proxy start --tld dev\nportless myapp next dev\n# -> https://myapp.dev\n```\n\nAny TLD in the Public Suffix List works: `.dev`, `.app`, `.com`, `.io`, etc.\n\n### Use a domain you own\n\nBare TLDs like `.dev` mean `myapp.dev` could collide with a real domain. Use a multi-segment TLD under a domain you control, so the app name stays clean and the domain structure lives in the TLD:\n\n```bash\nportless proxy start --tld local.yourcompany.dev\nportless myapp next dev\n# -> https://myapp.local.yourcompany.dev\n```\n\nThis ensures no outbound traffic reaches something you don't own. For teams, set a wildcard DNS record (`*.local.yourcompany.dev -> 127.0.0.1`) so every developer gets resolution without `/etc/hosts`, and every developer shares the same redirect URIs in the provider console.\n\n## Provider Setup\n\n### Google\n\n1. Go to [Google Cloud Console > Credentials](https://console.cloud.google.com/apis/credentials)\n2. Create or edit an OAuth 2.0 Client ID (Web application)\n3. Add the portless domain to **Authorized JavaScript origins**: `https://myapp.dev`\n4. Add the callback to **Authorized redirect URIs**: `https://myapp.dev/api/auth/callback/google`\n\nGoogle validates domains against the Public Suffix List. The domain must end with a recognized TLD. `.localhost` subdomains fail this check; `.dev`, `.app`, `.com`, etc. all pass.\n\nHTTPS is required for `.dev` and `.app` (HSTS-preloaded). Portless handles this automatically with `--https`.\n\n### Apple\n\nApple Sign In does not allow `localhost` or IP addresses at all.\n\n1. Go to [Apple Developer > Certificates, Identifiers & Profiles](https://developer.apple.com/account/resources)\n2. Register a Services ID\n3. Configure Sign In with Apple, adding the portless domain as a **Return URL**: `https://myapp.dev/api/auth/callback/apple`\n\nThe domain must be a real, publicly-resolvable domain name. Since portless maps the domain to 127.0.0.1 locally, the browser resolves it but Apple's server-side validation may require the domain to resolve publicly too. If Apple rejects the domain, add a public DNS A record pointing to 127.0.0.1 for your dev subdomain.\n\n### Microsoft (Entra / Azure AD)\n\n1. Go to [Azure Portal > App registrations](https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps)\n2. Create or edit an app registration\n3. Under **Authentication**, add a **Web** redirect URI: `https://myapp.dev/api/auth/callback/azure-ad`\n\nMicrosoft allows `http://localhost` with any port for development. It also accepts `.localhost` subdomains in most cases. Using a custom TLD with portless is still recommended for consistency across providers.\n\n### Facebook (Meta)\n\n1. Go to [Meta for Developers > App Dashboard](https://developers.facebook.com/apps/)\n2. Under **Facebook Login > Settings**, add the portless URL to **Valid OAuth Redirect URIs**: `https://myapp.dev/api/auth/callback/facebook`\n\nFacebook requires each redirect URI to be registered exactly (no wildcards). Strict Mode (enabled by default) enforces exact matching.\n\n### GitHub\n\n1. Go to [GitHub Developer Settings > OAuth Apps](https://github.com/settings/developers)\n2. Set **Authorization callback URL**: `https://myapp.dev/api/auth/callback/github`\n\nGitHub is permissive with localhost and subdomains. A custom TLD is not strictly required but keeps the setup consistent.\n\n## Auth Library Configuration\n\n### NextAuth / Auth.js\n\nSet `NEXTAUTH_URL` to match the portless domain:\n\n```env\nNEXTAUTH_URL=https://myapp.dev\n```\n\nNextAuth uses this to construct callback URLs. Without it, callbacks may use `localhost` and cause a mismatch.\n\n### Passport.js\n\nSet the `callbackURL` in each strategy to use the portless domain:\n\n```js\nnew GoogleStrategy({\n  clientID: process.env.GOOGLE_CLIENT_ID,\n  clientSecret: process.env.GOOGLE_CLIENT_SECRET,\n  callbackURL: process.env.BASE_URL + \"/auth/google/callback\",\n});\n```\n\nSet `BASE_URL=https://myapp.dev` in your environment.\n\n### Generic / Manual\n\nRead the `PORTLESS_URL` environment variable that portless injects into the child process:\n\n```js\nconst baseUrl = process.env.PORTLESS_URL || \"http://localhost:3000\";\nconst callbackUrl = `${baseUrl}/auth/callback`;\n```\n\n## Troubleshooting\n\n### \"redirect_uri_mismatch\" or \"invalid redirect URI\"\n\nThe redirect URI sent during the OAuth flow doesn't match what's registered with the provider. Check:\n\n1. The provider's registered redirect URI matches the portless domain exactly (protocol, host, path)\n2. `NEXTAUTH_URL` or equivalent is set to the portless URL (not `localhost`)\n3. The proxy is running with the correct TLD (`portless list` to verify)\n\n### Provider requires HTTPS\n\n`.dev` and `.app` TLDs are HSTS-preloaded, so browsers force HTTPS. Start the proxy:\n\n```bash\nportless proxy start --tld dev\n```\n\nPortless defaults to HTTPS on port 443 (auto-elevates with sudo). Run `portless trust` to add the local CA to your system trust store and eliminate browser warnings.\n\n### Apple rejects the domain\n\nApple may require the domain to resolve publicly. Add a DNS A record for your dev subdomain pointing to `127.0.0.1`:\n\n```\nmyapp.local.yourcompany.dev  A  127.0.0.1\n```\n\nOr use a wildcard: `*.local.yourcompany.dev  A  127.0.0.1`.\n\n### Callback goes to wrong URL after sign-in\n\nThe auth library is constructing the callback URL from `localhost` instead of the portless domain. Set the appropriate environment variable:\n\n- **NextAuth**: `NEXTAUTH_URL=https://myapp.dev`\n- **Auth.js v5**: `AUTH_URL=https://myapp.dev`\n- **Manual**: `PORTLESS_URL` is injected automatically; use it as the base URL\n\n## Example\n\nSee [`examples/google-oauth`](../../examples/google-oauth) for a complete working example with Next.js + NextAuth + Google OAuth using `--tld dev`.\n","skills/portless/SKILL.md":"---\nname: portless\ndescription: 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.\n---\n\n# Portless\n\nReplace port numbers with stable, named .localhost URLs. For humans and agents.\n\n## Why portless\n\n- **Port conflicts**: `EADDRINUSE` when two projects default to the same port\n- **Memorizing ports**: which app is on 3001 vs 8080?\n- **Refreshing shows the wrong app**: stop one server, start another on the same port, stale tab shows wrong content\n- **Monorepo multiplier**: every problem scales with each service in the repo\n- **Agents test the wrong port**: AI agents guess or hardcode the wrong port\n- **Cookie/storage clashes**: cookies on `localhost` bleed across apps; localStorage lost when ports shift\n- **Hardcoded ports in config**: CORS allowlists, OAuth redirects, `.env` files break when ports change\n- **Sharing URLs with teammates**: \"what port is that on?\" becomes a Slack question\n- **Browser history is useless**: `localhost:3000` history is a mix of unrelated projects\n\n## Installation\n\nInstall globally (recommended) or as a project dev dependency. Do NOT use `npx` or `pnpm dlx` for one-off execution.\n\n```bash\n# Global (available everywhere)\nnpm install -g portless\n\n# Or per-project dev dependency\nnpm install -D portless\n```\n\nWhen installed per-project, invoke via package.json scripts or `npx portless` (since the package is local, npx will not download anything).\n\n## Quick Start\n\n```bash\n# Install globally (or add -D to a project)\nnpm install -g portless\n\n# Run your app (auto-starts the HTTPS proxy on port 443)\nportless run next dev\n# -> https://<project>.localhost\n\n# Or with an explicit name\nportless myapp next dev\n# -> https://myapp.localhost\n```\n\nThe 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.\n\nIn 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.\n\n## Integration Patterns\n\n### Zero-config (recommended)\n\nBare `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:\n\n```bash\nportless        # -> runs \"dev\" script, https://<project>.localhost\npnpm dev        # -> works without portless, plain \"next dev\"\n```\n\nUse an optional `portless.json` to override defaults (name, script, port):\n\n```json\n{ \"name\": \"myapp\" }\n```\n\n```bash\nportless        # -> runs \"dev\" script, https://myapp.localhost\n```\n\n### Monorepo\n\nOne `portless.json` at the repo root. Portless discovers packages from `pnpm-workspace.yaml`, or the `\"workspaces\"` field in `package.json` (npm, yarn, bun):\n\n```json\n{\n  \"apps\": {\n    \"apps/web\": { \"name\": \"myapp\" },\n    \"apps/api\": { \"name\": \"api.myapp\" }\n  }\n}\n```\n\n```bash\nportless                  # from repo root: start all packages with a \"dev\" script\ncd apps/web && portless   # start just one package\nportless --script start   # run \"start\" instead of \"dev\"\n```\n\nThe `apps` map is optional and only provides name overrides. Unlisted packages auto-discover with inferred names.\n\nWithout 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`.\n\n### Turborepo\n\nFor turborepo projects, use portless as the `dev` script with the real command in a separate script:\n\n```json\n{\n  \"scripts\": { \"dev\": \"portless\", \"dev:app\": \"next dev\" },\n  \"portless\": { \"name\": \"myapp\", \"script\": \"dev:app\" }\n}\n```\n\n`pnpm dev` runs turbo, which runs `portless` in each package. Portless detects the package manager and runs `pnpm run dev:app` through the proxy.\n\nWhen `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.\n\n### package.json scripts\n\nYou can still use portless directly in scripts:\n\n```json\n{\n  \"scripts\": {\n    \"dev\": \"portless run next dev\"\n  }\n}\n```\n\nThe proxy auto-starts when you run an app. Or start it explicitly: `portless proxy start`.\n\n### Multi-app setups with subdomains\n\n```bash\nportless myapp next dev          # https://myapp.localhost\nportless api.myapp pnpm start    # https://api.myapp.localhost\nportless docs.myapp next dev     # https://docs.myapp.localhost\n```\n\nBy 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`).\n\n### Git worktrees\n\n`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:\n\n```bash\n# Main worktree (no prefix)\nportless run next dev   # -> https://myapp.localhost\n\n# Linked worktree on branch \"fix-ui\"\nportless run next dev   # -> https://fix-ui.myapp.localhost\n```\n\nNo config changes needed. Put `portless run` in `package.json` once and it works in all worktrees.\n\n### Bypassing portless\n\nSet `PORTLESS=0` to run the command directly without the proxy:\n\n```bash\nPORTLESS=0 pnpm dev   # Bypasses proxy, uses default port\n```\n\nWhen a proxied command is stopped with Ctrl+C, portless waits for its process tree to exit. A second\nCtrl+C forwards another interrupt, and remaining descendants are terminated after a short grace\nperiod.\n\n## How It Works\n\n1. `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.\n2. `portless <name> <cmd>` assigns a random free port (4000-4999) via the `PORT` env var and registers the app with the proxy\n3. The browser hits `https://<name>.localhost`; the proxy forwards to the app's assigned port\n\nOutside 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.\n\nWith `--unprivileged` (or `PORTLESS_UNPRIVILEGED=1`) the proxy takes a port below 1024 without `sudo`: it binds the wildcard addresses, which macOS allows for non-root processes, and refuses every non-loopback peer itself. It serves plain HTTP on port 80 unless `--https` is given, because installing the CA would be a prompt; with `--https` the CA is generated but not installed, so run `portless trust` once yourself. It never writes the hosts file: automatic hosts sync is off, `PORTLESS_SYNC_HOSTS=1` together with it is refused, and Safari or a custom TLD need `portless hosts sync` run once by you. Linux needs `net.ipv4.ip_unprivileged_port_start` to cover the port; portless checks and prints the sysctl command instead of falling back to another port. The mode is remembered for auto-start and never triggers a sudo prompt, so it suits scripts, agents and managed machines.\n\nWhen 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.\n\n`.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.\n\nUse `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`.\n\nTLDs 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`.\n\nMost 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.\n\n### State directory\n\nPortless 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.\n\n### Environment variables\n\n| Variable                | Description                                                                    |\n| ----------------------- | ------------------------------------------------------------------------------ |\n| `PORTLESS_PORT`         | Override the default proxy port (default: 443 with HTTPS, 80 without)          |\n| `PORTLESS_APP_PORT`     | Use a fixed port for the app (skip auto-assignment)                            |\n| `PORTLESS_HTTPS`        | HTTPS on by default; set to `0` to disable (same as `--no-tls`)                |\n| `PORTLESS_LAN`          | Set to `1` to always enable LAN mode (auto-detects LAN IP)                     |\n| `PORTLESS_LAN_IP`       | Pin a specific LAN IP for LAN mode                                             |\n| `PORTLESS_TLD`          | Use one or more TLDs, single or multi-segment (e.g. localhost,dev.example.com) |\n| `PORTLESS_WILDCARD`     | Set to `1` to allow unregistered subdomains to fall back to parent             |\n| `PORTLESS_UNPRIVILEGED` | Set to `1` to take a port below 1024 without sudo (same as `--unprivileged`)   |\n| `PORTLESS_SYNC_HOSTS`   | Set to `0` to disable auto-sync of /etc/hosts (on by default)                  |\n| `PORTLESS_TAILSCALE`    | Set to `1` to share apps on your Tailscale network (same as `--tailscale`)     |\n| `PORTLESS_FUNNEL`       | Set to `1` to share apps publicly via Tailscale Funnel (same as `--funnel`)    |\n| `PORTLESS_NGROK`        | Set to `1` to share apps publicly via ngrok (same as `--ngrok`)                |\n| `PORTLESS_CLOUDFLARE`   | Set to `1` to share apps publicly via Cloudflare (same as `--cloudflare`)      |\n| `PORTLESS_STATE_DIR`    | Override the state directory                                                   |\n| `PORTLESS=0`            | Bypass the proxy, run the command directly                                     |\n\n### HTTP/2 + HTTPS\n\nHTTPS 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.\n\n```bash\nportless proxy start --cert ./c.pem --key ./k.pem  # Use custom certs\nportless proxy start --no-tls                       # Disable HTTPS (plain HTTP)\nportless trust                                      # Add CA to trust store later\n```\n\nOn 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.\n\n### LAN mode\n\n```bash\nportless proxy start --lan\nportless proxy start --lan --https\nportless proxy start --lan --ip 192.168.1.42\n```\n\n`--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.\n\nPortless 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.\n\nLAN 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).\n\n- **Next.js**: add your `.local` hostnames to `allowedDevOrigins`:\n\n  ```js\n  // next.config.js\n  module.exports = {\n    allowedDevOrigins: [\"myapp.local\", \"*.myapp.local\"],\n  };\n  ```\n\n- **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`.\n\n### Tailscale sharing\n\nShare dev servers with teammates on your Tailscale network using `--tailscale`, or expose to the public internet with `--funnel`:\n\n```bash\nportless myapp --tailscale next dev\n# -> https://myapp.localhost           (local)\n# -> https://devbox.yourteam.ts.net    (tailnet)\n\nportless myapp --funnel next dev\n# -> https://myapp.localhost           (local)\n# -> https://devbox.yourteam.ts.net    (public internet)\n```\n\nTailscale 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.\n\nEach `--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.\n\n### ngrok sharing\n\nExpose a dev server to the public internet with ngrok using `--ngrok`:\n\n```bash\nportless myapp --ngrok next dev\n# -> https://myapp.localhost           (local)\n# -> https://abc123.ngrok.app          (public internet)\n```\n\nSet `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>`.\n\n### Cloudflare sharing\n\n```bash\nportless myapp --cloudflare next dev\n# -> https://<random>.trycloudflare.com (public internet)\n```\n\n`--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/).\n\n## OS startup service\n\nUse the service command when users want the proxy to start automatically after reboot:\n\n```bash\nportless service install\nportless service install --lan\nportless service install --wildcard\nPORTLESS_STATE_DIR=~/.portless-lan PORTLESS_LAN=1 portless service install\nportless service status\nportless service uninstall\n```\n\nThe 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.\n\nThe 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.\n\n## CLI Reference\n\n| Command                                           | Description                                                    |\n| ------------------------------------------------- | -------------------------------------------------------------- |\n| `portless`                                        | Run dev script through proxy                                   |\n| `portless`                                        | From monorepo root: run all workspace packages                 |\n| `portless --script <name>`                        | Run a specific package.json script (default: dev)              |\n| `portless run [cmd] [args...]`                    | Infer name from project, run through proxy (auto-starts)       |\n| `portless run --name <name> <cmd>`                | Override inferred base name (worktree prefix still applies)    |\n| `portless <name> <cmd> [args...]`                 | Run app at `https://<name>.localhost` (auto-starts proxy)      |\n| `portless get <name>`                             | Print URL for a service (for cross-service wiring)             |\n| `portless get <name> --no-worktree`               | Print URL without worktree prefix                              |\n| `portless list`                                   | Show active routes and uptime                                  |\n| `portless doctor`                                 | Check proxy, routes, DNS, CA trust, and LAN prerequisites      |\n| `portless trust`                                  | Add local CA to system trust store (for HTTPS)                 |\n| `portless clean`                                  | Remove state, CA trust entry, and /etc/hosts block             |\n| `portless prune`                                  | Kill orphaned dev server groups; `run` also does it on start   |\n| `portless prune --force`                          | Kill orphans with SIGKILL instead of SIGTERM                   |\n| `portless proxy start`                            | Start HTTPS proxy as a daemon (port 443, auto-elevates)        |\n| `portless proxy start --no-tls`                   | Start without HTTPS (plain HTTP on port 80)                    |\n| `portless proxy start --lan`                      | Start in LAN mode (mDNS `.local`, auto-follows LAN IP changes) |\n| `portless proxy start -p <number>`                | Start the proxy on a custom port                               |\n| `portless proxy start --unprivileged`             | Plain HTTP on port 80 without sudo, nothing installed          |\n| `portless proxy start --tld test`                 | Use .test instead of .localhost                                |\n| `portless proxy start --tld localhost --tld test` | Serve both TLDs from one proxy                                 |\n| `portless proxy start --tld dev.example.com`      | Use a multi-segment TLD for production-parity URLs             |\n| `portless proxy start --foreground`               | Start the proxy in foreground (for debugging)                  |\n| `portless proxy start --wildcard`                 | Allow unregistered subdomains to fall back to parent route     |\n| `portless proxy stop`                             | Stop the proxy                                                 |\n| `portless service install`                        | Start the HTTPS proxy when the OS starts                       |\n| `portless service install --lan`                  | Start the service in LAN mode                                  |\n| `portless service install --wildcard`             | Persist wildcard routing in the startup service                |\n| `portless service status`                         | Show service and proxy status                                  |\n| `portless service uninstall`                      | Remove the startup service                                     |\n| `portless alias <name> <port>`                    | Register a static route (e.g. for Docker containers)           |\n| `portless alias <name> <port> --force`            | Overwrite an existing route                                    |\n| `portless alias --remove <name>`                  | Remove a static route                                          |\n| `portless hosts sync`                             | Reconcile routes with /etc/hosts (fixes Safari)                |\n| `portless hosts clean`                            | Remove portless entries from /etc/hosts                        |\n| `portless <name> --app-port <n> <cmd>`            | Use a fixed port for the app instead of auto-assignment        |\n| `portless <name> --tailscale <cmd>`               | Share the app on your Tailscale network (tailnet)              |\n| `portless <name> --funnel <cmd>`                  | Share the app publicly via Tailscale Funnel                    |\n| `portless <name> --ngrok <cmd>`                   | Share the app publicly via ngrok                               |\n| `portless <name> --cloudflare <cmd>`              | Share the app publicly via a Cloudflare quick tunnel           |\n| `portless <name> --force <cmd>`                   | Kill the existing process and take over its route              |\n| `portless --name <name> <cmd>`                    | Force `<name>` as app name (bypasses subcommand dispatch)      |\n| `portless <name> -- <cmd> [args...]`              | Stop flag parsing; everything after `--` is passed to child    |\n| `portless --help` / `-h`                          | Show help                                                      |\n| `portless run --help`                             | Show help for a subcommand (also: alias, hosts, clean)         |\n| `portless --version` / `-v`                       | Show version                                                   |\n\n**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.\n\n## MCP server\n\n`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.\n\n```json\n{ \"mcpServers\": { \"portless\": { \"command\": \"portless\", \"args\": [\"mcp\"] } } }\n```\n\n## portless.json\n\nOptional config file. Portless looks for it in the current directory.\n\n| Field     | Type    | Default                    | Description                                              |\n| --------- | ------- | -------------------------- | -------------------------------------------------------- |\n| `name`    | string  | inferred from package.json | Base app name (worktree prefix still applies)            |\n| `script`  | string  | `\"dev\"`                    | Name of a package.json script to run                     |\n| `appPort` | number  | auto-assigned              | Fixed port for the child process                         |\n| `proxy`   | boolean | auto-detected              | Whether to route through the proxy (`false` for tasks)   |\n| `apps`    | object  |                            | Overrides for workspace packages, keyed by relative path |\n| `turbo`   | boolean | `true`                     | Set `false` to use direct spawning instead of turborepo  |\n\nEach `apps` entry has the same shape (`name`, `script`, `appPort`, `proxy`). When `apps` is present, top-level fields apply only in single-app mode.\n\n### package.json \"portless\" key\n\nInstead of a separate `portless.json`, you can add a `\"portless\"` key to your `package.json`. A string value is shorthand for setting the name:\n\n```json\n{ \"portless\": \"myapp\" }\n```\n\nAn object supports all per-app fields (`name`, `script`, `appPort`, `proxy`):\n\n```json\n{ \"portless\": { \"name\": \"myapp\", \"script\": \"dev:app\" } }\n```\n\nPrecedence (closest wins): CLI flags > package.json `\"portless\"` key > portless.json app entry > defaults.\n\n## Troubleshooting\n\n### Run diagnostics\n\nUse `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.\n\n### Proxy not running\n\nThe proxy auto-starts when you run an app with `portless <name> <cmd>`. If it doesn't start (e.g. port conflict), start it manually:\n\n```bash\nportless proxy start\n```\n\n### Port already in use\n\nAnother process is bound to the proxy port. Either stop it first, or use a different port:\n\n```bash\nportless proxy start -p 8080\n```\n\n### Framework not respecting PORT\n\nPortless 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.\n\nFor other frameworks that don't read `PORT`, pass the port manually:\n\n- **Webpack Dev Server**: use `--port $PORT`\n- **Custom servers**: read `process.env.PORT` and listen on it\n\n### Permission errors\n\nThe 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.\n\n```bash\nportless proxy start --https           # Auto-elevates with sudo for port 443\nportless proxy start -p 1355 --https   # No sudo needed (URLs include :1355)\nportless proxy start --unprivileged    # Plain HTTP on port 80, no sudo, clean URLs\nportless proxy stop                    # Stop (use sudo if started with sudo)\n```\n\n### Safari can't find .localhost URLs\n\nSafari 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.\n\nFix:\n\n```bash\nportless hosts sync    # Reconcile current routes with /etc/hosts\nportless hosts clean   # Remove entries later\n```\n\nAuto-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`.\n\nManual sync reconciles portless-managed entries with current routes and removes stale entries when there are no routes. It requires a successful initial hosts-file read before writing and verifies each write. A read or verification failure follows the normal sync error path.\n\n### Browser shows certificate warning with --https\n\nThe local CA may not be trusted yet. Run:\n\n```bash\nportless trust\n```\n\nThis adds the portless local CA to your system trust store. After that, restart the browser.\n\n### Remove portless from the machine\n\n```bash\nportless clean\n```\n\nStops the proxy if needed, removes the portless CA from the trust store (when portless added it), deletes known files under state directories, and removes the portless `/etc/hosts` block. May require `sudo` on macOS/Linux. If trust-store removal fails, portless retains its CA certificate and key so a later `portless clean` can safely retry.\n\n### Proxy loop (508 Loop Detected)\n\nIf your dev server proxies requests to another portless app (e.g. Vite proxying `/api` to `api.myapp.localhost`), the proxy must rewrite the `Host` header. Without this, portless routes the request back to the original app, creating an infinite loop.\n\nFix: set `changeOrigin: true` in the proxy config (Vite, webpack-dev-server, etc.):\n\n```ts\n// vite.config.ts\nproxy: {\n  \"/api\": {\n    target: \"https://api.myapp.localhost\",\n    changeOrigin: true,\n    ws: true,\n  },\n}\n```\n\nPortless automatically sets `NODE_EXTRA_CA_CERTS` in child processes so Node.js trusts the portless CA. If you run a separate Node.js process outside portless, point it at the CA manually: `NODE_EXTRA_CA_CERTS=~/.portless/ca.pem`. Alternatively, use `--no-tls` for plain HTTP.\n\n### Tailscale not working\n\nIf `--tailscale` or `--funnel` fails:\n\n```bash\ntailscale status     # Check if connected\ntailscale up         # Connect to your tailnet\n```\n\nRequires the Tailscale CLI to be installed (https://tailscale.com/download) and on PATH.\n\n### ngrok not working\n\nIf `--ngrok` fails:\n\n```bash\nngrok version                         # Check if installed\nngrok config add-authtoken <token>    # Configure authentication\n```\n\nRequires the ngrok CLI to be installed (https://ngrok.com/download) and on PATH.\n\n### Requirements\n\n- Node.js 24+\n- macOS, Linux, or Windows\n- `openssl` (for `--https` cert generation; ships with macOS and most Linux distributions; on Windows, install via `winget install -e --id ShiningLight.OpenSSL.Dev` or use the copy bundled with Git for Windows)\n- `tailscale` CLI (optional, for `--tailscale` and `--funnel`)\n- `ngrok` CLI (optional, for `--ngrok`)\n- `cloudflared` CLI (optional, for `--cloudflare`)\n"},"files":{"AGENTS.md":"# Agent Rules\n\n## Package Manager\n\nUse `pnpm` for all package management commands (not npm or yarn).\n\nException: End-user install instructions should use `npm install -g` (global) or `npm install -D` (project dev dependency) since npm is universal.\n\n## Dependencies\n\nAlways check for the latest npm version when adding dependencies. Use `pnpm add <package>` (without version) to get the latest, or verify with `npm view <package> version` first.\n\n## No Emojis\n\nDo not use emojis anywhere in this repository (code, comments, output, docs).\n\n## Dashes\n\nNever use `--` or em dashes as dashes in prose, comments, or user-facing output. Rephrase the sentence instead. The only exception is CLI flags (e.g. `--port`).\n\n## Boolean Environment Variables\n\nDocument boolean env vars using only `0` and `1` in CLI help, SKILL.md, docs pages, and README. Code accepts `true`/`false` as well (and `skip` for `PORTLESS`), but these alternatives are not documented.\n\n## Docs Updates\n\nWhen a change affects how humans or agents use portless (new/changed/removed commands, flags, behavior, or config), update all of these:\n\n1. `README.md` (user-facing documentation)\n2. `skills/portless/SKILL.md` (agent skill for using portless)\n3. `packages/portless/src/cli.ts` (`--help` output)\n4. `packages/portless/src/mcp.ts` (MCP tools and `portless mcp --help` output)\n\nUse the `technical-writer` skill (`.agents/skills/technical-writer/SKILL.md`) when you write or edit prose for this repo, including docs pages, READMEs, changelog entries, commit messages, PR descriptions, and issues.\n\n## MCP Tools\n\nAny change to a CLI command, flag, output, or environment variable must update the MCP tools in `packages/portless/src/mcp.ts` in the same change. `mcp.test.ts` reads `portless --help` and each tool command's `--help`, and fails when a command or flag is neither handled by a tool nor listed as omitted.\n\n## Releasing\n\nReleases are manual, single-PR affairs. The maintainer controls the changelog voice and format.\n\nTo prepare a release:\n\n1. Create a branch (e.g. `prepare-v1.2.0`)\n2. Bump the version in `packages/portless/package.json`\n3. Write the changelog entry in `CHANGELOG.md`, wrapped in `<!-- release:start -->` and `<!-- release:end -->` markers\n4. Remove the `<!-- release:start -->` and `<!-- release:end -->` markers from the previous release entry (only the latest release should have markers)\n5. Add a matching entry to `apps/docs/content/docs/changelog.mdx`\n6. Open a PR and merge to `main`\n\nCI compares the version in `packages/portless/package.json` to what's on npm. If it differs, it builds, publishes, and creates the GitHub release automatically. The release body is extracted from the content between the markers.\n\n## Windows Debugging\n\nA remote Windows Server 2022 EC2 instance is available for debugging Windows-specific issues. It uses AWS Systems Manager (SSM) with no SSH or open ports. Commands run via `aws ssm send-command` and return stdout/stderr.\n\nAll scripts require `AWS_PROFILE=portless-debug` (or the profile must be set as default). Prefix every command with it or export it for the session:\n\n```bash\nexport AWS_PROFILE=portless-debug\n```\n\n### Prerequisites\n\nThe instance must be provisioned first (one-time, by a human):\n\n```bash\n./scripts/windows-debug/provision.sh\n```\n\nRequires: AWS CLI v2 configured with `ec2:*`, `iam:CreateRole`, `iam:AttachRolePolicy`, `ssm:SendCommand`, `ssm:GetCommandInvocation` permissions and a default VPC.\n\n### Usage\n\nStart the instance (if stopped):\n\n```bash\n./scripts/windows-debug/start.sh\n```\n\nRun a command on Windows:\n\n```bash\n./scripts/windows-debug/run.sh \"<powershell-command>\"\n```\n\nSync the current git branch and rebuild:\n\n```bash\n./scripts/windows-debug/sync.sh\n```\n\nStop the instance when done (avoids cost):\n\n```bash\n./scripts/windows-debug/stop.sh\n```\n\n### Important notes\n\n**SSM agent takes a long time to come online.** After starting or restarting the instance, the SSM agent can take 5 to 10 minutes before it accepts commands. If `run.sh` returns `InvalidInstanceId`, wait and retry. Do not assume the instance is broken; poll with increasing intervals.\n\n**PowerShell uses `;` not `&&`.** The `run.sh` wrapper executes PowerShell, which does not support `&&` as a command separator. Use `;` instead:\n\n```bash\n./scripts/windows-debug/run.sh \"cd C:\\portless; pnpm test\"\n```\n\n**OpenSSL may not be at the expected path.** The bootstrap installs OpenSSL to `C:\\Program Files\\OpenSSL-Win64\\bin`, but this can fail silently. Git bundles its own OpenSSL at `C:\\Program Files\\Git\\mingw64\\bin`. If `openssl` is not found, add Git's path:\n\n```bash\n./scripts/windows-debug/run.sh '$env:PATH = \"C:\\Program Files\\Git\\mingw64\\bin;$env:PATH\"; openssl version'\n```\n\n**SSM runs as SYSTEM.** Commands execute as the SYSTEM account, not a normal user. This affects user-specific operations (e.g., `certutil -addstore -user Root` targets SYSTEM's trust store, not a real user's). Keep this in mind when testing user-facing features.\n\n### Common Workflows\n\nRun unit tests on Windows:\n\n```bash\n./scripts/windows-debug/run.sh \"cd C:\\portless; pnpm test\"\n```\n\nRun e2e tests on Windows:\n\n```bash\n./scripts/windows-debug/run.sh \"cd C:\\portless; pnpm test:e2e\"\n```\n\nCheck bootstrap progress (first boot only):\n\n```bash\n./scripts/windows-debug/run.sh \"Get-Content C:\\bootstrap.log\"\n```\n\nThe repo lives at `C:\\portless` on the instance. Node.js 24, pnpm 11, Git, and OpenSSL are pre-installed. The `run.sh` wrapper automatically adds these tools to PATH.\n\n<!-- opensrc:start -->\n\n## Source Code Reference\n\nSource code for dependencies is available in `opensrc/` for deeper understanding of implementation details.\n\nSee `opensrc/sources.json` for the list of available packages and their versions.\n\nUse this source code when you need to understand how a package works internally, not just its types/interface.\n\n### Fetching Additional Source Code\n\nTo fetch source code for a package or repository you need to understand, run:\n\n```bash\nnpx opensrc <package>           # npm package (e.g., npx opensrc zod)\nnpx opensrc pypi:<package>      # Python package (e.g., npx opensrc pypi:requests)\nnpx opensrc crates:<package>    # Rust crate (e.g., npx opensrc crates:serde)\nnpx opensrc <owner>/<repo>      # GitHub repo (e.g., npx opensrc vercel/ai)\n```\n\n<!-- opensrc:end -->\n","packages/portless/tsconfig.json":"{\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"esModuleInterop\": true,\n    \"strict\": true,\n    \"skipLibCheck\": true,\n    \"outDir\": \"dist\",\n    \"rootDir\": \"src\",\n    \"declaration\": true,\n    \"resolveJsonModule\": true\n  },\n  \"include\": [\"src/**/*\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n","packages/portless/package.json":"{\n  \"name\": \"portless\",\n  \"version\": \"0.15.13\",\n  \"description\": \"Replace port numbers with stable, named .localhost URLs. For humans and agents.\",\n  \"type\": \"module\",\n  \"main\": \"./dist/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"import\": \"./dist/index.js\",\n      \"types\": \"./dist/index.d.ts\"\n    }\n  },\n  \"bin\": {\n    \"portless\": \"./dist/cli.js\"\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"engines\": {\n    \"node\": \">=24\"\n  },\n  \"os\": [\n    \"darwin\",\n    \"linux\",\n    \"win32\"\n  ],\n  \"scripts\": {\n    \"build\": \"tsup\",\n    \"dev\": \"tsup --watch\",\n    \"lint\": \"eslint src/\",\n    \"lint:fix\": \"eslint src/ --fix\",\n    \"prepublishOnly\": \"cp ../../README.md . && pnpm build\",\n    \"test\": \"vitest run\",\n    \"test:coverage\": \"vitest run --coverage\",\n    \"test:watch\": \"vitest\",\n    \"type-check\": \"tsc --noEmit\"\n  },\n  \"keywords\": [\n    \"local\",\n    \"development\",\n    \"proxy\",\n    \"localhost\"\n  ],\n  \"author\": \"Vercel Labs\",\n  \"license\": \"Apache-2.0\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/vercel-labs/portless.git\"\n  },\n  \"homepage\": \"https://portless.sh\",\n  \"bugs\": {\n    \"url\": \"https://github.com/vercel-labs/portless/issues\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"^24.12.4\",\n    \"@vitest/coverage-v8\": \"^4.0.18\",\n    \"eslint\": \"^9.39.2\",\n    \"tsup\": \"^8.0.1\",\n    \"typescript\": \"^5.3.3\",\n    \"vitest\": \"^4.0.18\"\n  }\n}\n","skills/oauth/SKILL.md":"---\nname: oauth\ndescription: Configure OAuth providers (Google, Apple, Microsoft, Facebook, GitHub, etc.) to work with portless local dev URLs. Use when setting up OAuth redirect URIs, fixing \"redirect_uri_mismatch\" or \"invalid redirect\" errors, configuring sign-in providers for local development, or when a provider rejects .localhost subdomains. Triggers include \"OAuth not working with portless\", \"redirect URI mismatch\", \"Google/Apple/Microsoft sign-in fails locally\", \"configure OAuth for local dev\", or any task involving OAuth callback URLs with portless domains.\n---\n\n# OAuth with Portless\n\nOAuth providers validate redirect URIs against domain rules. `.localhost` subdomains fail on most providers because they are not in the Public Suffix List or are explicitly blocked. Portless fixes this with `--tld` to serve apps on real, valid domains.\n\n## The Problem\n\nWhen portless uses the default `.localhost` TLD, OAuth providers reject redirect URIs like `http://myapp.localhost:1355/callback`:\n\n| Provider  | `localhost` | `.localhost` subdomains | Reason                         |\n| --------- | ----------- | ----------------------- | ------------------------------ |\n| Google    | Allowed     | Rejected                | Not in their bundled PSL       |\n| Apple     | Rejected    | Rejected                | No localhost at all            |\n| Microsoft | Allowed     | Allowed                 | Permissive localhost handling  |\n| Facebook  | Allowed     | Varies                  | Must register each URI exactly |\n| GitHub    | Allowed     | Allowed                 | Permissive                     |\n\nGoogle and Apple are the strictest. Microsoft and GitHub are more lenient with localhost.\n\n## The Fix\n\nUse a valid TLD so the redirect URI passes provider validation:\n\n```bash\nportless proxy start --tld dev\nportless myapp next dev\n# -> https://myapp.dev\n```\n\nAny TLD in the Public Suffix List works: `.dev`, `.app`, `.com`, `.io`, etc.\n\n### Use a domain you own\n\nBare TLDs like `.dev` mean `myapp.dev` could collide with a real domain. Use a multi-segment TLD under a domain you control, so the app name stays clean and the domain structure lives in the TLD:\n\n```bash\nportless proxy start --tld local.yourcompany.dev\nportless myapp next dev\n# -> https://myapp.local.yourcompany.dev\n```\n\nThis ensures no outbound traffic reaches something you don't own. For teams, set a wildcard DNS record (`*.local.yourcompany.dev -> 127.0.0.1`) so every developer gets resolution without `/etc/hosts`, and every developer shares the same redirect URIs in the provider console.\n\n## Provider Setup\n\n### Google\n\n1. Go to [Google Cloud Console > Credentials](https://console.cloud.google.com/apis/credentials)\n2. Create or edit an OAuth 2.0 Client ID (Web application)\n3. Add the portless domain to **Authorized JavaScript origins**: `https://myapp.dev`\n4. Add the callback to **Authorized redirect URIs**: `https://myapp.dev/api/auth/callback/google`\n\nGoogle validates domains against the Public Suffix List. The domain must end with a recognized TLD. `.localhost` subdomains fail this check; `.dev`, `.app`, `.com`, etc. all pass.\n\nHTTPS is required for `.dev` and `.app` (HSTS-preloaded). Portless handles this automatically with `--https`.\n\n### Apple\n\nApple Sign In does not allow `localhost` or IP addresses at all.\n\n1. Go to [Apple Developer > Certificates, Identifiers & Profiles](https://developer.apple.com/account/resources)\n2. Register a Services ID\n3. Configure Sign In with Apple, adding the portless domain as a **Return URL**: `https://myapp.dev/api/auth/callback/apple`\n\nThe domain must be a real, publicly-resolvable domain name. Since portless maps the domain to 127.0.0.1 locally, the browser resolves it but Apple's server-side validation may require the domain to resolve publicly too. If Apple rejects the domain, add a public DNS A record pointing to 127.0.0.1 for your dev subdomain.\n\n### Microsoft (Entra / Azure AD)\n\n1. Go to [Azure Portal > App registrations](https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps)\n2. Create or edit an app registration\n3. Under **Authentication**, add a **Web** redirect URI: `https://myapp.dev/api/auth/callback/azure-ad`\n\nMicrosoft allows `http://localhost` with any port for development. It also accepts `.localhost` subdomains in most cases. Using a custom TLD with portless is still recommended for consistency across providers.\n\n### Facebook (Meta)\n\n1. Go to [Meta for Developers > App Dashboard](https://developers.facebook.com/apps/)\n2. Under **Facebook Login > Settings**, add the portless URL to **Valid OAuth Redirect URIs**: `https://myapp.dev/api/auth/callback/facebook`\n\nFacebook requires each redirect URI to be registered exactly (no wildcards). Strict Mode (enabled by default) enforces exact matching.\n\n### GitHub\n\n1. Go to [GitHub Developer Settings > OAuth Apps](https://github.com/settings/developers)\n2. Set **Authorization callback URL**: `https://myapp.dev/api/auth/callback/github`\n\nGitHub is permissive with localhost and subdomains. A custom TLD is not strictly required but keeps the setup consistent.\n\n## Auth Library Configuration\n\n### NextAuth / Auth.js\n\nSet `NEXTAUTH_URL` to match the portless domain:\n\n```env\nNEXTAUTH_URL=https://myapp.dev\n```\n\nNextAuth uses this to construct callback URLs. Without it, callbacks may use `localhost` and cause a mismatch.\n\n### Passport.js\n\nSet the `callbackURL` in each strategy to use the portless domain:\n\n```js\nnew GoogleStrategy({\n  clientID: process.env.GOOGLE_CLIENT_ID,\n  clientSecret: process.env.GOOGLE_CLIENT_SECRET,\n  callbackURL: process.env.BASE_URL + \"/auth/google/callback\",\n});\n```\n\nSet `BASE_URL=https://myapp.dev` in your environment.\n\n### Generic / Manual\n\nRead the `PORTLESS_URL` environment variable that portless injects into the child process:\n\n```js\nconst baseUrl = process.env.PORTLESS_URL || \"http://localhost:3000\";\nconst callbackUrl = `${baseUrl}/auth/callback`;\n```\n\n## Troubleshooting\n\n### \"redirect_uri_mismatch\" or \"invalid redirect URI\"\n\nThe redirect URI sent during the OAuth flow doesn't match what's registered with the provider. Check:\n\n1. The provider's registered redirect URI matches the portless domain exactly (protocol, host, path)\n2. `NEXTAUTH_URL` or equivalent is set to the portless URL (not `localhost`)\n3. The proxy is running with the correct TLD (`portless list` to verify)\n\n### Provider requires HTTPS\n\n`.dev` and `.app` TLDs are HSTS-preloaded, so browsers force HTTPS. Start the proxy:\n\n```bash\nportless proxy start --tld dev\n```\n\nPortless defaults to HTTPS on port 443 (auto-elevates with sudo). Run `portless trust` to add the local CA to your system trust store and eliminate browser warnings.\n\n### Apple rejects the domain\n\nApple may require the domain to resolve publicly. Add a DNS A record for your dev subdomain pointing to `127.0.0.1`:\n\n```\nmyapp.local.yourcompany.dev  A  127.0.0.1\n```\n\nOr use a wildcard: `*.local.yourcompany.dev  A  127.0.0.1`.\n\n### Callback goes to wrong URL after sign-in\n\nThe auth library is constructing the callback URL from `localhost` instead of the portless domain. Set the appropriate environment variable:\n\n- **NextAuth**: `NEXTAUTH_URL=https://myapp.dev`\n- **Auth.js v5**: `AUTH_URL=https://myapp.dev`\n- **Manual**: `PORTLESS_URL` is injected automatically; use it as the base URL\n\n## Example\n\nSee [`examples/google-oauth`](../../examples/google-oauth) for a complete working example with Next.js + NextAuth + Google OAuth using `--tld dev`.\n","skills/portless/SKILL.md":"---\nname: portless\ndescription: 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.\n---\n\n# Portless\n\nReplace port numbers with stable, named .localhost URLs. For humans and agents.\n\n## Why portless\n\n- **Port conflicts**: `EADDRINUSE` when two projects default to the same port\n- **Memorizing ports**: which app is on 3001 vs 8080?\n- **Refreshing shows the wrong app**: stop one server, start another on the same port, stale tab shows wrong content\n- **Monorepo multiplier**: every problem scales with each service in the repo\n- **Agents test the wrong port**: AI agents guess or hardcode the wrong port\n- **Cookie/storage clashes**: cookies on `localhost` bleed across apps; localStorage lost when ports shift\n- **Hardcoded ports in config**: CORS allowlists, OAuth redirects, `.env` files break when ports change\n- **Sharing URLs with teammates**: \"what port is that on?\" becomes a Slack question\n- **Browser history is useless**: `localhost:3000` history is a mix of unrelated projects\n\n## Installation\n\nInstall globally (recommended) or as a project dev dependency. Do NOT use `npx` or `pnpm dlx` for one-off execution.\n\n```bash\n# Global (available everywhere)\nnpm install -g portless\n\n# Or per-project dev dependency\nnpm install -D portless\n```\n\nWhen installed per-project, invoke via package.json scripts or `npx portless` (since the package is local, npx will not download anything).\n\n## Quick Start\n\n```bash\n# Install globally (or add -D to a project)\nnpm install -g portless\n\n# Run your app (auto-starts the HTTPS proxy on port 443)\nportless run next dev\n# -> https://<project>.localhost\n\n# Or with an explicit name\nportless myapp next dev\n# -> https://myapp.localhost\n```\n\nThe 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.\n\nIn 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.\n\n## Integration Patterns\n\n### Zero-config (recommended)\n\nBare `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:\n\n```bash\nportless        # -> runs \"dev\" script, https://<project>.localhost\npnpm dev        # -> works without portless, plain \"next dev\"\n```\n\nUse an optional `portless.json` to override defaults (name, script, port):\n\n```json\n{ \"name\": \"myapp\" }\n```\n\n```bash\nportless        # -> runs \"dev\" script, https://myapp.localhost\n```\n\n### Monorepo\n\nOne `portless.json` at the repo root. Portless discovers packages from `pnpm-workspace.yaml`, or the `\"workspaces\"` field in `package.json` (npm, yarn, bun):\n\n```json\n{\n  \"apps\": {\n    \"apps/web\": { \"name\": \"myapp\" },\n    \"apps/api\": { \"name\": \"api.myapp\" }\n  }\n}\n```\n\n```bash\nportless                  # from repo root: start all packages with a \"dev\" script\ncd apps/web && portless   # start just one package\nportless --script start   # run \"start\" instead of \"dev\"\n```\n\nThe `apps` map is optional and only provides name overrides. Unlisted packages auto-discover with inferred names.\n\nWithout 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`.\n\n### Turborepo\n\nFor turborepo projects, use portless as the `dev` script with the real command in a separate script:\n\n```json\n{\n  \"scripts\": { \"dev\": \"portless\", \"dev:app\": \"next dev\" },\n  \"portless\": { \"name\": \"myapp\", \"script\": \"dev:app\" }\n}\n```\n\n`pnpm dev` runs turbo, which runs `portless` in each package. Portless detects the package manager and runs `pnpm run dev:app` through the proxy.\n\nWhen `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.\n\n### package.json scripts\n\nYou can still use portless directly in scripts:\n\n```json\n{\n  \"scripts\": {\n    \"dev\": \"portless run next dev\"\n  }\n}\n```\n\nThe proxy auto-starts when you run an app. Or start it explicitly: `portless proxy start`.\n\n### Multi-app setups with subdomains\n\n```bash\nportless myapp next dev          # https://myapp.localhost\nportless api.myapp pnpm start    # https://api.myapp.localhost\nportless docs.myapp next dev     # https://docs.myapp.localhost\n```\n\nBy 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`).\n\n### Git worktrees\n\n`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:\n\n```bash\n# Main worktree (no prefix)\nportless run next dev   # -> https://myapp.localhost\n\n# Linked worktree on branch \"fix-ui\"\nportless run next dev   # -> https://fix-ui.myapp.localhost\n```\n\nNo config changes needed. Put `portless run` in `package.json` once and it works in all worktrees.\n\n### Bypassing portless\n\nSet `PORTLESS=0` to run the command directly without the proxy:\n\n```bash\nPORTLESS=0 pnpm dev   # Bypasses proxy, uses default port\n```\n\nWhen a proxied command is stopped with Ctrl+C, portless waits for its process tree to exit. A second\nCtrl+C forwards another interrupt, and remaining descendants are terminated after a short grace\nperiod.\n\n## How It Works\n\n1. `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.\n2. `portless <name> <cmd>` assigns a random free port (4000-4999) via the `PORT` env var and registers the app with the proxy\n3. The browser hits `https://<name>.localhost`; the proxy forwards to the app's assigned port\n\nOutside 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.\n\nWith `--unprivileged` (or `PORTLESS_UNPRIVILEGED=1`) the proxy takes a port below 1024 without `sudo`: it binds the wildcard addresses, which macOS allows for non-root processes, and refuses every non-loopback peer itself. It serves plain HTTP on port 80 unless `--https` is given, because installing the CA would be a prompt; with `--https` the CA is generated but not installed, so run `portless trust` once yourself. It never writes the hosts file: automatic hosts sync is off, `PORTLESS_SYNC_HOSTS=1` together with it is refused, and Safari or a custom TLD need `portless hosts sync` run once by you. Linux needs `net.ipv4.ip_unprivileged_port_start` to cover the port; portless checks and prints the sysctl command instead of falling back to another port. The mode is remembered for auto-start and never triggers a sudo prompt, so it suits scripts, agents and managed machines.\n\nWhen 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.\n\n`.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.\n\nUse `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`.\n\nTLDs 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`.\n\nMost 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.\n\n### State directory\n\nPortless 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.\n\n### Environment variables\n\n| Variable                | Description                                                                    |\n| ----------------------- | ------------------------------------------------------------------------------ |\n| `PORTLESS_PORT`         | Override the default proxy port (default: 443 with HTTPS, 80 without)          |\n| `PORTLESS_APP_PORT`     | Use a fixed port for the app (skip auto-assignment)                            |\n| `PORTLESS_HTTPS`        | HTTPS on by default; set to `0` to disable (same as `--no-tls`)                |\n| `PORTLESS_LAN`          | Set to `1` to always enable LAN mode (auto-detects LAN IP)                     |\n| `PORTLESS_LAN_IP`       | Pin a specific LAN IP for LAN mode                                             |\n| `PORTLESS_TLD`          | Use one or more TLDs, single or multi-segment (e.g. localhost,dev.example.com) |\n| `PORTLESS_WILDCARD`     | Set to `1` to allow unregistered subdomains to fall back to parent             |\n| `PORTLESS_UNPRIVILEGED` | Set to `1` to take a port below 1024 without sudo (same as `--unprivileged`)   |\n| `PORTLESS_SYNC_HOSTS`   | Set to `0` to disable auto-sync of /etc/hosts (on by default)                  |\n| `PORTLESS_TAILSCALE`    | Set to `1` to share apps on your Tailscale network (same as `--tailscale`)     |\n| `PORTLESS_FUNNEL`       | Set to `1` to share apps publicly via Tailscale Funnel (same as `--funnel`)    |\n| `PORTLESS_NGROK`        | Set to `1` to share apps publicly via ngrok (same as `--ngrok`)                |\n| `PORTLESS_CLOUDFLARE`   | Set to `1` to share apps publicly via Cloudflare (same as `--cloudflare`)      |\n| `PORTLESS_STATE_DIR`    | Override the state directory                                                   |\n| `PORTLESS=0`            | Bypass the proxy, run the command directly                                     |\n\n### HTTP/2 + HTTPS\n\nHTTPS 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.\n\n```bash\nportless proxy start --cert ./c.pem --key ./k.pem  # Use custom certs\nportless proxy start --no-tls                       # Disable HTTPS (plain HTTP)\nportless trust                                      # Add CA to trust store later\n```\n\nOn 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.\n\n### LAN mode\n\n```bash\nportless proxy start --lan\nportless proxy start --lan --https\nportless proxy start --lan --ip 192.168.1.42\n```\n\n`--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.\n\nPortless 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.\n\nLAN 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).\n\n- **Next.js**: add your `.local` hostnames to `allowedDevOrigins`:\n\n  ```js\n  // next.config.js\n  module.exports = {\n    allowedDevOrigins: [\"myapp.local\", \"*.myapp.local\"],\n  };\n  ```\n\n- **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`.\n\n### Tailscale sharing\n\nShare dev servers with teammates on your Tailscale network using `--tailscale`, or expose to the public internet with `--funnel`:\n\n```bash\nportless myapp --tailscale next dev\n# -> https://myapp.localhost           (local)\n# -> https://devbox.yourteam.ts.net    (tailnet)\n\nportless myapp --funnel next dev\n# -> https://myapp.localhost           (local)\n# -> https://devbox.yourteam.ts.net    (public internet)\n```\n\nTailscale 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.\n\nEach `--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.\n\n### ngrok sharing\n\nExpose a dev server to the public internet with ngrok using `--ngrok`:\n\n```bash\nportless myapp --ngrok next dev\n# -> https://myapp.localhost           (local)\n# -> https://abc123.ngrok.app          (public internet)\n```\n\nSet `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>`.\n\n### Cloudflare sharing\n\n```bash\nportless myapp --cloudflare next dev\n# -> https://<random>.trycloudflare.com (public internet)\n```\n\n`--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/).\n\n## OS startup service\n\nUse the service command when users want the proxy to start automatically after reboot:\n\n```bash\nportless service install\nportless service install --lan\nportless service install --wildcard\nPORTLESS_STATE_DIR=~/.portless-lan PORTLESS_LAN=1 portless service install\nportless service status\nportless service uninstall\n```\n\nThe 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.\n\nThe 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.\n\n## CLI Reference\n\n| Command                                           | Description                                                    |\n| ------------------------------------------------- | -------------------------------------------------------------- |\n| `portless`                                        | Run dev script through proxy                                   |\n| `portless`                                        | From monorepo root: run all workspace packages                 |\n| `portless --script <name>`                        | Run a specific package.json script (default: dev)              |\n| `portless run [cmd] [args...]`                    | Infer name from project, run through proxy (auto-starts)       |\n| `portless run --name <name> <cmd>`                | Override inferred base name (worktree prefix still applies)    |\n| `portless <name> <cmd> [args...]`                 | Run app at `https://<name>.localhost` (auto-starts proxy)      |\n| `portless get <name>`                             | Print URL for a service (for cross-service wiring)             |\n| `portless get <name> --no-worktree`               | Print URL without worktree prefix                              |\n| `portless list`                                   | Show active routes and uptime                                  |\n| `portless doctor`                                 | Check proxy, routes, DNS, CA trust, and LAN prerequisites      |\n| `portless trust`                                  | Add local CA to system trust store (for HTTPS)                 |\n| `portless clean`                                  | Remove state, CA trust entry, and /etc/hosts block             |\n| `portless prune`                                  | Kill orphaned dev server groups; `run` also does it on start   |\n| `portless prune --force`                          | Kill orphans with SIGKILL instead of SIGTERM                   |\n| `portless proxy start`                            | Start HTTPS proxy as a daemon (port 443, auto-elevates)        |\n| `portless proxy start --no-tls`                   | Start without HTTPS (plain HTTP on port 80)                    |\n| `portless proxy start --lan`                      | Start in LAN mode (mDNS `.local`, auto-follows LAN IP changes) |\n| `portless proxy start -p <number>`                | Start the proxy on a custom port                               |\n| `portless proxy start --unprivileged`             | Plain HTTP on port 80 without sudo, nothing installed          |\n| `portless proxy start --tld test`                 | Use .test instead of .localhost                                |\n| `portless proxy start --tld localhost --tld test` | Serve both TLDs from one proxy                                 |\n| `portless proxy start --tld dev.example.com`      | Use a multi-segment TLD for production-parity URLs             |\n| `portless proxy start --foreground`               | Start the proxy in foreground (for debugging)                  |\n| `portless proxy start --wildcard`                 | Allow unregistered subdomains to fall back to parent route     |\n| `portless proxy stop`                             | Stop the proxy                                                 |\n| `portless service install`                        | Start the HTTPS proxy when the OS starts                       |\n| `portless service install --lan`                  | Start the service in LAN mode                                  |\n| `portless service install --wildcard`             | Persist wildcard routing in the startup service                |\n| `portless service status`                         | Show service and proxy status                                  |\n| `portless service uninstall`                      | Remove the startup service                                     |\n| `portless alias <name> <port>`                    | Register a static route (e.g. for Docker containers)           |\n| `portless alias <name> <port> --force`            | Overwrite an existing route                                    |\n| `portless alias --remove <name>`                  | Remove a static route                                          |\n| `portless hosts sync`                             | Reconcile routes with /etc/hosts (fixes Safari)                |\n| `portless hosts clean`                            | Remove portless entries from /etc/hosts                        |\n| `portless <name> --app-port <n> <cmd>`            | Use a fixed port for the app instead of auto-assignment        |\n| `portless <name> --tailscale <cmd>`               | Share the app on your Tailscale network (tailnet)              |\n| `portless <name> --funnel <cmd>`                  | Share the app publicly via Tailscale Funnel                    |\n| `portless <name> --ngrok <cmd>`                   | Share the app publicly via ngrok                               |\n| `portless <name> --cloudflare <cmd>`              | Share the app publicly via a Cloudflare quick tunnel           |\n| `portless <name> --force <cmd>`                   | Kill the existing process and take over its route              |\n| `portless --name <name> <cmd>`                    | Force `<name>` as app name (bypasses subcommand dispatch)      |\n| `portless <name> -- <cmd> [args...]`              | Stop flag parsing; everything after `--` is passed to child    |\n| `portless --help` / `-h`                          | Show help                                                      |\n| `portless run --help`                             | Show help for a subcommand (also: alias, hosts, clean)         |\n| `portless --version` / `-v`                       | Show version                                                   |\n\n**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.\n\n## MCP server\n\n`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.\n\n```json\n{ \"mcpServers\": { \"portless\": { \"command\": \"portless\", \"args\": [\"mcp\"] } } }\n```\n\n## portless.json\n\nOptional config file. Portless looks for it in the current directory.\n\n| Field     | Type    | Default                    | Description                                              |\n| --------- | ------- | -------------------------- | -------------------------------------------------------- |\n| `name`    | string  | inferred from package.json | Base app name (worktree prefix still applies)            |\n| `script`  | string  | `\"dev\"`                    | Name of a package.json script to run                     |\n| `appPort` | number  | auto-assigned              | Fixed port for the child process                         |\n| `proxy`   | boolean | auto-detected              | Whether to route through the proxy (`false` for tasks)   |\n| `apps`    | object  |                            | Overrides for workspace packages, keyed by relative path |\n| `turbo`   | boolean | `true`                     | Set `false` to use direct spawning instead of turborepo  |\n\nEach `apps` entry has the same shape (`name`, `script`, `appPort`, `proxy`). When `apps` is present, top-level fields apply only in single-app mode.\n\n### package.json \"portless\" key\n\nInstead of a separate `portless.json`, you can add a `\"portless\"` key to your `package.json`. A string value is shorthand for setting the name:\n\n```json\n{ \"portless\": \"myapp\" }\n```\n\nAn object supports all per-app fields (`name`, `script`, `appPort`, `proxy`):\n\n```json\n{ \"portless\": { \"name\": \"myapp\", \"script\": \"dev:app\" } }\n```\n\nPrecedence (closest wins): CLI flags > package.json `\"portless\"` key > portless.json app entry > defaults.\n\n## Troubleshooting\n\n### Run diagnostics\n\nUse `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.\n\n### Proxy not running\n\nThe proxy auto-starts when you run an app with `portless <name> <cmd>`. If it doesn't start (e.g. port conflict), start it manually:\n\n```bash\nportless proxy start\n```\n\n### Port already in use\n\nAnother process is bound to the proxy port. Either stop it first, or use a different port:\n\n```bash\nportless proxy start -p 8080\n```\n\n### Framework not respecting PORT\n\nPortless 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.\n\nFor other frameworks that don't read `PORT`, pass the port manually:\n\n- **Webpack Dev Server**: use `--port $PORT`\n- **Custom servers**: read `process.env.PORT` and listen on it\n\n### Permission errors\n\nThe 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.\n\n```bash\nportless proxy start --https           # Auto-elevates with sudo for port 443\nportless proxy start -p 1355 --https   # No sudo needed (URLs include :1355)\nportless proxy start --unprivileged    # Plain HTTP on port 80, no sudo, clean URLs\nportless proxy stop                    # Stop (use sudo if started with sudo)\n```\n\n### Safari can't find .localhost URLs\n\nSafari 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.\n\nFix:\n\n```bash\nportless hosts sync    # Reconcile current routes with /etc/hosts\nportless hosts clean   # Remove entries later\n```\n\nAuto-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`.\n\nManual sync reconciles portless-managed entries with current routes and removes stale entries when there are no routes. It requires a successful initial hosts-file read before writing and verifies each write. A read or verification failure follows the normal sync error path.\n\n### Browser shows certificate warning with --https\n\nThe local CA may not be trusted yet. Run:\n\n```bash\nportless trust\n```\n\nThis adds the portless local CA to your system trust store. After that, restart the browser.\n\n### Remove portless from the machine\n\n```bash\nportless clean\n```\n\nStops the proxy if needed, removes the portless CA from the trust store (when portless added it), deletes known files under state directories, and removes the portless `/etc/hosts` block. May require `sudo` on macOS/Linux. If trust-store removal fails, portless retains its CA certificate and key so a later `portless clean` can safely retry.\n\n### Proxy loop (508 Loop Detected)\n\nIf your dev server proxies requests to another portless app (e.g. Vite proxying `/api` to `api.myapp.localhost`), the proxy must rewrite the `Host` header. Without this, portless routes the request back to the original app, creating an infinite loop.\n\nFix: set `changeOrigin: true` in the proxy config (Vite, webpack-dev-server, etc.):\n\n```ts\n// vite.config.ts\nproxy: {\n  \"/api\": {\n    target: \"https://api.myapp.localhost\",\n    changeOrigin: true,\n    ws: true,\n  },\n}\n```\n\nPortless automatically sets `NODE_EXTRA_CA_CERTS` in child processes so Node.js trusts the portless CA. If you run a separate Node.js process outside portless, point it at the CA manually: `NODE_EXTRA_CA_CERTS=~/.portless/ca.pem`. Alternatively, use `--no-tls` for plain HTTP.\n\n### Tailscale not working\n\nIf `--tailscale` or `--funnel` fails:\n\n```bash\ntailscale status     # Check if connected\ntailscale up         # Connect to your tailnet\n```\n\nRequires the Tailscale CLI to be installed (https://tailscale.com/download) and on PATH.\n\n### ngrok not working\n\nIf `--ngrok` fails:\n\n```bash\nngrok version                         # Check if installed\nngrok config add-authtoken <token>    # Configure authentication\n```\n\nRequires the ngrok CLI to be installed (https://ngrok.com/download) and on PATH.\n\n### Requirements\n\n- Node.js 24+\n- macOS, Linux, or Windows\n- `openssl` (for `--https` cert generation; ships with macOS and most Linux distributions; on Windows, install via `winget install -e --id ShiningLight.OpenSSL.Dev` or use the copy bundled with Git for Windows)\n- `tailscale` CLI (optional, for `--tailscale` and `--funnel`)\n- `ngrok` CLI (optional, for `--ngrok`)\n- `cloudflared` CLI (optional, for `--cloudflare`)\n"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/portless/HEAD/AGENTS.md","title":"AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# Agent Rules\n\n## Package Manager\n\nUse `pnpm` for all package management commands (not npm or yarn).\n\nException: End-user install instructions should use `npm install -g` (global) or `npm install -D` (project dev dependency) since npm is universal.\n\n## Dependencies\n\nAlways check for the latest npm version when adding dependencies. Use `pnpm add <package>` (without version) to get the latest, or verify with `npm view <package> version` first.\n\n## No Emojis\n\nDo not use emojis anywhere in this repository (code, comments, output, docs).\n\n## Dashes\n\nNever use `--` or em dashes as dashes in prose, comments, or user-facing output. Rephrase the sentence instead. The only exception is CLI flags (e.g. `--port`).\n\n## Boolean Environment Variables\n\nDocument boolean env vars using only `0` and `1` in CLI help, SKILL.md, docs pages, and README. Code accepts `true`/`false` as well (and `skip` for `PORTLESS`), but these alternatives are not documented.\n\n## Docs Updates\n\nWhen a change affects how humans or agents use portless (new/changed/removed commands, flags, behavior, or config), update all of these:\n\n1. `README.md` (user-facing documentation)\n2. `skills/portless/SKILL.md` (agent skill for using portless)\n3. `packages/portless/src/cli.ts` (`--help` output)\n4. `packages/portless/src/mcp.ts` (MCP tools and `portless mcp --help` output)\n\nUse the `technical-writer` skill (`.agents/skills/technical-writer/SKILL.md`) when you write or edit prose for this repo, including docs pages, READMEs, changelog entries, commit messages, PR descriptions, and issues.\n\n## MCP Tools\n\nAny change to a CLI command, flag, output, or environment variable must update the MCP tools in `packages/portless/src/mcp.ts` in the same change. `mcp.test.ts` reads `portless --help` and each tool command's `--help`, and fails when a command or flag is neither handled by a tool nor listed as omitted.\n\n## Releasing\n\nReleases are manual, single-PR affairs. The maintainer controls the changelog voice and format.\n\nTo prepare a release:\n\n1. Create a branch (e.g. `prepare-v1.2.0`)\n2. Bump the version in `packages/portless/package.json`\n3. Write the changelog entry in `CHANGELOG.md`, wrapped in `<!-- release:start -->` and `<!-- release:end -->` markers\n4. Remove the `<!-- release:start -->` and `<!-- release:end -->` markers from the previous release entry (only the latest release should have markers)\n5. Add a matching entry to `apps/docs/content/docs/changelog.mdx`\n6. Open a PR and merge to `main`\n\nCI compares the version in `packages/portless/package.json` to what's on npm. If it differs, it builds, publishes, and creates the GitHub release automatically. The release body is extracted from the content between the markers.\n\n## Windows Debugging\n\nA remote Windows Server 2022 EC2 instance is available for debugging Windows-specific issues. It uses AWS Systems Manager (SSM) with no SSH or open ports. Commands run via `aws ssm send-command` and return stdout/stderr.\n\nAll scripts require `AWS_PROFILE=portless-debug` (or the profile must be set as default). Prefix every command with it or export it for the session:\n\n```bash\nexport AWS_PROFILE=portless-debug\n```\n\n### Prerequisites\n\nThe instance must be provisioned first (one-time, by a human):\n\n```bash\n./scripts/windows-debug/provision.sh\n```\n\nRequires: AWS CLI v2 configured with `ec2:*`, `iam:CreateRole`, `iam:AttachRolePolicy`, `ssm:SendCommand`, `ssm:GetCommandInvocation` permissions and a default VPC.\n\n### Usage\n\nStart the instance (if stopped):\n\n```bash\n./scripts/windows-debug/start.sh\n```\n\nRun a command on Windows:\n\n```bash\n./scripts/windows-debug/run.sh \"<powershell-command>\"\n```\n\nSync the current git branch and rebuild:\n\n```bash\n./scripts/windows-debug/sync.sh\n```\n\nStop the instance when done (avoids cost):\n\n```bash\n./scripts/windows-debug/stop.sh\n```\n\n### Important notes\n\n**SSM agent takes a long time to come online.** After starting or restarting the instance, the SSM agent can take 5 to 10 minutes before it accepts commands. If `run.sh` returns `InvalidInstanceId`, wait and retry. Do not assume the instance is broken; poll with increasing intervals.\n\n**PowerShell uses `;` not `&&`.** The `run.sh` wrapper executes PowerShell, which does not support `&&` as a command separator. Use `;` instead:\n\n```bash\n./scripts/windows-debug/run.sh \"cd C:\\portless; pnpm test\"\n```\n\n**OpenSSL may not be at the expected path.** The bootstrap installs OpenSSL to `C:\\Program Files\\OpenSSL-Win64\\bin`, but this can fail silently. Git bundles its own OpenSSL at `C:\\Program Files\\Git\\mingw64\\bin`. If `openssl` is not found, add Git's path:\n\n```bash\n./scripts/windows-debug/run.sh '$env:PATH = \"C:\\Program Files\\Git\\mingw64\\bin;$env:PATH\"; openssl version'\n```\n\n**SSM runs as SYSTEM.** Commands execute as the SYSTEM account, not a normal user. This affects user-specific operations (e.g., `certutil -addstore -user Root` targets SYSTEM's trust store, not a real user's). Keep this in mind when testing user-facing features.\n\n### Common Workflows\n\nRun unit tests on Windows:\n\n```bash\n./scripts/windows-debug/run.sh \"cd C:\\portless; pnpm test\"\n```\n\nRun e2e tests on Windows:\n\n```bash\n./scripts/windows-debug/run.sh \"cd C:\\portless; pnpm test:e2e\"\n```\n\nCheck bootstrap progress (first boot only):\n\n```bash\n./scripts/windows-debug/run.sh \"Get-Content C:\\bootstrap.log\"\n```\n\nThe repo lives at `C:\\portless` on the instance. Node.js 24, pnpm 11, Git, and OpenSSL are pre-installed. The `run.sh` wrapper automatically adds these tools to PATH.\n\n<!-- opensrc:start -->\n\n## Source Code Reference\n\nSource code for dependencies is available in `opensrc/` for deeper understanding of implementation details.\n\nSee `opensrc/sources.json` for the list of available packages and their versions.\n\nUse this source code when you need to understand how a package works internally, not just its types/interface.\n\n### Fetching Additional Source Code\n\nTo fetch source code for a package or repository you need to understand, run:\n\n```bash\nnpx opensrc <package>           # npm package (e.g., npx opensrc zod)\nnpx opensrc pypi:<package>      # Python package (e.g., npx opensrc pypi:requests)\nnpx opensrc crates:<package>    # Rust crate (e.g., npx opensrc crates:serde)\nnpx opensrc <owner>/<repo>      # GitHub repo (e.g., npx opensrc vercel/ai)\n```\n\n<!-- opensrc:end -->\n","isInternal":false,"tokens":1569,"sizeBytes":6307},{"name":"package.json","path":"packages/portless/package.json","rawUrl":"https://raw.githubusercontent.com/vercel-labs/portless/HEAD/packages/portless/package.json","title":"portless","category":"anthropic-skill","format":"json","content":"{\n  \"name\": \"portless\",\n  \"version\": \"0.15.13\",\n  \"description\": \"Replace port numbers with stable, named .localhost URLs. For humans and agents.\",\n  \"type\": \"module\",\n  \"main\": \"./dist/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"import\": \"./dist/index.js\",\n      \"types\": \"./dist/index.d.ts\"\n    }\n  },\n  \"bin\": {\n    \"portless\": \"./dist/cli.js\"\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"engines\": {\n    \"node\": \">=24\"\n  },\n  \"os\": [\n    \"darwin\",\n    \"linux\",\n    \"win32\"\n  ],\n  \"scripts\": {\n    \"build\": \"tsup\",\n    \"dev\": \"tsup --watch\",\n    \"lint\": \"eslint src/\",\n    \"lint:fix\": \"eslint src/ --fix\",\n    \"prepublishOnly\": \"cp ../../README.md . && pnpm build\",\n    \"test\": \"vitest run\",\n    \"test:coverage\": \"vitest run --coverage\",\n    \"test:watch\": \"vitest\",\n    \"type-check\": \"tsc --noEmit\"\n  },\n  \"keywords\": [\n    \"local\",\n    \"development\",\n    \"proxy\",\n    \"localhost\"\n  ],\n  \"author\": \"Vercel Labs\",\n  \"license\": \"Apache-2.0\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/vercel-labs/portless.git\"\n  },\n  \"homepage\": \"https://portless.sh\",\n  \"bugs\": {\n    \"url\": \"https://github.com/vercel-labs/portless/issues\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"^24.12.4\",\n    \"@vitest/coverage-v8\": \"^4.0.18\",\n    \"eslint\": \"^9.39.2\",\n    \"tsup\": \"^8.0.1\",\n    \"typescript\": \"^5.3.3\",\n    \"vitest\": \"^4.0.18\"\n  }\n}\n","frontmatter":{"name":"portless","version":"0.15.13","description":"Replace port numbers with stable, named .localhost URLs. For humans and agents.","type":"module","main":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"import":"./dist/index.js","types":"./dist/index.d.ts"}},"bin":{"portless":"./dist/cli.js"},"files":["dist"],"engines":{"node":">=24"},"os":["darwin","linux","win32"],"scripts":{"build":"tsup","dev":"tsup --watch","lint":"eslint src/","lint:fix":"eslint src/ --fix","prepublishOnly":"cp ../../README.md . && pnpm build","test":"vitest run","test:coverage":"vitest run --coverage","test:watch":"vitest","type-check":"tsc --noEmit"},"keywords":["local","development","proxy","localhost"],"author":"Vercel Labs","license":"Apache-2.0","repository":{"type":"git","url":"https://github.com/vercel-labs/portless.git"},"homepage":"https://portless.sh","bugs":{"url":"https://github.com/vercel-labs/portless/issues"},"devDependencies":{"@types/node":"^24.12.4","@vitest/coverage-v8":"^4.0.18","eslint":"^9.39.2","tsup":"^8.0.1","typescript":"^5.3.3","vitest":"^4.0.18"}},"isInternal":false,"tokens":466,"sizeBytes":1377},{"name":"tsconfig.json","path":"packages/portless/tsconfig.json","rawUrl":"https://raw.githubusercontent.com/vercel-labs/portless/HEAD/packages/portless/tsconfig.json","title":"Portless Skill","category":"anthropic-skill","format":"json","content":"{\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"esModuleInterop\": true,\n    \"strict\": true,\n    \"skipLibCheck\": true,\n    \"outDir\": \"dist\",\n    \"rootDir\": \"src\",\n    \"declaration\": true,\n    \"resolveJsonModule\": true\n  },\n  \"include\": [\"src/**/*\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n","frontmatter":{"compilerOptions":{"target":"ES2022","module":"ESNext","moduleResolution":"bundler","esModuleInterop":true,"strict":true,"skipLibCheck":true,"outDir":"dist","rootDir":"src","declaration":true,"resolveJsonModule":true},"include":["src/**/*"],"exclude":["node_modules","dist"]},"isInternal":false,"tokens":107,"sizeBytes":354},{"name":"SKILL.md","path":"skills/oauth/SKILL.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/portless/HEAD/skills/oauth/SKILL.md","title":"oauth","category":"anthropic-skill","format":"markdown","content":"---\nname: oauth\ndescription: Configure OAuth providers (Google, Apple, Microsoft, Facebook, GitHub, etc.) to work with portless local dev URLs. Use when setting up OAuth redirect URIs, fixing \"redirect_uri_mismatch\" or \"invalid redirect\" errors, configuring sign-in providers for local development, or when a provider rejects .localhost subdomains. Triggers include \"OAuth not working with portless\", \"redirect URI mismatch\", \"Google/Apple/Microsoft sign-in fails locally\", \"configure OAuth for local dev\", or any task involving OAuth callback URLs with portless domains.\n---\n\n# OAuth with Portless\n\nOAuth providers validate redirect URIs against domain rules. `.localhost` subdomains fail on most providers because they are not in the Public Suffix List or are explicitly blocked. Portless fixes this with `--tld` to serve apps on real, valid domains.\n\n## The Problem\n\nWhen portless uses the default `.localhost` TLD, OAuth providers reject redirect URIs like `http://myapp.localhost:1355/callback`:\n\n| Provider  | `localhost` | `.localhost` subdomains | Reason                         |\n| --------- | ----------- | ----------------------- | ------------------------------ |\n| Google    | Allowed     | Rejected                | Not in their bundled PSL       |\n| Apple     | Rejected    | Rejected                | No localhost at all            |\n| Microsoft | Allowed     | Allowed                 | Permissive localhost handling  |\n| Facebook  | Allowed     | Varies                  | Must register each URI exactly |\n| GitHub    | Allowed     | Allowed                 | Permissive                     |\n\nGoogle and Apple are the strictest. Microsoft and GitHub are more lenient with localhost.\n\n## The Fix\n\nUse a valid TLD so the redirect URI passes provider validation:\n\n```bash\nportless proxy start --tld dev\nportless myapp next dev\n# -> https://myapp.dev\n```\n\nAny TLD in the Public Suffix List works: `.dev`, `.app`, `.com`, `.io`, etc.\n\n### Use a domain you own\n\nBare TLDs like `.dev` mean `myapp.dev` could collide with a real domain. Use a multi-segment TLD under a domain you control, so the app name stays clean and the domain structure lives in the TLD:\n\n```bash\nportless proxy start --tld local.yourcompany.dev\nportless myapp next dev\n# -> https://myapp.local.yourcompany.dev\n```\n\nThis ensures no outbound traffic reaches something you don't own. For teams, set a wildcard DNS record (`*.local.yourcompany.dev -> 127.0.0.1`) so every developer gets resolution without `/etc/hosts`, and every developer shares the same redirect URIs in the provider console.\n\n## Provider Setup\n\n### Google\n\n1. Go to [Google Cloud Console > Credentials](https://console.cloud.google.com/apis/credentials)\n2. Create or edit an OAuth 2.0 Client ID (Web application)\n3. Add the portless domain to **Authorized JavaScript origins**: `https://myapp.dev`\n4. Add the callback to **Authorized redirect URIs**: `https://myapp.dev/api/auth/callback/google`\n\nGoogle validates domains against the Public Suffix List. The domain must end with a recognized TLD. `.localhost` subdomains fail this check; `.dev`, `.app`, `.com`, etc. all pass.\n\nHTTPS is required for `.dev` and `.app` (HSTS-preloaded). Portless handles this automatically with `--https`.\n\n### Apple\n\nApple Sign In does not allow `localhost` or IP addresses at all.\n\n1. Go to [Apple Developer > Certificates, Identifiers & Profiles](https://developer.apple.com/account/resources)\n2. Register a Services ID\n3. Configure Sign In with Apple, adding the portless domain as a **Return URL**: `https://myapp.dev/api/auth/callback/apple`\n\nThe domain must be a real, publicly-resolvable domain name. Since portless maps the domain to 127.0.0.1 locally, the browser resolves it but Apple's server-side validation may require the domain to resolve publicly too. If Apple rejects the domain, add a public DNS A record pointing to 127.0.0.1 for your dev subdomain.\n\n### Microsoft (Entra / Azure AD)\n\n1. Go to [Azure Portal > App registrations](https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps)\n2. Create or edit an app registration\n3. Under **Authentication**, add a **Web** redirect URI: `https://myapp.dev/api/auth/callback/azure-ad`\n\nMicrosoft allows `http://localhost` with any port for development. It also accepts `.localhost` subdomains in most cases. Using a custom TLD with portless is still recommended for consistency across providers.\n\n### Facebook (Meta)\n\n1. Go to [Meta for Developers > App Dashboard](https://developers.facebook.com/apps/)\n2. Under **Facebook Login > Settings**, add the portless URL to **Valid OAuth Redirect URIs**: `https://myapp.dev/api/auth/callback/facebook`\n\nFacebook requires each redirect URI to be registered exactly (no wildcards). Strict Mode (enabled by default) enforces exact matching.\n\n### GitHub\n\n1. Go to [GitHub Developer Settings > OAuth Apps](https://github.com/settings/developers)\n2. Set **Authorization callback URL**: `https://myapp.dev/api/auth/callback/github`\n\nGitHub is permissive with localhost and subdomains. A custom TLD is not strictly required but keeps the setup consistent.\n\n## Auth Library Configuration\n\n### NextAuth / Auth.js\n\nSet `NEXTAUTH_URL` to match the portless domain:\n\n```env\nNEXTAUTH_URL=https://myapp.dev\n```\n\nNextAuth uses this to construct callback URLs. Without it, callbacks may use `localhost` and cause a mismatch.\n\n### Passport.js\n\nSet the `callbackURL` in each strategy to use the portless domain:\n\n```js\nnew GoogleStrategy({\n  clientID: process.env.GOOGLE_CLIENT_ID,\n  clientSecret: process.env.GOOGLE_CLIENT_SECRET,\n  callbackURL: process.env.BASE_URL + \"/auth/google/callback\",\n});\n```\n\nSet `BASE_URL=https://myapp.dev` in your environment.\n\n### Generic / Manual\n\nRead the `PORTLESS_URL` environment variable that portless injects into the child process:\n\n```js\nconst baseUrl = process.env.PORTLESS_URL || \"http://localhost:3000\";\nconst callbackUrl = `${baseUrl}/auth/callback`;\n```\n\n## Troubleshooting\n\n### \"redirect_uri_mismatch\" or \"invalid redirect URI\"\n\nThe redirect URI sent during the OAuth flow doesn't match what's registered with the provider. Check:\n\n1. The provider's registered redirect URI matches the portless domain exactly (protocol, host, path)\n2. `NEXTAUTH_URL` or equivalent is set to the portless URL (not `localhost`)\n3. The proxy is running with the correct TLD (`portless list` to verify)\n\n### Provider requires HTTPS\n\n`.dev` and `.app` TLDs are HSTS-preloaded, so browsers force HTTPS. Start the proxy:\n\n```bash\nportless proxy start --tld dev\n```\n\nPortless defaults to HTTPS on port 443 (auto-elevates with sudo). Run `portless trust` to add the local CA to your system trust store and eliminate browser warnings.\n\n### Apple rejects the domain\n\nApple may require the domain to resolve publicly. Add a DNS A record for your dev subdomain pointing to `127.0.0.1`:\n\n```\nmyapp.local.yourcompany.dev  A  127.0.0.1\n```\n\nOr use a wildcard: `*.local.yourcompany.dev  A  127.0.0.1`.\n\n### Callback goes to wrong URL after sign-in\n\nThe auth library is constructing the callback URL from `localhost` instead of the portless domain. Set the appropriate environment variable:\n\n- **NextAuth**: `NEXTAUTH_URL=https://myapp.dev`\n- **Auth.js v5**: `AUTH_URL=https://myapp.dev`\n- **Manual**: `PORTLESS_URL` is injected automatically; use it as the base URL\n\n## Example\n\nSee [`examples/google-oauth`](../../examples/google-oauth) for a complete working example with Next.js + NextAuth + Google OAuth using `--tld dev`.\n","frontmatter":{"name":"oauth","description":"Configure OAuth providers (Google, Apple, Microsoft, Facebook, GitHub, etc.) to work with portless local dev URLs. Use when setting up OAuth redirect URIs, fixing \"redirect_uri_mismatch\" or \"invalid redirect\" errors, configuring sign-in providers for local development, or when a provider rejects .localhost subdomains. Triggers include \"OAuth not working with portless\", \"redirect URI mismatch\", \"Google/Apple/Microsoft sign-in fails locally\", \"configure OAuth for local dev\", or any task involving OAuth callback URLs with portless domains."},"isInternal":false,"tokens":1733,"sizeBytes":7465},{"name":"SKILL.md","path":"skills/portless/SKILL.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/portless/HEAD/skills/portless/SKILL.md","title":"portless","category":"anthropic-skill","format":"markdown","content":"---\nname: portless\ndescription: 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.\n---\n\n# Portless\n\nReplace port numbers with stable, named .localhost URLs. For humans and agents.\n\n## Why portless\n\n- **Port conflicts**: `EADDRINUSE` when two projects default to the same port\n- **Memorizing ports**: which app is on 3001 vs 8080?\n- **Refreshing shows the wrong app**: stop one server, start another on the same port, stale tab shows wrong content\n- **Monorepo multiplier**: every problem scales with each service in the repo\n- **Agents test the wrong port**: AI agents guess or hardcode the wrong port\n- **Cookie/storage clashes**: cookies on `localhost` bleed across apps; localStorage lost when ports shift\n- **Hardcoded ports in config**: CORS allowlists, OAuth redirects, `.env` files break when ports change\n- **Sharing URLs with teammates**: \"what port is that on?\" becomes a Slack question\n- **Browser history is useless**: `localhost:3000` history is a mix of unrelated projects\n\n## Installation\n\nInstall globally (recommended) or as a project dev dependency. Do NOT use `npx` or `pnpm dlx` for one-off execution.\n\n```bash\n# Global (available everywhere)\nnpm install -g portless\n\n# Or per-project dev dependency\nnpm install -D portless\n```\n\nWhen installed per-project, invoke via package.json scripts or `npx portless` (since the package is local, npx will not download anything).\n\n## Quick Start\n\n```bash\n# Install globally (or add -D to a project)\nnpm install -g portless\n\n# Run your app (auto-starts the HTTPS proxy on port 443)\nportless run next dev\n# -> https://<project>.localhost\n\n# Or with an explicit name\nportless myapp next dev\n# -> https://myapp.localhost\n```\n\nThe 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.\n\nIn 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.\n\n## Integration Patterns\n\n### Zero-config (recommended)\n\nBare `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:\n\n```bash\nportless        # -> runs \"dev\" script, https://<project>.localhost\npnpm dev        # -> works without portless, plain \"next dev\"\n```\n\nUse an optional `portless.json` to override defaults (name, script, port):\n\n```json\n{ \"name\": \"myapp\" }\n```\n\n```bash\nportless        # -> runs \"dev\" script, https://myapp.localhost\n```\n\n### Monorepo\n\nOne `portless.json` at the repo root. Portless discovers packages from `pnpm-workspace.yaml`, or the `\"workspaces\"` field in `package.json` (npm, yarn, bun):\n\n```json\n{\n  \"apps\": {\n    \"apps/web\": { \"name\": \"myapp\" },\n    \"apps/api\": { \"name\": \"api.myapp\" }\n  }\n}\n```\n\n```bash\nportless                  # from repo root: start all packages with a \"dev\" script\ncd apps/web && portless   # start just one package\nportless --script start   # run \"start\" instead of \"dev\"\n```\n\nThe `apps` map is optional and only provides name overrides. Unlisted packages auto-discover with inferred names.\n\nWithout 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`.\n\n### Turborepo\n\nFor turborepo projects, use portless as the `dev` script with the real command in a separate script:\n\n```json\n{\n  \"scripts\": { \"dev\": \"portless\", \"dev:app\": \"next dev\" },\n  \"portless\": { \"name\": \"myapp\", \"script\": \"dev:app\" }\n}\n```\n\n`pnpm dev` runs turbo, which runs `portless` in each package. Portless detects the package manager and runs `pnpm run dev:app` through the proxy.\n\nWhen `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.\n\n### package.json scripts\n\nYou can still use portless directly in scripts:\n\n```json\n{\n  \"scripts\": {\n    \"dev\": \"portless run next dev\"\n  }\n}\n```\n\nThe proxy auto-starts when you run an app. Or start it explicitly: `portless proxy start`.\n\n### Multi-app setups with subdomains\n\n```bash\nportless myapp next dev          # https://myapp.localhost\nportless api.myapp pnpm start    # https://api.myapp.localhost\nportless docs.myapp next dev     # https://docs.myapp.localhost\n```\n\nBy 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`).\n\n### Git worktrees\n\n`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:\n\n```bash\n# Main worktree (no prefix)\nportless run next dev   # -> https://myapp.localhost\n\n# Linked worktree on branch \"fix-ui\"\nportless run next dev   # -> https://fix-ui.myapp.localhost\n```\n\nNo config changes needed. Put `portless run` in `package.json` once and it works in all worktrees.\n\n### Bypassing portless\n\nSet `PORTLESS=0` to run the command directly without the proxy:\n\n```bash\nPORTLESS=0 pnpm dev   # Bypasses proxy, uses default port\n```\n\nWhen a proxied command is stopped with Ctrl+C, portless waits for its process tree to exit. A second\nCtrl+C forwards another interrupt, and remaining descendants are terminated after a short grace\nperiod.\n\n## How It Works\n\n1. `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.\n2. `portless <name> <cmd>` assigns a random free port (4000-4999) via the `PORT` env var and registers the app with the proxy\n3. The browser hits `https://<name>.localhost`; the proxy forwards to the app's assigned port\n\nOutside 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.\n\nWith `--unprivileged` (or `PORTLESS_UNPRIVILEGED=1`) the proxy takes a port below 1024 without `sudo`: it binds the wildcard addresses, which macOS allows for non-root processes, and refuses every non-loopback peer itself. It serves plain HTTP on port 80 unless `--https` is given, because installing the CA would be a prompt; with `--https` the CA is generated but not installed, so run `portless trust` once yourself. It never writes the hosts file: automatic hosts sync is off, `PORTLESS_SYNC_HOSTS=1` together with it is refused, and Safari or a custom TLD need `portless hosts sync` run once by you. Linux needs `net.ipv4.ip_unprivileged_port_start` to cover the port; portless checks and prints the sysctl command instead of falling back to another port. The mode is remembered for auto-start and never triggers a sudo prompt, so it suits scripts, agents and managed machines.\n\nWhen 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.\n\n`.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.\n\nUse `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`.\n\nTLDs 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`.\n\nMost 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.\n\n### State directory\n\nPortless 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.\n\n### Environment variables\n\n| Variable                | Description                                                                    |\n| ----------------------- | ------------------------------------------------------------------------------ |\n| `PORTLESS_PORT`         | Override the default proxy port (default: 443 with HTTPS, 80 without)          |\n| `PORTLESS_APP_PORT`     | Use a fixed port for the app (skip auto-assignment)                            |\n| `PORTLESS_HTTPS`        | HTTPS on by default; set to `0` to disable (same as `--no-tls`)                |\n| `PORTLESS_LAN`          | Set to `1` to always enable LAN mode (auto-detects LAN IP)                     |\n| `PORTLESS_LAN_IP`       | Pin a specific LAN IP for LAN mode                                             |\n| `PORTLESS_TLD`          | Use one or more TLDs, single or multi-segment (e.g. localhost,dev.example.com) |\n| `PORTLESS_WILDCARD`     | Set to `1` to allow unregistered subdomains to fall back to parent             |\n| `PORTLESS_UNPRIVILEGED` | Set to `1` to take a port below 1024 without sudo (same as `--unprivileged`)   |\n| `PORTLESS_SYNC_HOSTS`   | Set to `0` to disable auto-sync of /etc/hosts (on by default)                  |\n| `PORTLESS_TAILSCALE`    | Set to `1` to share apps on your Tailscale network (same as `--tailscale`)     |\n| `PORTLESS_FUNNEL`       | Set to `1` to share apps publicly via Tailscale Funnel (same as `--funnel`)    |\n| `PORTLESS_NGROK`        | Set to `1` to share apps publicly via ngrok (same as `--ngrok`)                |\n| `PORTLESS_CLOUDFLARE`   | Set to `1` to share apps publicly via Cloudflare (same as `--cloudflare`)      |\n| `PORTLESS_STATE_DIR`    | Override the state directory                                                   |\n| `PORTLESS=0`            | Bypass the proxy, run the command directly                                     |\n\n### HTTP/2 + HTTPS\n\nHTTPS 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.\n\n```bash\nportless proxy start --cert ./c.pem --key ./k.pem  # Use custom certs\nportless proxy start --no-tls                       # Disable HTTPS (plain HTTP)\nportless trust                                      # Add CA to trust store later\n```\n\nOn 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.\n\n### LAN mode\n\n```bash\nportless proxy start --lan\nportless proxy start --lan --https\nportless proxy start --lan --ip 192.168.1.42\n```\n\n`--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.\n\nPortless 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.\n\nLAN 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).\n\n- **Next.js**: add your `.local` hostnames to `allowedDevOrigins`:\n\n  ```js\n  // next.config.js\n  module.exports = {\n    allowedDevOrigins: [\"myapp.local\", \"*.myapp.local\"],\n  };\n  ```\n\n- **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`.\n\n### Tailscale sharing\n\nShare dev servers with teammates on your Tailscale network using `--tailscale`, or expose to the public internet with `--funnel`:\n\n```bash\nportless myapp --tailscale next dev\n# -> https://myapp.localhost           (local)\n# -> https://devbox.yourteam.ts.net    (tailnet)\n\nportless myapp --funnel next dev\n# -> https://myapp.localhost           (local)\n# -> https://devbox.yourteam.ts.net    (public internet)\n```\n\nTailscale 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.\n\nEach `--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.\n\n### ngrok sharing\n\nExpose a dev server to the public internet with ngrok using `--ngrok`:\n\n```bash\nportless myapp --ngrok next dev\n# -> https://myapp.localhost           (local)\n# -> https://abc123.ngrok.app          (public internet)\n```\n\nSet `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>`.\n\n### Cloudflare sharing\n\n```bash\nportless myapp --cloudflare next dev\n# -> https://<random>.trycloudflare.com (public internet)\n```\n\n`--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/).\n\n## OS startup service\n\nUse the service command when users want the proxy to start automatically after reboot:\n\n```bash\nportless service install\nportless service install --lan\nportless service install --wildcard\nPORTLESS_STATE_DIR=~/.portless-lan PORTLESS_LAN=1 portless service install\nportless service status\nportless service uninstall\n```\n\nThe 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.\n\nThe 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.\n\n## CLI Reference\n\n| Command                                           | Description                                                    |\n| ------------------------------------------------- | -------------------------------------------------------------- |\n| `portless`                                        | Run dev script through proxy                                   |\n| `portless`                                        | From monorepo root: run all workspace packages                 |\n| `portless --script <name>`                        | Run a specific package.json script (default: dev)              |\n| `portless run [cmd] [args...]`                    | Infer name from project, run through proxy (auto-starts)       |\n| `portless run --name <name> <cmd>`                | Override inferred base name (worktree prefix still applies)    |\n| `portless <name> <cmd> [args...]`                 | Run app at `https://<name>.localhost` (auto-starts proxy)      |\n| `portless get <name>`                             | Print URL for a service (for cross-service wiring)             |\n| `portless get <name> --no-worktree`               | Print URL without worktree prefix                              |\n| `portless list`                                   | Show active routes and uptime                                  |\n| `portless doctor`                                 | Check proxy, routes, DNS, CA trust, and LAN prerequisites      |\n| `portless trust`                                  | Add local CA to system trust store (for HTTPS)                 |\n| `portless clean`                                  | Remove state, CA trust entry, and /etc/hosts block             |\n| `portless prune`                                  | Kill orphaned dev server groups; `run` also does it on start   |\n| `portless prune --force`                          | Kill orphans with SIGKILL instead of SIGTERM                   |\n| `portless proxy start`                            | Start HTTPS proxy as a daemon (port 443, auto-elevates)        |\n| `portless proxy start --no-tls`                   | Start without HTTPS (plain HTTP on port 80)                    |\n| `portless proxy start --lan`                      | Start in LAN mode (mDNS `.local`, auto-follows LAN IP changes) |\n| `portless proxy start -p <number>`                | Start the proxy on a custom port                               |\n| `portless proxy start --unprivileged`             | Plain HTTP on port 80 without sudo, nothing installed          |\n| `portless proxy start --tld test`                 | Use .test instead of .localhost                                |\n| `portless proxy start --tld localhost --tld test` | Serve both TLDs from one proxy                                 |\n| `portless proxy start --tld dev.example.com`      | Use a multi-segment TLD for production-parity URLs             |\n| `portless proxy start --foreground`               | Start the proxy in foreground (for debugging)                  |\n| `portless proxy start --wildcard`                 | Allow unregistered subdomains to fall back to parent route     |\n| `portless proxy stop`                             | Stop the proxy                                                 |\n| `portless service install`                        | Start the HTTPS proxy when the OS starts                       |\n| `portless service install --lan`                  | Start the service in LAN mode                                  |\n| `portless service install --wildcard`             | Persist wildcard routing in the startup service                |\n| `portless service status`                         | Show service and proxy status                                  |\n| `portless service uninstall`                      | Remove the startup service                                     |\n| `portless alias <name> <port>`                    | Register a static route (e.g. for Docker containers)           |\n| `portless alias <name> <port> --force`            | Overwrite an existing route                                    |\n| `portless alias --remove <name>`                  | Remove a static route                                          |\n| `portless hosts sync`                             | Reconcile routes with /etc/hosts (fixes Safari)                |\n| `portless hosts clean`                            | Remove portless entries from /etc/hosts                        |\n| `portless <name> --app-port <n> <cmd>`            | Use a fixed port for the app instead of auto-assignment        |\n| `portless <name> --tailscale <cmd>`               | Share the app on your Tailscale network (tailnet)              |\n| `portless <name> --funnel <cmd>`                  | Share the app publicly via Tailscale Funnel                    |\n| `portless <name> --ngrok <cmd>`                   | Share the app publicly via ngrok                               |\n| `portless <name> --cloudflare <cmd>`              | Share the app publicly via a Cloudflare quick tunnel           |\n| `portless <name> --force <cmd>`                   | Kill the existing process and take over its route              |\n| `portless --name <name> <cmd>`                    | Force `<name>` as app name (bypasses subcommand dispatch)      |\n| `portless <name> -- <cmd> [args...]`              | Stop flag parsing; everything after `--` is passed to child    |\n| `portless --help` / `-h`                          | Show help                                                      |\n| `portless run --help`                             | Show help for a subcommand (also: alias, hosts, clean)         |\n| `portless --version` / `-v`                       | Show version                                                   |\n\n**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.\n\n## MCP server\n\n`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.\n\n```json\n{ \"mcpServers\": { \"portless\": { \"command\": \"portless\", \"args\": [\"mcp\"] } } }\n```\n\n## portless.json\n\nOptional config file. Portless looks for it in the current directory.\n\n| Field     | Type    | Default                    | Description                                              |\n| --------- | ------- | -------------------------- | -------------------------------------------------------- |\n| `name`    | string  | inferred from package.json | Base app name (worktree prefix still applies)            |\n| `script`  | string  | `\"dev\"`                    | Name of a package.json script to run                     |\n| `appPort` | number  | auto-assigned              | Fixed port for the child process                         |\n| `proxy`   | boolean | auto-detected              | Whether to route through the proxy (`false` for tasks)   |\n| `apps`    | object  |                            | Overrides for workspace packages, keyed by relative path |\n| `turbo`   | boolean | `true`                     | Set `false` to use direct spawning instead of turborepo  |\n\nEach `apps` entry has the same shape (`name`, `script`, `appPort`, `proxy`). When `apps` is present, top-level fields apply only in single-app mode.\n\n### package.json \"portless\" key\n\nInstead of a separate `portless.json`, you can add a `\"portless\"` key to your `package.json`. A string value is shorthand for setting the name:\n\n```json\n{ \"portless\": \"myapp\" }\n```\n\nAn object supports all per-app fields (`name`, `script`, `appPort`, `proxy`):\n\n```json\n{ \"portless\": { \"name\": \"myapp\", \"script\": \"dev:app\" } }\n```\n\nPrecedence (closest wins): CLI flags > package.json `\"portless\"` key > portless.json app entry > defaults.\n\n## Troubleshooting\n\n### Run diagnostics\n\nUse `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.\n\n### Proxy not running\n\nThe proxy auto-starts when you run an app with `portless <name> <cmd>`. If it doesn't start (e.g. port conflict), start it manually:\n\n```bash\nportless proxy start\n```\n\n### Port already in use\n\nAnother process is bound to the proxy port. Either stop it first, or use a different port:\n\n```bash\nportless proxy start -p 8080\n```\n\n### Framework not respecting PORT\n\nPortless 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.\n\nFor other frameworks that don't read `PORT`, pass the port manually:\n\n- **Webpack Dev Server**: use `--port $PORT`\n- **Custom servers**: read `process.env.PORT` and listen on it\n\n### Permission errors\n\nThe 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.\n\n```bash\nportless proxy start --https           # Auto-elevates with sudo for port 443\nportless proxy start -p 1355 --https   # No sudo needed (URLs include :1355)\nportless proxy start --unprivileged    # Plain HTTP on port 80, no sudo, clean URLs\nportless proxy stop                    # Stop (use sudo if started with sudo)\n```\n\n### Safari can't find .localhost URLs\n\nSafari 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.\n\nFix:\n\n```bash\nportless hosts sync    # Reconcile current routes with /etc/hosts\nportless hosts clean   # Remove entries later\n```\n\nAuto-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`.\n\nManual sync reconciles portless-managed entries with current routes and removes stale entries when there are no routes. It requires a successful initial hosts-file read before writing and verifies each write. A read or verification failure follows the normal sync error path.\n\n### Browser shows certificate warning with --https\n\nThe local CA may not be trusted yet. Run:\n\n```bash\nportless trust\n```\n\nThis adds the portless local CA to your system trust store. After that, restart the browser.\n\n### Remove portless from the machine\n\n```bash\nportless clean\n```\n\nStops the proxy if needed, removes the portless CA from the trust store (when portless added it), deletes known files under state directories, and removes the portless `/etc/hosts` block. May require `sudo` on macOS/Linux. If trust-store removal fails, portless retains its CA certificate and key so a later `portless clean` can safely retry.\n\n### Proxy loop (508 Loop Detected)\n\nIf your dev server proxies requests to another portless app (e.g. Vite proxying `/api` to `api.myapp.localhost`), the proxy must rewrite the `Host` header. Without this, portless routes the request back to the original app, creating an infinite loop.\n\nFix: set `changeOrigin: true` in the proxy config (Vite, webpack-dev-server, etc.):\n\n```ts\n// vite.config.ts\nproxy: {\n  \"/api\": {\n    target: \"https://api.myapp.localhost\",\n    changeOrigin: true,\n    ws: true,\n  },\n}\n```\n\nPortless automatically sets `NODE_EXTRA_CA_CERTS` in child processes so Node.js trusts the portless CA. If you run a separate Node.js process outside portless, point it at the CA manually: `NODE_EXTRA_CA_CERTS=~/.portless/ca.pem`. Alternatively, use `--no-tls` for plain HTTP.\n\n### Tailscale not working\n\nIf `--tailscale` or `--funnel` fails:\n\n```bash\ntailscale status     # Check if connected\ntailscale up         # Connect to your tailnet\n```\n\nRequires the Tailscale CLI to be installed (https://tailscale.com/download) and on PATH.\n\n### ngrok not working\n\nIf `--ngrok` fails:\n\n```bash\nngrok version                         # Check if installed\nngrok config add-authtoken <token>    # Configure authentication\n```\n\nRequires the ngrok CLI to be installed (https://ngrok.com/download) and on PATH.\n\n### Requirements\n\n- Node.js 24+\n- macOS, Linux, or Windows\n- `openssl` (for `--https` cert generation; ships with macOS and most Linux distributions; on Windows, install via `winget install -e --id ShiningLight.OpenSSL.Dev` or use the copy bundled with Git for Windows)\n- `tailscale` CLI (optional, for `--tailscale` and `--funnel`)\n- `ngrok` CLI (optional, for `--ngrok`)\n- `cloudflared` CLI (optional, for `--cloudflare`)\n","frontmatter":{"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."},"isInternal":false,"tokens":8015,"sizeBytes":33797}],"systemPromptSnippet":"<yakaai_skills repo=\"vercel-labs/portless\">\n<!-- File: AGENTS.md (Tokens: ~1569 | Category: root-instruction) -->\n# Agent Rules\n\n## Package Manager\n\nUse `pnpm` for all package management commands (not npm or yarn).\n\nException: End-user install instructions should use `npm install -g` (global) or `npm install -D` (project dev dependency) since npm is universal.\n\n## Dependencies\n\nAlways check for the latest npm version when adding dependencies. Use `pnpm add <package>` (without version) to get the latest, or verify with `npm view <package> version` first.\n\n## No Emojis\n\nDo not use emojis anywhere in this repository (code, comments, output, docs).\n\n## Dashes\n\nNever use `--` or em dashes as dashes in prose, comments, or user-facing output. Rephrase the sentence instead. The only exception is CLI flags (e.g. `--port`).\n\n## Boolean Environment Variables\n\nDocument boolean env vars using only `0` and `1` in CLI help, SKILL.md, docs pages, and README. Code accepts `true`/`false` as well (and `skip` for `PORTLESS`), but these alternatives are not documented.\n\n## Docs Updates\n\nWhen a change affects how humans or agents use portless (new/changed/removed commands, flags, behavior, or config), update all of these:\n\n1. `README.md` (user-facing documentation)\n2. `skills/portless/SKILL.md` (agent skill for using portless)\n3. `packages/portless/src/cli.ts` (`--help` output)\n4. `packages/portless/src/mcp.ts` (MCP tools and `portless mcp --help` output)\n\nUse the `technical-writer` skill (`.agents/skills/technical-writer/SKILL.md`) when you write or edit prose for this repo, including docs pages, READMEs, changelog entries, commit messages, PR descriptions, and issues.\n\n## MCP Tools\n\nAny change to a CLI command, flag, output, or environment variable must update the MCP tools in `packages/portless/src/mcp.ts` in the same change. `mcp.test.ts` reads `portless --help` and each tool command's `--help`, and fails when a command or flag is neither handled by a tool nor listed as omitted.\n\n## Releasing\n\nReleases are manual, single-PR affairs. The maintainer controls the changelog voice and format.\n\nTo prepare a release:\n\n1. Create a branch (e.g. `prepare-v1.2.0`)\n2. Bump the version in `packages/portless/package.json`\n3. Write the changelog entry in `CHANGELOG.md`, wrapped in `<!-- release:start -->` and `<!-- release:end -->` markers\n4. Remove the `<!-- release:start -->` and `<!-- release:end -->` markers from the previous release entry (only the latest release should have markers)\n5. Add a matching entry to `apps/docs/content/docs/changelog.mdx`\n6. Open a PR and merge to `main`\n\nCI compares the version in `packages/portless/package.json` to what's on npm. If it differs, it builds, publishes, and creates the GitHub release automatically. The release body is extracted from the content between the markers.\n\n## Windows Debugging\n\nA remote Windows Server 2022 EC2 instance is available for debugging Windows-specific issues. It uses AWS Systems Manager (SSM) with no SSH or open ports. Commands run via `aws ssm send-command` and return stdout/stderr.\n\nAll scripts require `AWS_PROFILE=portless-debug` (or the profile must be set as default). Prefix every command with it or export it for the session:\n\n```bash\nexport AWS_PROFILE=portless-debug\n```\n\n### Prerequisites\n\nThe instance must be provisioned first (one-time, by a human):\n\n```bash\n./scripts/windows-debug/provision.sh\n```\n\nRequires: AWS CLI v2 configured with `ec2:*`, `iam:CreateRole`, `iam:AttachRolePolicy`, `ssm:SendCommand`, `ssm:GetCommandInvocation` permissions and a default VPC.\n\n### Usage\n\nStart the instance (if stopped):\n\n```bash\n./scripts/windows-debug/start.sh\n```\n\nRun a command on Windows:\n\n```bash\n./scripts/windows-debug/run.sh \"<powershell-command>\"\n```\n\nSync the current git branch and rebuild:\n\n```bash\n./scripts/windows-debug/sync.sh\n```\n\nStop the instance when done (avoids cost):\n\n```bash\n./scripts/windows-debug/stop.sh\n```\n\n### Important notes\n\n**SSM agent takes a long time to come online.** After starting or restarting the instance, the SSM agent can take 5 to 10 minutes before it accepts commands. If `run.sh` returns `InvalidInstanceId`, wait and retry. Do not assume the instance is broken; poll with increasing intervals.\n\n**PowerShell uses `;` not `&&`.** The `run.sh` wrapper executes PowerShell, which does not support `&&` as a command separator. Use `;` instead:\n\n```bash\n./scripts/windows-debug/run.sh \"cd C:\\portless; pnpm test\"\n```\n\n**OpenSSL may not be at the expected path.** The bootstrap installs OpenSSL to `C:\\Program Files\\OpenSSL-Win64\\bin`, but this can fail silently. Git bundles its own OpenSSL at `C:\\Program Files\\Git\\mingw64\\bin`. If `openssl` is not found, add Git's path:\n\n```bash\n./scripts/windows-debug/run.sh '$env:PATH = \"C:\\Program Files\\Git\\mingw64\\bin;$env:PATH\"; openssl version'\n```\n\n**SSM runs as SYSTEM.** Commands execute as the SYSTEM account, not a normal user. This affects user-specific operations (e.g., `certutil -addstore -user Root` targets SYSTEM's trust store, not a real user's). Keep this in mind when testing user-facing features.\n\n### Common Workflows\n\nRun unit tests on Windows:\n\n```bash\n./scripts/windows-debug/run.sh \"cd C:\\portless; pnpm test\"\n```\n\nRun e2e tests on Windows:\n\n```bash\n./scripts/windows-debug/run.sh \"cd C:\\portless; pnpm test:e2e\"\n```\n\nCheck bootstrap progress (first boot only):\n\n```bash\n./scripts/windows-debug/run.sh \"Get-Content C:\\bootstrap.log\"\n```\n\nThe repo lives at `C:\\portless` on the instance. Node.js 24, pnpm 11, Git, and OpenSSL are pre-installed. The `run.sh` wrapper automatically adds these tools to PATH.\n\n<!-- opensrc:start -->\n\n## Source Code Reference\n\nSource code for dependencies is available in `opensrc/` for deeper understanding of implementation details.\n\nSee `opensrc/sources.json` for the list of available packages and their versions.\n\nUse this source code when you need to understand how a package works internally, not just its types/interface.\n\n### Fetching Additional Source Code\n\nTo fetch source code for a package or repository you need to understand, run:\n\n```bash\nnpx opensrc <package>           # npm package (e.g., npx opensrc zod)\nnpx opensrc pypi:<package>      # Python package (e.g., npx opensrc pypi:requests)\nnpx opensrc crates:<package>    # Rust crate (e.g., npx opensrc crates:serde)\nnpx opensrc <owner>/<repo>      # GitHub repo (e.g., npx opensrc vercel/ai)\n```\n\n<!-- opensrc:end -->\n\n<!-- File: packages/portless/package.json (Tokens: ~466 | Category: anthropic-skill) -->\n{\n  \"name\": \"portless\",\n  \"version\": \"0.15.13\",\n  \"description\": \"Replace port numbers with stable, named .localhost URLs. For humans and agents.\",\n  \"type\": \"module\",\n  \"main\": \"./dist/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"import\": \"./dist/index.js\",\n      \"types\": \"./dist/index.d.ts\"\n    }\n  },\n  \"bin\": {\n    \"portless\": \"./dist/cli.js\"\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"engines\": {\n    \"node\": \">=24\"\n  },\n  \"os\": [\n    \"darwin\",\n    \"linux\",\n    \"win32\"\n  ],\n  \"scripts\": {\n    \"build\": \"tsup\",\n    \"dev\": \"tsup --watch\",\n    \"lint\": \"eslint src/\",\n    \"lint:fix\": \"eslint src/ --fix\",\n    \"prepublishOnly\": \"cp ../../README.md . && pnpm build\",\n    \"test\": \"vitest run\",\n    \"test:coverage\": \"vitest run --coverage\",\n    \"test:watch\": \"vitest\",\n    \"type-check\": \"tsc --noEmit\"\n  },\n  \"keywords\": [\n    \"local\",\n    \"development\",\n    \"proxy\",\n    \"localhost\"\n  ],\n  \"author\": \"Vercel Labs\",\n  \"license\": \"Apache-2.0\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/vercel-labs/portless.git\"\n  },\n  \"homepage\": \"https://portless.sh\",\n  \"bugs\": {\n    \"url\": \"https://github.com/vercel-labs/portless/issues\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"^24.12.4\",\n    \"@vitest/coverage-v8\": \"^4.0.18\",\n    \"eslint\": \"^9.39.2\",\n    \"tsup\": \"^8.0.1\",\n    \"typescript\": \"^5.3.3\",\n    \"vitest\": \"^4.0.18\"\n  }\n}\n\n<!-- File: packages/portless/tsconfig.json (Tokens: ~107 | Category: anthropic-skill) -->\n{\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"esModuleInterop\": true,\n    \"strict\": true,\n    \"skipLibCheck\": true,\n    \"outDir\": \"dist\",\n    \"rootDir\": \"src\",\n    \"declaration\": true,\n    \"resolveJsonModule\": true\n  },\n  \"include\": [\"src/**/*\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n\n<!-- File: skills/oauth/SKILL.md (Tokens: ~1733 | Category: anthropic-skill) -->\n---\nname: oauth\ndescription: Configure OAuth providers (Google, Apple, Microsoft, Facebook, GitHub, etc.) to work with portless local dev URLs. Use when setting up OAuth redirect URIs, fixing \"redirect_uri_mismatch\" or \"invalid redirect\" errors, configuring sign-in providers for local development, or when a provider rejects .localhost subdomains. Triggers include \"OAuth not working with portless\", \"redirect URI mismatch\", \"Google/Apple/Microsoft sign-in fails locally\", \"configure OAuth for local dev\", or any task involving OAuth callback URLs with portless domains.\n---\n\n# OAuth with Portless\n\nOAuth providers validate redirect URIs against domain rules. `.localhost` subdomains fail on most providers because they are not in the Public Suffix List or are explicitly blocked. Portless fixes this with `--tld` to serve apps on real, valid domains.\n\n## The Problem\n\nWhen portless uses the default `.localhost` TLD, OAuth providers reject redirect URIs like `http://myapp.localhost:1355/callback`:\n\n| Provider  | `localhost` | `.localhost` subdomains | Reason                         |\n| --------- | ----------- | ----------------------- | ------------------------------ |\n| Google    | Allowed     | Rejected                | Not in their bundled PSL       |\n| Apple     | Rejected    | Rejected                | No localhost at all            |\n| Microsoft | Allowed     | Allowed                 | Permissive localhost handling  |\n| Facebook  | Allowed     | Varies                  | Must register each URI exactly |\n| GitHub    | Allowed     | Allowed                 | Permissive                     |\n\nGoogle and Apple are the strictest. Microsoft and GitHub are more lenient with localhost.\n\n## The Fix\n\nUse a valid TLD so the redirect URI passes provider validation:\n\n```bash\nportless proxy start --tld dev\nportless myapp next dev\n# -> https://myapp.dev\n```\n\nAny TLD in the Public Suffix List works: `.dev`, `.app`, `.com`, `.io`, etc.\n\n### Use a domain you own\n\nBare TLDs like `.dev` mean `myapp.dev` could collide with a real domain. Use a multi-segment TLD under a domain you control, so the app name stays clean and the domain structure lives in the TLD:\n\n```bash\nportless proxy start --tld local.yourcompany.dev\nportless myapp next dev\n# -> https://myapp.local.yourcompany.dev\n```\n\nThis ensures no outbound traffic reaches something you don't own. For teams, set a wildcard DNS record (`*.local.yourcompany.dev -> 127.0.0.1`) so every developer gets resolution without `/etc/hosts`, and every developer shares the same redirect URIs in the provider console.\n\n## Provider Setup\n\n### Google\n\n1. Go to [Google Cloud Console > Credentials](https://console.cloud.google.com/apis/credentials)\n2. Create or edit an OAuth 2.0 Client ID (Web application)\n3. Add the portless domain to **Authorized JavaScript origins**: `https://myapp.dev`\n4. Add the callback to **Authorized redirect URIs**: `https://myapp.dev/api/auth/callback/google`\n\nGoogle validates domains against the Public Suffix List. The domain must end with a recognized TLD. `.localhost` subdomains fail this check; `.dev`, `.app`, `.com`, etc. all pass.\n\nHTTPS is required for `.dev` and `.app` (HSTS-preloaded). Portless handles this automatically with `--https`.\n\n### Apple\n\nApple Sign In does not allow `localhost` or IP addresses at all.\n\n1. Go to [Apple Developer > Certificates, Identifiers & Profiles](https://developer.apple.com/account/resources)\n2. Register a Services ID\n3. Configure Sign In with Apple, adding the portless domain as a **Return URL**: `https://myapp.dev/api/auth/callback/apple`\n\nThe domain must be a real, publicly-resolvable domain name. Since portless maps the domain to 127.0.0.1 locally, the browser resolves it but Apple's server-side validation may require the domain to resolve publicly too. If Apple rejects the domain, add a public DNS A record pointing to 127.0.0.1 for your dev subdomain.\n\n### Microsoft (Entra / Azure AD)\n\n1. Go to [Azure Portal > App registrations](https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps)\n2. Create or edit an app registration\n3. Under **Authentication**, add a **Web** redirect URI: `https://myapp.dev/api/auth/callback/azure-ad`\n\nMicrosoft allows `http://localhost` with any port for development. It also accepts `.localhost` subdomains in most cases. Using a custom TLD with portless is still recommended for consistency across providers.\n\n### Facebook (Meta)\n\n1. Go to [Meta for Developers > App Dashboard](https://developers.facebook.com/apps/)\n2. Under **Facebook Login > Settings**, add the portless URL to **Valid OAuth Redirect URIs**: `https://myapp.dev/api/auth/callback/facebook`\n\nFacebook requires each redirect URI to be registered exactly (no wildcards). Strict Mode (enabled by default) enforces exact matching.\n\n### GitHub\n\n1. Go to [GitHub Developer Settings > OAuth Apps](https://github.com/settings/developers)\n2. Set **Authorization callback URL**: `https://myapp.dev/api/auth/callback/github`\n\nGitHub is permissive with localhost and subdomains. A custom TLD is not strictly required but keeps the setup consistent.\n\n## Auth Library Configuration\n\n### NextAuth / Auth.js\n\nSet `NEXTAUTH_URL` to match the portless domain:\n\n```env\nNEXTAUTH_URL=https://myapp.dev\n```\n\nNextAuth uses this to construct callback URLs. Without it, callbacks may use `localhost` and cause a mismatch.\n\n### Passport.js\n\nSet the `callbackURL` in each strategy to use the portless domain:\n\n```js\nnew GoogleStrategy({\n  clientID: process.env.GOOGLE_CLIENT_ID,\n  clientSecret: process.env.GOOGLE_CLIENT_SECRET,\n  callbackURL: process.env.BASE_URL + \"/auth/google/callback\",\n});\n```\n\nSet `BASE_URL=https://myapp.dev` in your environment.\n\n### Generic / Manual\n\nRead the `PORTLESS_URL` environment variable that portless injects into the child process:\n\n```js\nconst baseUrl = process.env.PORTLESS_URL || \"http://localhost:3000\";\nconst callbackUrl = `${baseUrl}/auth/callback`;\n```\n\n## Troubleshooting\n\n### \"redirect_uri_mismatch\" or \"invalid redirect URI\"\n\nThe redirect URI sent during the OAuth flow doesn't match what's registered with the provider. Check:\n\n1. The provider's registered redirect URI matches the portless domain exactly (protocol, host, path)\n2. `NEXTAUTH_URL` or equivalent is set to the portless URL (not `localhost`)\n3. The proxy is running with the correct TLD (`portless list` to verify)\n\n### Provider requires HTTPS\n\n`.dev` and `.app` TLDs are HSTS-preloaded, so browsers force HTTPS. Start the proxy:\n\n```bash\nportless proxy start --tld dev\n```\n\nPortless defaults to HTTPS on port 443 (auto-elevates with sudo). Run `portless trust` to add the local CA to your system trust store and eliminate browser warnings.\n\n### Apple rejects the domain\n\nApple may require the domain to resolve publicly. Add a DNS A record for your dev subdomain pointing to `127.0.0.1`:\n\n```\nmyapp.local.yourcompany.dev  A  127.0.0.1\n```\n\nOr use a wildcard: `*.local.yourcompany.dev  A  127.0.0.1`.\n\n### Callback goes to wrong URL after sign-in\n\nThe auth library is constructing the callback URL from `localhost` instead of the portless domain. Set the appropriate environment variable:\n\n- **NextAuth**: `NEXTAUTH_URL=https://myapp.dev`\n- **Auth.js v5**: `AUTH_URL=https://myapp.dev`\n- **Manual**: `PORTLESS_URL` is injected automatically; use it as the base URL\n\n## Example\n\nSee [`examples/google-oauth`](../../examples/google-oauth) for a complete working example with Next.js + NextAuth + Google OAuth using `--tld dev`.\n\n<!-- File: skills/portless/SKILL.md (Tokens: ~8015 | Category: anthropic-skill) -->\n---\nname: portless\ndescription: 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.\n---\n\n# Portless\n\nReplace port numbers with stable, named .localhost URLs. For humans and agents.\n\n## Why portless\n\n- **Port conflicts**: `EADDRINUSE` when two projects default to the same port\n- **Memorizing ports**: which app is on 3001 vs 8080?\n- **Refreshing shows the wrong app**: stop one server, start another on the same port, stale tab shows wrong content\n- **Monorepo multiplier**: every problem scales with each service in the repo\n- **Agents test the wrong port**: AI agents guess or hardcode the wrong port\n- **Cookie/storage clashes**: cookies on `localhost` bleed across apps; localStorage lost when ports shift\n- **Hardcoded ports in config**: CORS allowlists, OAuth redirects, `.env` files break when ports change\n- **Sharing URLs with teammates**: \"what port is that on?\" becomes a Slack question\n- **Browser history is useless**: `localhost:3000` history is a mix of unrelated projects\n\n## Installation\n\nInstall globally (recommended) or as a project dev dependency. Do NOT use `npx` or `pnpm dlx` for one-off execution.\n\n```bash\n# Global (available everywhere)\nnpm install -g portless\n\n# Or per-project dev dependency\nnpm install -D portless\n```\n\nWhen installed per-project, invoke via package.json scripts or `npx portless` (since the package is local, npx will not download anything).\n\n## Quick Start\n\n```bash\n# Install globally (or add -D to a project)\nnpm install -g portless\n\n# Run your app (auto-starts the HTTPS proxy on port 443)\nportless run next dev\n# -> https://<project>.localhost\n\n# Or with an explicit name\nportless myapp next dev\n# -> https://myapp.localhost\n```\n\nThe 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.\n\nIn 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.\n\n## Integration Patterns\n\n### Zero-config (recommended)\n\nBare `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:\n\n```bash\nportless        # -> runs \"dev\" script, https://<project>.localhost\npnpm dev        # -> works without portless, plain \"next dev\"\n```\n\nUse an optional `portless.json` to override defaults (name, script, port):\n\n```json\n{ \"name\": \"myapp\" }\n```\n\n```bash\nportless        # -> runs \"dev\" script, https://myapp.localhost\n```\n\n### Monorepo\n\nOne `portless.json` at the repo root. Portless discovers packages from `pnpm-workspace.yaml`, or the `\"workspaces\"` field in `package.json` (npm, yarn, bun):\n\n```json\n{\n  \"apps\": {\n    \"apps/web\": { \"name\": \"myapp\" },\n    \"apps/api\": { \"name\": \"api.myapp\" }\n  }\n}\n```\n\n```bash\nportless                  # from repo root: start all packages with a \"dev\" script\ncd apps/web && portless   # start just one package\nportless --script start   # run \"start\" instead of \"dev\"\n```\n\nThe `apps` map is optional and only provides name overrides. Unlisted packages auto-discover with inferred names.\n\nWithout 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`.\n\n### Turborepo\n\nFor turborepo projects, use portless as the `dev` script with the real command in a separate script:\n\n```json\n{\n  \"scripts\": { \"dev\": \"portless\", \"dev:app\": \"next dev\" },\n  \"portless\": { \"name\": \"myapp\", \"script\": \"dev:app\" }\n}\n```\n\n`pnpm dev` runs turbo, which runs `portless` in each package. Portless detects the package manager and runs `pnpm run dev:app` through the proxy.\n\nWhen `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.\n\n### package.json scripts\n\nYou can still use portless directly in scripts:\n\n```json\n{\n  \"scripts\": {\n    \"dev\": \"portless run next dev\"\n  }\n}\n```\n\nThe proxy auto-starts when you run an app. Or start it explicitly: `portless proxy start`.\n\n### Multi-app setups with subdomains\n\n```bash\nportless myapp next dev          # https://myapp.localhost\nportless api.myapp pnpm start    # https://api.myapp.localhost\nportless docs.myapp next dev     # https://docs.myapp.localhost\n```\n\nBy 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`).\n\n### Git worktrees\n\n`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:\n\n```bash\n# Main worktree (no prefix)\nportless run next dev   # -> https://myapp.localhost\n\n# Linked worktree on branch \"fix-ui\"\nportless run next dev   # -> https://fix-ui.myapp.localhost\n```\n\nNo config changes needed. Put `portless run` in `package.json` once and it works in all worktrees.\n\n### Bypassing portless\n\nSet `PORTLESS=0` to run the command directly without the proxy:\n\n```bash\nPORTLESS=0 pnpm dev   # Bypasses proxy, uses default port\n```\n\nWhen a proxied command is stopped with Ctrl+C, portless waits for its process tree to exit. A second\nCtrl+C forwards another interrupt, and remaining descendants are terminated after a short grace\nperiod.\n\n## How It Works\n\n1. `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.\n2. `portless <name> <cmd>` assigns a random free port (4000-4999) via the `PORT` env var and registers the app with the proxy\n3. The browser hits `https://<name>.localhost`; the proxy forwards to the app's assigned port\n\nOutside 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.\n\nWith `--unprivileged` (or `PORTLESS_UNPRIVILEGED=1`) the proxy takes a port below 1024 without `sudo`: it binds the wildcard addresses, which macOS allows for non-root processes, and refuses every non-loopback peer itself. It serves plain HTTP on port 80 unless `--https` is given, because installing the CA would be a prompt; with `--https` the CA is generated but not installed, so run `portless trust` once yourself. It never writes the hosts file: automatic hosts sync is off, `PORTLESS_SYNC_HOSTS=1` together with it is refused, and Safari or a custom TLD need `portless hosts sync` run once by you. Linux needs `net.ipv4.ip_unprivileged_port_start` to cover the port; portless checks and prints the sysctl command instead of falling back to another port. The mode is remembered for auto-start and never triggers a sudo prompt, so it suits scripts, agents and managed machines.\n\nWhen 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.\n\n`.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.\n\nUse `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`.\n\nTLDs 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`.\n\nMost 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.\n\n### State directory\n\nPortless 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.\n\n### Environment variables\n\n| Variable                | Description                                                                    |\n| ----------------------- | ------------------------------------------------------------------------------ |\n| `PORTLESS_PORT`         | Override the default proxy port (default: 443 with HTTPS, 80 without)          |\n| `PORTLESS_APP_PORT`     | Use a fixed port for the app (skip auto-assignment)                            |\n| `PORTLESS_HTTPS`        | HTTPS on by default; set to `0` to disable (same as `--no-tls`)                |\n| `PORTLESS_LAN`          | Set to `1` to always enable LAN mode (auto-detects LAN IP)                     |\n| `PORTLESS_LAN_IP`       | Pin a specific LAN IP for LAN mode                                             |\n| `PORTLESS_TLD`          | Use one or more TLDs, single or multi-segment (e.g. localhost,dev.example.com) |\n| `PORTLESS_WILDCARD`     | Set to `1` to allow unregistered subdomains to fall back to parent             |\n| `PORTLESS_UNPRIVILEGED` | Set to `1` to take a port below 1024 without sudo (same as `--unprivileged`)   |\n| `PORTLESS_SYNC_HOSTS`   | Set to `0` to disable auto-sync of /etc/hosts (on by default)                  |\n| `PORTLESS_TAILSCALE`    | Set to `1` to share apps on your Tailscale network (same as `--tailscale`)     |\n| `PORTLESS_FUNNEL`       | Set to `1` to share apps publicly via Tailscale Funnel (same as `--funnel`)    |\n| `PORTLESS_NGROK`        | Set to `1` to share apps publicly via ngrok (same as `--ngrok`)                |\n| `PORTLESS_CLOUDFLARE`   | Set to `1` to share apps publicly via Cloudflare (same as `--cloudflare`)      |\n| `PORTLESS_STATE_DIR`    | Override the state directory                                                   |\n| `PORTLESS=0`            | Bypass the proxy, run the command directly                                     |\n\n### HTTP/2 + HTTPS\n\nHTTPS 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.\n\n```bash\nportless proxy start --cert ./c.pem --key ./k.pem  # Use custom certs\nportless proxy start --no-tls                       # Disable HTTPS (plain HTTP)\nportless trust                                      # Add CA to trust store later\n```\n\nOn 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.\n\n### LAN mode\n\n```bash\nportless proxy start --lan\nportless proxy start --lan --https\nportless proxy start --lan --ip 192.168.1.42\n```\n\n`--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.\n\nPortless 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.\n\nLAN 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).\n\n- **Next.js**: add your `.local` hostnames to `allowedDevOrigins`:\n\n  ```js\n  // next.config.js\n  module.exports = {\n    allowedDevOrigins: [\"myapp.local\", \"*.myapp.local\"],\n  };\n  ```\n\n- **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`.\n\n### Tailscale sharing\n\nShare dev servers with teammates on your Tailscale network using `--tailscale`, or expose to the public internet with `--funnel`:\n\n```bash\nportless myapp --tailscale next dev\n# -> https://myapp.localhost           (local)\n# -> https://devbox.yourteam.ts.net    (tailnet)\n\nportless myapp --funnel next dev\n# -> https://myapp.localhost           (local)\n# -> https://devbox.yourteam.ts.net    (public internet)\n```\n\nTailscale 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.\n\nEach `--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.\n\n### ngrok sharing\n\nExpose a dev server to the public internet with ngrok using `--ngrok`:\n\n```bash\nportless myapp --ngrok next dev\n# -> https://myapp.localhost           (local)\n# -> https://abc123.ngrok.app          (public internet)\n```\n\nSet `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>`.\n\n### Cloudflare sharing\n\n```bash\nportless myapp --cloudflare next dev\n# -> https://<random>.trycloudflare.com (public internet)\n```\n\n`--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/).\n\n## OS startup service\n\nUse the service command when users want the proxy to start automatically after reboot:\n\n```bash\nportless service install\nportless service install --lan\nportless service install --wildcard\nPORTLESS_STATE_DIR=~/.portless-lan PORTLESS_LAN=1 portless service install\nportless service status\nportless service uninstall\n```\n\nThe 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.\n\nThe 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.\n\n## CLI Reference\n\n| Command                                           | Description                                                    |\n| ------------------------------------------------- | -------------------------------------------------------------- |\n| `portless`                                        | Run dev script through proxy                                   |\n| `portless`                                        | From monorepo root: run all workspace packages                 |\n| `portless --script <name>`                        | Run a specific package.json script (default: dev)              |\n| `portless run [cmd] [args...]`                    | Infer name from project, run through proxy (auto-starts)       |\n| `portless run --name <name> <cmd>`                | Override inferred base name (worktree prefix still applies)    |\n| `portless <name> <cmd> [args...]`                 | Run app at `https://<name>.localhost` (auto-starts proxy)      |\n| `portless get <name>`                             | Print URL for a service (for cross-service wiring)             |\n| `portless get <name> --no-worktree`               | Print URL without worktree prefix                              |\n| `portless list`                                   | Show active routes and uptime                                  |\n| `portless doctor`                                 | Check proxy, routes, DNS, CA trust, and LAN prerequisites      |\n| `portless trust`                                  | Add local CA to system trust store (for HTTPS)                 |\n| `portless clean`                                  | Remove state, CA trust entry, and /etc/hosts block             |\n| `portless prune`                                  | Kill orphaned dev server groups; `run` also does it on start   |\n| `portless prune --force`                          | Kill orphans with SIGKILL instead of SIGTERM                   |\n| `portless proxy start`                            | Start HTTPS proxy as a daemon (port 443, auto-elevates)        |\n| `portless proxy start --no-tls`                   | Start without HTTPS (plain HTTP on port 80)                    |\n| `portless proxy start --lan`                      | Start in LAN mode (mDNS `.local`, auto-follows LAN IP changes) |\n| `portless proxy start -p <number>`                | Start the proxy on a custom port                               |\n| `portless proxy start --unprivileged`             | Plain HTTP on port 80 without sudo, nothing installed          |\n| `portless proxy start --tld test`                 | Use .test instead of .localhost                                |\n| `portless proxy start --tld localhost --tld test` | Serve both TLDs from one proxy                                 |\n| `portless proxy start --tld dev.example.com`      | Use a multi-segment TLD for production-parity URLs             |\n| `portless proxy start --foreground`               | Start the proxy in foreground (for debugging)                  |\n| `portless proxy start --wildcard`                 | Allow unregistered subdomains to fall back to parent route     |\n| `portless proxy stop`                             | Stop the proxy                                                 |\n| `portless service install`                        | Start the HTTPS proxy when the OS starts                       |\n| `portless service install --lan`                  | Start the service in LAN mode                                  |\n| `portless service install --wildcard`             | Persist wildcard routing in the startup service                |\n| `portless service status`                         | Show service and proxy status                                  |\n| `portless service uninstall`                      | Remove the startup service                                     |\n| `portless alias <name> <port>`                    | Register a static route (e.g. for Docker containers)           |\n| `portless alias <name> <port> --force`            | Overwrite an existing route                                    |\n| `portless alias --remove <name>`                  | Remove a static route                                          |\n| `portless hosts sync`                             | Reconcile routes with /etc/hosts (fixes Safari)                |\n| `portless hosts clean`                            | Remove portless entries from /etc/hosts                        |\n| `portless <name> --app-port <n> <cmd>`            | Use a fixed port for the app instead of auto-assignment        |\n| `portless <name> --tailscale <cmd>`               | Share the app on your Tailscale network (tailnet)              |\n| `portless <name> --funnel <cmd>`                  | Share the app publicly via Tailscale Funnel                    |\n| `portless <name> --ngrok <cmd>`                   | Share the app publicly via ngrok                               |\n| `portless <name> --cloudflare <cmd>`              | Share the app publicly via a Cloudflare quick tunnel           |\n| `portless <name> --force <cmd>`                   | Kill the existing process and take over its route              |\n| `portless --name <name> <cmd>`                    | Force `<name>` as app name (bypasses subcommand dispatch)      |\n| `portless <name> -- <cmd> [args...]`              | Stop flag parsing; everything after `--` is passed to child    |\n| `portless --help` / `-h`                          | Show help                                                      |\n| `portless run --help`                             | Show help for a subcommand (also: alias, hosts, clean)         |\n| `portless --version` / `-v`                       | Show version                                                   |\n\n**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.\n\n## MCP server\n\n`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.\n\n```json\n{ \"mcpServers\": { \"portless\": { \"command\": \"portless\", \"args\": [\"mcp\"] } } }\n```\n\n## portless.json\n\nOptional config file. Portless looks for it in the current directory.\n\n| Field     | Type    | Default                    | Description                                              |\n| --------- | ------- | -------------------------- | -------------------------------------------------------- |\n| `name`    | string  | inferred from package.json | Base app name (worktree prefix still applies)            |\n| `script`  | string  | `\"dev\"`                    | Name of a package.json script to run                     |\n| `appPort` | number  | auto-assigned              | Fixed port for the child process                         |\n| `proxy`   | boolean | auto-detected              | Whether to route through the proxy (`false` for tasks)   |\n| `apps`    | object  |                            | Overrides for workspace packages, keyed by relative path |\n| `turbo`   | boolean | `true`                     | Set `false` to use direct spawning instead of turborepo  |\n\nEach `apps` entry has the same shape (`name`, `script`, `appPort`, `proxy`). When `apps` is present, top-level fields apply only in single-app mode.\n\n### package.json \"portless\" key\n\nInstead of a separate `portless.json`, you can add a `\"portless\"` key to your `package.json`. A string value is shorthand for setting the name:\n\n```json\n{ \"portless\": \"myapp\" }\n```\n\nAn object supports all per-app fields (`name`, `script`, `appPort`, `proxy`):\n\n```json\n{ \"portless\": { \"name\": \"myapp\", \"script\": \"dev:app\" } }\n```\n\nPrecedence (closest wins): CLI flags > package.json `\"portless\"` key > portless.json app entry > defaults.\n\n## Troubleshooting\n\n### Run diagnostics\n\nUse `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.\n\n### Proxy not running\n\nThe proxy auto-starts when you run an app with `portless <name> <cmd>`. If it doesn't start (e.g. port conflict), start it manually:\n\n```bash\nportless proxy start\n```\n\n### Port already in use\n\nAnother process is bound to the proxy port. Either stop it first, or use a different port:\n\n```bash\nportless proxy start -p 8080\n```\n\n### Framework not respecting PORT\n\nPortless 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.\n\nFor other frameworks that don't read `PORT`, pass the port manually:\n\n- **Webpack Dev Server**: use `--port $PORT`\n- **Custom servers**: read `process.env.PORT` and listen on it\n\n### Permission errors\n\nThe 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.\n\n```bash\nportless proxy start --https           # Auto-elevates with sudo for port 443\nportless proxy start -p 1355 --https   # No sudo needed (URLs include :1355)\nportless proxy start --unprivileged    # Plain HTTP on port 80, no sudo, clean URLs\nportless proxy stop                    # Stop (use sudo if started with sudo)\n```\n\n### Safari can't find .localhost URLs\n\nSafari 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.\n\nFix:\n\n```bash\nportless hosts sync    # Reconcile current routes with /etc/hosts\nportless hosts clean   # Remove entries later\n```\n\nAuto-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`.\n\nManual sync reconciles portless-managed entries with current routes and removes stale entries when there are no routes. It requires a successful initial hosts-file read before writing and verifies each write. A read or verification failure follows the normal sync error path.\n\n### Browser shows certificate warning with --https\n\nThe local CA may not be trusted yet. Run:\n\n```bash\nportless trust\n```\n\nThis adds the portless local CA to your system trust store. After that, restart the browser.\n\n### Remove portless from the machine\n\n```bash\nportless clean\n```\n\nStops the proxy if needed, removes the portless CA from the trust store (when portless added it), deletes known files under state directories, and removes the portless `/etc/hosts` block. May require `sudo` on macOS/Linux. If trust-store removal fails, portless retains its CA certificate and key so a later `portless clean` can safely retry.\n\n### Proxy loop (508 Loop Detected)\n\nIf your dev server proxies requests to another portless app (e.g. Vite proxying `/api` to `api.myapp.localhost`), the proxy must rewrite the `Host` header. Without this, portless routes the request back to the original app, creating an infinite loop.\n\nFix: set `changeOrigin: true` in the proxy config (Vite, webpack-dev-server, etc.):\n\n```ts\n// vite.config.ts\nproxy: {\n  \"/api\": {\n    target: \"https://api.myapp.localhost\",\n    changeOrigin: true,\n    ws: true,\n  },\n}\n```\n\nPortless automatically sets `NODE_EXTRA_CA_CERTS` in child processes so Node.js trusts the portless CA. If you run a separate Node.js process outside portless, point it at the CA manually: `NODE_EXTRA_CA_CERTS=~/.portless/ca.pem`. Alternatively, use `--no-tls` for plain HTTP.\n\n### Tailscale not working\n\nIf `--tailscale` or `--funnel` fails:\n\n```bash\ntailscale status     # Check if connected\ntailscale up         # Connect to your tailnet\n```\n\nRequires the Tailscale CLI to be installed (https://tailscale.com/download) and on PATH.\n\n### ngrok not working\n\nIf `--ngrok` fails:\n\n```bash\nngrok version                         # Check if installed\nngrok config add-authtoken <token>    # Configure authentication\n```\n\nRequires the ngrok CLI to be installed (https://ngrok.com/download) and on PATH.\n\n### Requirements\n\n- Node.js 24+\n- macOS, Linux, or Windows\n- `openssl` (for `--https` cert generation; ships with macOS and most Linux distributions; on Windows, install via `winget install -e --id ShiningLight.OpenSSL.Dev` or use the copy bundled with Git for Windows)\n- `tailscale` CLI (optional, for `--tailscale` and `--funnel`)\n- `ngrok` CLI (optional, for `--ngrok`)\n- `cloudflared` CLI (optional, for `--cloudflare`)\n</yakaai_skills>"}