vercel-labs-skills
The CLI for the open agent skills ecosystem. Supports OpenCode, Claude Code, Codex, Cursor, and 75 more.
Replace port numbers with stable, named .localhost URLs for local development. For humans and agents.
$ npx -y skills add vercel-labs/portless --agent claude-code
Repo: vercel-labs/portless
What's inside
Replace port numbers with stable, named .localhost URLs for local development. For humans and agents.
- "dev": "next dev" # http://localhost:3000
+ "dev": "portless run next dev" # https://myapp.localhost
Global (recommended):
npm install -g portless
Or as a project dev dependency:
npm install -D portless
portless is pre-1.0. When installed per-project, different contributors may run different versions. The state directory format may change between releases, which can require re-running
portless trust.
portless myapp next dev
# -> https://myapp.localhost
HTTPS with HTTP/2 is enabled by default. On first run, portless generates a local CA, trusts it, and binds port 443 (auto-elevates with sudo on macOS/Linux). Use --no-tls for plain HTTP.
The proxy auto-starts when you run an app. A random port (4000-4999) is assigned via the PORT environment variable. Most frameworks (Next.js, Express, Nuxt, etc.) respect this automatically. For frameworks that ignore PORT (Vite, VitePlus, Astro, React Router, Angular, Expo, React Native), portless auto-injects the right --port flag and, when needed, a matching --host flag. 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.
When auto-starting, portless 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 (PORTLESS_PORT, PORTLESS_HTTPS, etc.) always take priority.
Portless stores per-user state in ~/.portless. When the proxy runs under sudo, it resolves this path from the invoking user's home so the proxy and unprivileged app processes share the same route registrations.
In non-interactive environments (no TTY, or CI=1), portless exits with a descriptive error instead of prompting, so task runners like turborepo and CI scripts fail early with a clear message.
Bare portless works out of the box. It runs the "dev" script from package.json through the proxy, inferring the app name from the package name, git root, or directory:
portless # -> runs "dev" script, https://<project>.localhost
Use an optional portless.json to override defaults:
{ "name": "myapp" }
portless # -> runs "dev" script, https://myapp.localhost
The script defaults to "dev". The name is inferred from package.json if not set in config.
One portless.json at the repo root covers all workspace packages. Portless discovers packages from pnpm-workspace.yaml, or the "workspaces" field in package.json (npm, yarn, bun):
{
"apps": {
"apps/web": { "name": "myapp" },
"apps/api": { "name": "api.myapp" }
}
}
portless # from repo root: starts all workspace packages with a "dev" script
cd apps/web && portless # start just one package
The apps map is optional and only needed for name overrides. Packages not listed still auto-discover with names inferred from their package.json.
Without an apps map, hostnames follow the <package>.<project>.localhost convention. The project name comes from the most common npm scope across workspace packages (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 gets the bare <project>.localhost without duplication.
| Field | Type | Default | Description |
|---|---|---|---|
name | string | inferred | Base app name. Worktree prefix still applies. |
script | string | "dev" | Name of a package.json script to run. |
appPort | number | auto | Fixed port for the child process. |
proxy | boolean | auto | Whether to route through the proxy. Auto-detected. |
apps | object | Overrides for workspace packages, keyed by relative path. | |
turbo | boolean | true | Set false to use direct spawning instead of turborepo. |
Instead of a separate portless.json, you can add a "portless" key to your package.json. A string value is shorthand for setting the name:
{
"name": "@myorg/web",
"portless": "myapp"
}
An object supports all per-app fields (name, script, appPort, proxy):
{
"name": "@myorg/web",
"portless": { "name": "myapp", "script": "dev:app" }
}
The package.json "portless" key takes precedence over portless.json app entries but is overridden by CLI flags.
Override the default script for a single invocation:
portless --script start # run "start" instead of "dev"
portless --script test # run "test" instead of "dev"
To use portless with turborepo, put portless as the dev script and the real command in a separate script:
{
"scripts": {
"dev": "portless",
"dev:app": "next dev"
},
"portless": { "name": "myapp", "script": "dev:app" }
}
Turbo runs each package's dev script, which invokes portless. Portless reads the config, detects the package manager, and runs pnpm run dev:app (or yarn/bun/npm) through the proxy. No changes to turbo.json or turbo.jsonc are needed.
pnpm dev at the root works through turbo as usual. People without portless can run pnpm run dev:app directly.
When portless runs from a workspace root, it uses the existing Turbo integration to preserve task ordering when either turbo.json or turbo.jsonc is readable. Set "turbo": false in the root portless configuration to use direct spawning instead.
You can still use portless in package.json scripts:
{
"scripts": {
"dev": "portless run next dev"
}
}
With a portless.json, you can simplify to:
{
"scripts": {
"dev": "next dev"
}
}
Then run portless or portless run to go through the proxy.
When you press Ctrl+C, portless forwards the interrupt and waits for the command's process tree to exit. Press Ctrl+C again to forward another interrupt. Any remaining descendants are terminated after a short grace period.
Organize services with subdomains:
portless api.myapp pnpm start
# -> https://api.myapp.localhost
portless docs.myapp next dev
# -> https://docs.myapp.localhost
By default, only explicitly registered subdomains are routed (strict mode). Use --wildcard when starting the proxy to allow any subdomain of a registered route to fall back to that app (e.g. tenant1.myapp.localhost routes to the myapp app without extra registration).
portless run automatically detects git worktrees. In a linked worktree, the branch name is prepended as a subdomain so each worktree gets its own URL without any config changes:
# Main worktree (no prefix)
portless run next dev # -> https://myapp.localhost
# Linked worktree on branch "fix-ui"
portless run next dev # -> https://fix-ui.myapp.localhost
Use --name to override the inferred base name while keeping the worktree prefix:
portless run --name myapp next dev # -> https://fix-ui.myapp.localhost
Put portless run in your package.json once and it works everywhere. The main checkout uses the plain name, each worktree gets a unique subdomain. No collisions, no --force.
By default, portless uses .localhost which auto-resolves to 127.0.0.1 in most browsers. If you prefer a different TLD (e.g. .test), use --tld:
portless proxy start --tld test
portless myapp next dev
# -> https://myapp.test
The proxy auto-syncs /etc/hosts for route hostnames (including .test), so those domains resolve on your machine.
Repeat --tld to serve the same app names under multiple TLDs from one proxy:
portless proxy start --tld localhost --tld test
portless myapp next dev
# -> https://myapp.localhost
# -> https://myapp.test
When multiple TLDs are configured, PORTLESS_URL uses the first TLD. PORTLESS_TLD also accepts a comma separated list, e.g. PORTLESS_TLD=localhost,test.
Recommended: .test (IANA-reserved, no collision risk). Avoid .local (conflicts with mDNS/Bonjour) and .dev (Google-owned, forces HTTPS via HSTS).
The --tld value accepts a lowercase DNS name (one or more dot-separated labels, no trailing dot), so a domain you own can be used as the "TLD". This gives local URLs the same structure as production, which keeps OAuth redirect URIs, cross-subdomain cookies, and host-based routing working the same way in both environments:
portless proxy start --tld dev.example.com
portless myapp next dev
# -> https://myapp.dev.example.com
Each label must follow DNS rules: lowercase letters, digits, and interior hyphens, with at most 63 characters per label and 253 characters total. The full hostname (app.TLD) is also subject to the 253-character DNS limit.
The proxy auto-syncs /etc/hosts for registered hostnames, so myapp.dev.example.com resolves to 127.0.0.1 on your machine. This is a loopback-only setup: outside LAN mode the proxy binds only to 127.0.0.1 and ::1 (see below), so a custom TLD is reachable only from the machine running the proxy. Reaching the proxy from other devices requires LAN mode (--lan), but LAN mode serves apps under the .local TLD and ignores a custom --tld, so the two cannot be combined today.
Strict OAuth providers (Google, Apple) reject .localhost and .test redirect URIs but accept a real domain, so https://myapp.dev.example.com/api/auth/callback/google works as a redirect URI.
The CLI for the open agent skills ecosystem. Supports OpenCode, Claude Code, Codex, Cursor, and 75 more.
Give coding agents access to any package's source code.
Comprehensive Vercel ecosystem plugin — relational knowledge graph, skills for every major product, specialized agents, and Vercel conventions. Turns any AI agent into a Vercel expert.
FAQ
portless is a Claude Code plugin with 2 hand-picked skills for development work, indexed on Flowy. Install it with the command on its page. It includes oauth, portless. Its skills do not fire on their own yet. Request auto-invocation to have Flowy route them as you prompt. Free and open source.
Is this plugin yours?
Claim it with GitHubSubmit a pluginPromote it