{"slug":"portless-ops","title":"portless-ops","summary":"Portless local-dev HTTPS proxy: replaces port numbers with named URLs (Caddy/nginx alternative for local dev). Triggers on: portless, local https proxy, named localhost URL, custom TLD, portless alias, portless.json, local CA trust, boot persistence, monorepo routing, Tailscale d","platform":"Claude","tags":[],"authorName":"LLM Mart","authorSlug":"llm-mart","score":0,"source":"github","price":null,"verified":false,"createdAt":"2026-09-30T19:36:51.074314Z","repo":{"url":"https://github.com/0xDarkMatter/claude-mods","stars":43,"forks":7,"license":"MIT","updatedAt":"2026-09-30T15:18:48Z"},"bodyHtml":"<hr>\n<h2>name: portless-ops\ndescription: \"Portless local-dev HTTPS proxy: replaces port numbers with named URLs (Caddy/nginx alternative for local dev). Triggers on: portless, local https proxy, named localhost URL, custom TLD, portless alias, portless.json, local CA trust, boot persistence, monorepo routing, Tailscale dev sharing.\"\nlicense: MIT\nallowed-tools: \"Read Write Bash Edit\"\nmetadata:\nauthor: claude-mods\nrelated-skills: process-compose-ops, mcp-ops, cli-ops\nupstream: <a href=\"https://github.com/vercel-labs/portless\">https://github.com/vercel-labs/portless</a></h2>\n<h1>Portless Operations</h1>\n<p>Portless (Vercel Labs) is a local-dev HTTPS proxy that replaces port numbers with named URLs. Replacement for Caddy/nginx in the local-dev role; not for production.</p>\n<p><strong>Upstream:</strong> <a href=\"https://github.com/vercel-labs/portless\">vercel-labs/portless</a> (Apache-2.0). The portless repo ships canonical skills in its source tree (not in the npm package). Verbatim copies kept in <code>references/</code>:</p>\n<ul>\n<li><strong><a href=\"references/upstream-portless.md\"><code>references/upstream-portless.md</code></a></strong> — full CLI reference, integration patterns (zero-config, monorepo, turborepo, worktrees, Tailscale), HTTPS/LAN setup, troubleshooting</li>\n<li><strong><a href=\"references/upstream-oauth.md\"><code>references/upstream-oauth.md</code></a></strong> — OAuth provider compatibility (Google, Apple, Microsoft, Facebook, GitHub), TLD selection for OAuth, callback URI configuration</li>\n</ul>\n<p>This SKILL.md adds <strong>operational patterns</strong> we've validated in production (Windows specifics, the static-alias-with-supervisor pattern, TLD-reset procedure, supply-chain hygiene). For canonical CLI usage, prefer the upstream reference files.</p>\n<h2>Mental Model</h2>\n<table>\n<thead>\n<tr>\n<th>Layer</th>\n<th>Portless owns</th>\n<th>Portless does NOT own</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Routing</td>\n<td>hostname → port mapping, HTTPS termination, HTTP/2, CA trust</td>\n<td>process supervision (use Process Compose or PM2)</td>\n</tr>\n<tr>\n<td>Naming</td>\n<td><code>&lt;name&gt;.&lt;tld&gt;</code> shape — one TLD per proxy</td>\n<td>per-service distinct TLDs (not supported)</td>\n</tr>\n<tr>\n<td>Process spawning</td>\n<td>when invoked as <code>portless myapp &lt;cmd&gt;</code></td>\n<td>crash recovery, restart policy, health checks</td>\n</tr>\n</tbody>\n</table>\n<p><strong>Key shape constraint:</strong> portless always renders <code>&lt;alias-name&gt;.&lt;tld&gt;</code>. You can't mix two TLDs in one proxy because TLD is per-instance — a dotted alias like <code>portless alias api.&lt;app&gt; 8108</code> gets the TLD appended → <code>api.&lt;app&gt;.&lt;tld&gt;</code>.</p>\n<h2>Install</h2>\n<pre><code># Pin a specific version (zero runtime deps, low supply-chain surface)\nnpm install -g portless@0.13.0\n\n# Verify\nportless --version\n</code></pre>\n<p>Record the pinned version in your repo. Upgrades are explicit PRs.</p>\n<h2>CLI Quick Reference</h2>\n<pre><code># example values — substitute your own (TLD, app name, ports)\n# Proxy lifecycle\nportless proxy start --tld lab --port 443   # HTTPS proxy on 443, *.lab routes\nportless proxy start --tld test --port 1355 # Non-privileged port for testing\nportless proxy stop\nportless trust                              # Add CA to system trust store\n\n# Aliases (for services portless didn't spawn — PM2, Process Compose, Docker, etc.)\nportless alias axiom 8108                   # https://axiom.lab → :8108\nportless alias axiom 8108 --force           # Overwrite existing\nportless alias --remove axiom               # Note: appends TLD! be careful\n\n# Spawn-mode (portless manages the process)\nportless myapp next dev                     # https://myapp.lab, auto port 4000-4999\nportless run pnpm dev                       # Auto-infer name from package.json\n\n# Discovery (agent-friendly)\nportless list                               # Active routes\nportless get axiom                          # Returns: https://axiom.lab\n\n# Boot persistence\nportless service install                    # OS-native startup task\nportless service status\nportless service uninstall\n</code></pre>\n<h2>The Static-Alias Pattern (portless + external process supervisor)</h2>\n<p>The common pattern: a process supervisor (Process Compose, PM2, Docker) runs your dev servers on fixed ports. Portless just routes named URLs to those ports.</p>\n<pre><code># Started by Process Compose, listening on &lt;your-port&gt;\n# Now make it reachable at https://&lt;your-app&gt;.&lt;your-tld&gt;\nportless alias &lt;your-app&gt; &lt;your-port&gt;\n</code></pre>\n<p>Decoupling means:</p>\n<ul>\n<li>Restart the dev server (<code>pm2 restart &lt;your-app&gt;</code>, <code>process-compose process restart &lt;your-app&gt;</code>) → portless keeps routing transparently</li>\n<li>Swap one supervisor for another → portless layer is untouched</li>\n</ul>\n<p><strong>Source of truth pattern:</strong> keep alias registration in your supervisor config. Example <code>scripts/install.ps1</code>:</p>\n<pre><code>$services = (yq '.processes | keys | .[]' process-compose.yaml)\nforeach ($svc in $services) {\n  $port = (yq \".processes.$svc.readiness_probe.http_get.port\" process-compose.yaml)\n  if ($port -and $port -ne \"null\") {\n    portless alias $svc $port --force\n  }\n}\n</code></pre>\n<h2>TLD Selection</h2>\n<table>\n<thead>\n<tr>\n<th>TLD</th>\n<th>When to use</th>\n<th>Caveats</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>.localhost</code> (default)</td>\n<td>Quickest start</td>\n<td>Auto-resolves to 127.0.0.1 on most systems</td>\n</tr>\n<tr>\n<td><code>.lab</code></td>\n<td>Personal/distinctive</td>\n<td>Not IANA-reserved (no DNS collision in practice for local)</td>\n</tr>\n<tr>\n<td><code>.test</code></td>\n<td>OAuth-friendly</td>\n<td>IANA-reserved; safe</td>\n</tr>\n<tr>\n<td><code>.dev</code></td>\n<td>OAuth (Google, Apple)</td>\n<td>Google-owned, forces HTTPS — portless handles this fine</td>\n</tr>\n<tr>\n<td><code>.local</code></td>\n<td>Avoid</td>\n<td>mDNS/Bonjour conflict</td>\n</tr>\n</tbody>\n</table>\n<p>OAuth providers reject <code>.localhost</code> subdomains (not in Public Suffix List). Switch to <code>--tld test</code> or <code>--tld dev</code> for OAuth dev work. See <a href=\"references/upstream-oauth.md\"><code>references/upstream-oauth.md</code></a> for full per-provider setup.</p>\n<h2>Reset (clean slate)</h2>\n<pre><code># Stop proxy\nportless proxy stop\n\n# Wipe all aliases (routes.json)\nrm ~/.portless/routes.json    # Linux/macOS\nRemove-Item \"$env:USERPROFILE\\.portless\\routes.json\"   # PowerShell\n\n# Start fresh with desired TLD\nportless proxy start --tld &lt;tld&gt; --port 443\n\n# Re-register aliases from your supervisor config\n</code></pre>\n<p>This is the right pattern when you change TLD — <code>portless alias --remove</code> appends the active TLD which makes it fight you.</p>\n<h2>Windows-Specific Notes</h2>\n<h3><code>openssl</code> required on PATH</h3>\n<p>Portless uses OpenSSL to generate the local CA. Git for Windows ships it:</p>\n<pre><code># Persistent: add to user PATH\n$gitBin = \"C:\\Program Files\\Git\\usr\\bin\"\n$current = [Environment]::GetEnvironmentVariable(\"PATH\", \"User\")\nif ($current -notlike \"*$gitBin*\") {\n    [Environment]::SetEnvironmentVariable(\"PATH\", \"$gitBin;$current\", \"User\")\n}\n</code></pre>\n<p>Without it: <code>Error: openssl failed: spawnSync openssl ENOENT</code></p>\n<h3>Boot persistence</h3>\n<p><code>portless service install</code> registers a Task Scheduler entry. Pair it with your supervisor's own boot task (e.g., for Process Compose, register a separate task via <code>scripts/boot-task-install.ps1</code>).</p>\n<p>Verify both registered:</p>\n<pre><code>Get-ScheduledTask | Where-Object {\n    $_.TaskName -like \"*ortless*\" -or $_.TaskName -like \"*ompose*\"\n}\n</code></pre>\n<h3>curl vs browser cert handling</h3>\n<p>curl on Windows uses its own bundled CA store, not the system one. So <code>curl https://&lt;your-app&gt;.&lt;your-tld&gt;/</code> returns code 000 (cert untrusted) even after <code>portless trust</code>. Browsers work fine because they use the system store.</p>\n<p>Test from curl with <code>-k</code> (skip verify), or <code>--cacert ~/.portless/ca.pem</code>:</p>\n<pre><code>curl -k https://&lt;your-app&gt;.&lt;your-tld&gt;/        # quick test\ncurl --cacert ~/.portless/ca.pem https://&lt;your-app&gt;.&lt;your-tld&gt;/   # proper\n</code></pre>\n<h2>Common Errors</h2>\n<table>\n<thead>\n<tr>\n<th>Error</th>\n<th>Cause</th>\n<th>Fix</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>openssl failed: spawnSync openssl ENOENT</code></td>\n<td>OpenSSL not on PATH</td>\n<td>Add Git's <code>usr/bin</code> to PATH</td>\n</tr>\n<tr>\n<td><code>Error: No alias found for \"foo.lab\"</code> (you asked for <code>foo</code>)</td>\n<td><code>--remove</code> appends TLD; sometimes adds an extra</td>\n<td>Wipe <code>routes.json</code> and re-register</td>\n</tr>\n<tr>\n<td>Browser shows cert warning</td>\n<td>CA not in system trust store</td>\n<td>Re-run <code>portless trust</code> (may need admin)</td>\n</tr>\n<tr>\n<td><code>https://name.lab</code> shows \"No app registered\"</td>\n<td>Alias not set or proxy stopped</td>\n<td><code>portless list</code> to confirm; re-register if needed</td>\n</tr>\n<tr>\n<td>Safari can't resolve <code>*.lab</code></td>\n<td>Safari uses system DNS, not Node's resolver</td>\n<td><code>portless hosts sync</code> to write /etc/hosts</td>\n</tr>\n<tr>\n<td>Port 443 conflict on <code>portless proxy start</code></td>\n<td>Another service bound (Caddy, IIS)</td>\n<td>Stop the other service, or use <code>--port 1355</code> for testing</td>\n</tr>\n</tbody>\n</table>\n<h2>Worked Example: Replacing Caddy with portless</h2>\n<p>A PM2+Caddy to Process Compose+portless migration is worth keeping in its own small repo (e.g. <code>~/infra/local-stack/</code>), with these key files:</p>\n<ul>\n<li><code>process-compose.yaml</code> — supervisor config with health-checked services</li>\n<li><code>scripts/cutover.ps1</code> — stops PM2/Caddy, starts portless+PC, registers aliases</li>\n<li><code>docs/MIGRATION-LOG.md</code> — every issue hit during cutover and how it was solved</li>\n<li><code>docs/SUPPLY-CHAIN.md</code> — pinning + verification procedures</li>\n</ul>\n<h2>Anti-Patterns</h2>\n<pre><code>BAD:  portless alias name 8000; portless alias name 8001   # second silently fails without --force\nGOOD: portless alias name 8001 --force\n\nBAD:  use portless as production reverse proxy\nGOOD: keep portless as dev-only; production = nginx/Caddy/cloud LB\n\nBAD:  rely on portless for crash recovery (it has none for spawned processes)\nGOOD: pair portless with Process Compose / PM2 / supervisord for supervision\n\nBAD:  change TLD by stopping/starting with different --tld and hoping aliases update\nGOOD: stop proxy, wipe routes.json, start with new TLD, re-register from supervisor config\n</code></pre>\n<h2>Resources in this skill</h2>\n<h3><code>references/</code></h3>\n<ul>\n<li><code>upstream-portless.md</code> — canonical portless SKILL.md verbatim (CLI ref, monorepo, turborepo, worktrees, LAN, Tailscale, HTTPS, troubleshooting)</li>\n<li><code>upstream-oauth.md</code> — canonical OAuth setup for Google/Apple/Microsoft/Facebook/GitHub</li>\n<li><code>tld-selection.md</code> — decision tree for picking the right TLD; trade-offs of <code>.test</code>/<code>.dev</code>/<code>.localhost</code>/custom-owned</li>\n<li><code>windows-specifics.md</code> — openssl PATH, certutil quirks, curl-vs-browser cert handling, PS 5.1 gotchas</li>\n<li><code>integration-patterns.md</code> — combos with Process Compose / Docker / PM2 / Tailscale / git worktrees</li>\n</ul>\n<h3><code>scripts/</code></h3>\n<ul>\n<li><code>install-portless.ps1</code> — verified install: inspect tarball, scan for IOCs from recent attacks, install only if clean</li>\n<li><code>reset-state.ps1</code> — clean state reset (used when changing TLD; <code>--remove</code> can't clear old-TLD aliases)</li>\n<li><code>sync-aliases-from-yaml.ps1</code> — derive portless aliases from a process-compose.yaml</li>\n</ul>\n<h3><code>assets/</code></h3>\n<ul>\n<li><code>portless.json.simple.json</code> — single-app config template</li>\n<li><code>portless.json.monorepo.json</code> — workspace monorepo with name overrides</li>\n<li><code>portless.json.with-custom-tld.json</code> — documents TLD choice in repo</li>\n<li><code>package.json-portless-key.json</code> — alternative: portless config inside package.json</li>\n</ul>\n<h2>Related Skills</h2>\n<ul>\n<li><code>process-compose-ops</code> — the supervisor we pair with portless</li>\n<li><code>mcp-ops</code> — agent-friendly tooling; portless <code>get &lt;name&gt;</code> provides URL discovery for agents</li>\n<li><code>cli-ops</code> — general CLI tool patterns</li>\n</ul>\n","files":[{"path":"assets/package.json-portless-key.json","sizeBytes":797,"isText":true},{"path":"assets/portless.json.monorepo.json","sizeBytes":680,"isText":true},{"path":"assets/portless.json.simple.json","sizeBytes":386,"isText":true},{"path":"assets/portless.json.with-custom-tld.json","sizeBytes":493,"isText":true},{"path":"references/integration-patterns.md","sizeBytes":5378,"isText":true},{"path":"references/tld-selection.md","sizeBytes":4313,"isText":true},{"path":"references/upstream-oauth.md","sizeBytes":7834,"isText":true},{"path":"references/upstream-portless.md","sizeBytes":22638,"isText":true},{"path":"references/windows-specifics.md","sizeBytes":5194,"isText":true},{"path":"scripts/install-portless.ps1","sizeBytes":5695,"isText":false},{"path":"scripts/reset-state.ps1","sizeBytes":2431,"isText":false},{"path":"scripts/sync-aliases-from-yaml.ps1","sizeBytes":1618,"isText":false},{"path":"SKILL.md","sizeBytes":10528,"isText":true},{"path":"tests/run.sh","sizeBytes":7007,"isText":true}],"reviewScore":null,"reviewSummary":null,"trust":{"provenance":"trusted-source-unreviewed","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow.","bodySource":null},"bodyLocked":false,"purchaseUrl":null,"sourceUrl":null,"report":{"provenance":"trusted-source-unreviewed","screen":{"ran":true,"outcome":"notes-only","suspicious":0,"notes":1,"hiddenCharacters":false},"virusScan":{"engine":"clamav","status":"clean","scannedAt":"2026-09-30T19:38:28.181158Z","sha256":"125DE50CBE8981D1282339368F2CDA23EC1A8F219016ECC056C2C9C5DFA187C7","sizeBytes":31990},"review":null,"source":{"repositoryUrl":"https://github.com/0xDarkMatter/claude-mods","path":"skills/portless-ops","license":"MIT","commit":"3dfaf0ba5753026a99ee13f9d9ed56b9793bb6e8","subtreeSha":"0FED2E8AEF81DA682C66048017BDBE79896368D6636ECC943B9DDF2A2097796D","lastSyncedAt":"2026-09-30T19:37:28.226022Z"},"reviewedAt":"2026-09-30T19:41:17.581569Z","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow."},"install":[{"target":"skills-cli","command":"npx skills add https://github.com/0xDarkMatter/claude-mods/tree/main/skills/portless-ops"},{"target":"claude-code","command":"claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install 0xdarkmatter-claude-mods@llmmart"},{"target":"git","command":"git clone https://github.com/0xDarkMatter/claude-mods.git"}]}