pinggy-tunnel – DevOps & Infrastructure skill for AI agents
Zero-install localhost tunnels over SSH via Pinggy.
Install the pinggy-tunnel skill with one command
Pick the tool you use, copy the command and paste it in your terminal. The skill lands in that tool's skills folder and is ready in your next session.
Claude Code
mkdir -p ~/.claude/skills && curl -fsSL https://skill.dn1nguoi.com/z/pinggy-tunnel-ca8aa.tgz | tar xz -C ~/.claude/skills
Call it in Claude Code: /pinggy-tunnel – then describe the task you want done.
Codex
mkdir -p ~/.codex/skills && curl -fsSL https://skill.dn1nguoi.com/z/pinggy-tunnel-ca8aa.tgz | tar xz -C ~/.codex/skills
Call it in Codex: $pinggy-tunnel – then describe the task you want done.
Hermes Agent
mkdir -p ~/.hermes/skills && curl -fsSL https://skill.dn1nguoi.com/z/pinggy-tunnel-ca8aa.tgz | tar xz -C ~/.hermes/skills
Call it in Hermes Agent: /pinggy-tunnel – then describe the task you want done.
DeepSeek
mkdir -p ~/.deepcode/skills && curl -fsSL https://skill.dn1nguoi.com/z/pinggy-tunnel-ca8aa.tgz | tar xz -C ~/.deepcode/skills
Call it in DeepSeek: /pinggy-tunnel – then describe the task you want done.
SKILL.md content
Pinggy Tunnel Skill
Expose a local service (dev server, webhook receiver, MCP endpoint, demo) to the public internet using a Pinggy SSH reverse tunnel. No daemon to install — the user's stock SSH client connects to a.pinggy.io:443 and Pinggy hands back a public HTTP/HTTPS URL.
Free tier: 60-minute tunnels, random subdomain, no signup. Pro tier ($3/mo) is an opt-in with a token.
When to Use
- User asks to "expose this locally", "share my dev server", "make this URL public", "tunnel port N", "get a public URL for a webhook"
- Need to receive a webhook callback during a local task (Stripe, GitHub, Discord, AgentMail)
- Sharing a one-off HTTP demo (MCP server, Ollama/vLLM endpoint, dashboard) with a remote party
- The host has SSH but no
cloudflared/ngrokbinary, and installing one would be overkill
If the host already has cloudflared configured, prefer the cloudflared-quick-tunnel skill — Cloudflare quick tunnels don't expire after 60 minutes.
Prerequisites
sshon PATH (ssh -V). Default on Linux, macOS, and Windows 10+. No other install.- A local service listening on
127.0.0.1:<port>before the tunnel starts. Pinggy will return URLs but they'll 502 until the local origin is up.
Optional:
PINGGY_TOKENenv var for paid Pro features (persistent subdomain, custom domain, multiple tunnels, no 60-minute cap). Free tier needs no credentials.
Quick Reference
# Plain HTTP/HTTPS tunnel for port 8000 (free tier)
ssh -p 443 -o StrictHostKeyChecking=no -o ServerAliveInterval=30 \
-R0:localhost:8000 [email protected]
# TCP tunnel (databases, raw SSH, etc.)
ssh -p 443 -o StrictHostKeyChecking=no -R0:localhost:5432 [email protected]
# TLS tunnel (Pinggy can't decrypt — bring your own certs at origin)
ssh -p 443 -o StrictHostKeyChecking=no -R0:localhost:443 [email protected]
# Basic auth gate (b:user:pass)
ssh -p 443 -o StrictHostKeyChecking=no -R0:localhost:8000 \
"b:admin:[email protected]"
# Bearer token gate (k:token)
ssh -p 443 -o StrictHostKeyChecking=no -R0:localhost:8000 \
"k:[email protected]"
# IP whitelist (w:CIDR)
ssh -p 443 -o StrictHostKeyChecking=no -R0:localhost:8000 \
"w:203.0.113.0/[email protected]"
# Enable CORS + force HTTPS redirect
ssh -p 443 -o StrictHostKeyChecking=no -R0:localhost:8000 \
"co+x:[email protected]"
# Pro tier (persistent URL, no 60-min cap)
ssh -p 443 -o StrictHostKeyChecking=no -R0:localhost:8000 "$PINGGY_TOKEN+a.pinggy.io"Procedure — Start a Tunnel and Get the URL
The model SHOULD use the terminal tool. The tunnel must stay alive for the duration of the share, so run it as a background process and parse the public URL from stdout.
1. Confirm a local origin is up
curl -sI http://127.0.0.1:8000/ | head -1
# expect HTTP/1.x 200 (or any non-connection-refused response)If nothing is listening yet, start it first (e.g. python -m http.server 8000 --bind 127.0.0.1). Pinggy will happily return a URL pointed at nothing — the user will see 502 until the origin comes up.
2. Launch the tunnel as a background process
Use terminal(background=True) and capture output to a logfile (Pinggy prints the URLs on stdout, then keeps the connection open):
LOG=~/.hermes/cache/scratch/pinggy-8000.log
nohup ssh -p 443 \
-o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null \
-o ServerAliveInterval=30 \
-o ServerAliveCountMax=3 \
-R0:localhost:8000 [email protected] \
> "$LOG" 2>&1 &
echo $! > ~/.hermes/cache/scratch/pinggy-8000.pidStrictHostKeyChecking=no + UserKnownHostsFile=/dev/null skips the first-run host-key prompt. ServerAliveInterval=30 keeps the SSH session from getting torn down by an idle NAT.
3. Parse the URL out of the log
sleep 4
grep -oE 'https://[a-z0-9-]+\.[a-z]+\.pinggy\.link' ~/.hermes/cache/scratch/pinggy-8000.log | head -1Expected output looks like:
You are not authenticated.
Your tunnel will expire in 60 minutes.
http://yqycl-98-162-69-48.a.free.pinggy.link
https://yqycl-98-162-69-48.a.free.pinggy.linkHand the https://...pinggy.link URL to the user.
4. Verify
curl -sI https://<the-url>/ | head -3
# expect 200/302/whatever the local origin actually returnsIf you get 502 Bad Gateway, the SSH session is up but the local origin isn't listening — fix step 1 first.
5. Teardown
kill "$(cat ~/.hermes/cache/scratch/pinggy-8000.pid)"
# or, if the pid file got lost:
pkill -f 'ssh -p 443 .* free@a\.pinggy\.io'If you have a session_id from terminal(background=True), prefer process(action='kill', session_id=...).
Access Control via Username Keywords
Pinggy stacks control flags into the SSH username separated by +. Always quote the whole user@host argument when it contains a +:
| Keyword | Effect |
|---------|--------|
| b:user:pass | HTTP Basic auth gate |
| k:token | Bearer-token header gate (Authorization: Bearer <token>) |
| w:CIDR | IP whitelist (single IP or CIDR, repeatable) |
| co | Add Access-Control-Allow-Origin: * (CORS) |
| x:https | Force HTTPS — auto-redirect HTTP to HTTPS |
| a:Name:Value | Add request header |
| u:Name:Value | Update request header |
| r:Name | Remove request header |
| qr | Print a QR code of the URL to stdout (handy for mobile sharing) |
Combine freely: "b:admin:secret+co+x:[email protected]".
Web Debugger (optional)
Pinggy can mirror the inbound traffic to localhost:4300 for inspection. Add a local forward to the SSH command:
ssh -p 443 -L4300:localhost:4300 -R0:localhost:8000 [email protected]Then open http://localhost:4300 in a browser to see live request/response pairs.
Pitfalls
- 60-minute hard cap on the free tier. The SSH session terminates at the 60-minute mark; the URL goes dead. For longer shares, either use
PINGGY_TOKEN(Pro) or auto-restart with a shell loop (note that the URL changes on every restart for free-tier). - Free-tier URL is random and changes on restart. Don't bookmark it, don't paste it into a config file. Re-parse from the log each time.
- Concurrent free tunnels are limited to one per source IP. Starting a second tunnel from the same machine usually kills the first. Pro tier lifts this.
+in usernames must be quoted. Baressh ... b:admin:[email protected]works in bash but breaks under shells that treat+specially or when assembled programmatically. Always wrap in double quotes.- Don't tunnel anything sensitive without an access-control flag. A bare HTTP tunnel is reachable by anyone with the URL. Use
b:,k:, orw:for non-public services. process(action='log')may miss SSH banner output. Pinggy prints the URLs and then the SSH session goes interactive. Always redirect to a logfile andgrepthe file directly — same pattern ascloudflared-quick-tunnel.- Host-key prompt on first run. Default OpenSSH config asks the user to accept Pinggy's host key. Always pass
-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/nullfor unattended runs. - TCP and TLS tunnels return a
<subdomain>.a.pinggy.online:<port>pair, not an https URL. Parse with a different regex (tcp://and a port). Don't assume every Pinggy tunnel is HTTP. - Pro mode requires the token as the username, not a flag. Use
"$PINGGY_TOKEN+a.pinggy.io"(nofree@). With a token you can also add:persistentfor a stable subdomain — seepinggy.io/docs/.
Recipes
Composite patterns combining a local origin with a Pinggy tunnel. Each recipe is self-contained — start the origin, start the tunnel, parse the URL, hand it back to the user.
Recipe 1 — Receive a webhook callback
Use this when an external service (Stripe, GitHub, Discord, AgentMail, etc.) needs to POST to a publicly reachable URL during a local task.
# 1. Tiny capturing server: every request gets appended to ~/.hermes/cache/scratch/webhook-hits.log
cat >~/.hermes/cache/scratch/webhook-server.py <<'PY'
import http.server, json, datetime, pathlib
LOG = pathlib.Path("~/.hermes/cache/scratch/webhook-hits.log").expanduser()
class H(http.server.BaseHTTPRequestHandler):
def _capture(self):
n = int(self.headers.get("content-length") or 0)
body = self.rfile.read(n).decode("utf-8", "replace") if n else ""
rec = {"t": datetime.datetime.utcnow().isoformat(), "path": self.path,
"method": self.command, "headers": dict(self.headers), "body": body}
with LOG.open("a") as f: f.write(json.dumps(rec) + "\n")
self.send_response(200); self.send_header("content-type","application/json")
self.end_headers(); self.wfile.write(b'{"ok":true}\n')
def do_GET(self): self._capture()
def do_POST(self): self._capture()
def log_message(self,*a,**k): pass
http.server.HTTPServer(("127.0.0.1", 18080), H).serve_forever()
PY
nohup python ~/.hermes/cache/scratch/webhook-server.py >~… (full content is in the download package)
Related skills
- cloudflare-workers – Build and deploy edge functions with Cloudflare Workers and Wrangler. Use for APIs, cron…
- cloudformation – Deploy AWS resources with CloudFormation templates. Create stacks, use nested stacks…
- cloudish – Deploy a Dockerfile, source folder, or existing image to Cloudish as a running container…
- quantized-export – Export a promoted fine-tuned model in the right deployment format — merged safetensors…
- kubernetes-operator – Use when building a Kubernetes Operator — custom controllers that reconcile CRD state…
- kubernetes-operator – Use when building a Kubernetes Operator — custom controllers that reconcile CRD state…
- product-price-monitor – Watch product, flight, or listing prices; alert on target.
- risk-metrics-calculation – Calculate portfolio risk metrics including VaR, CVaR, Sharpe, Sortino, and drawdown…