mcpField
E
expose-localhost

expose-localhost

github.com/ngrokCloud & DevOps15336 installsUpdated today
Repository

About expose-localhost

expose-localhost is a skill for publishing local services to the internet through ngrok. It guides users sharing development or TCP services through access controls, traffic policies, and reusable configuration.

Key features

  1. 01Create HTTP or TCP tunnels with ngrok
  2. 02Configure OAuth, basic auth, and IP restrictions
  3. 03Apply rate limits to open access
  4. 04Detect service ports and check configuration
  5. 05Save tunnel policies and startup scripts

Use cases

  1. 01Share a protected development preview with reviewers
  2. 02Temporarily expose SSH or database access
  3. 03Set up a reusable tunnel startup workflow

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/ngrok/skills --skill expose-localhost

After installing, type/expose-localhostin Claude Code to confirm it loaded.

Bundled filesreferences/TROUBLESHOOTING.md

Details

From the project README

Expose Localhost

Expose a local service to the public internet using ngrok. Optionally add OAuth, OWASP protection, or rate limiting via Traffic Policy.

Prerequisites

  • ngrok CLI installed (ngrok command available)
  • Auth token configured (ngrok config add-authtoken <TOKEN>)

Workflow

Step 1: Pre-flight & Configuration

Silently verify ngrok is ready:

ngrok config check

If auth token missing, tell user to run: ngrok config add-authtoken <TOKEN> (get token at https://dashboard.ngrok.com/get-started/your-authtoken)

Then ask all questions upfront before doing anything:

Before I expose your service, I need a few details:

1. **Port**: I see your app runs on port 3000. Is that correct?
2. **Domain**: Use your dev domain, or do you have a custom domain?
3. **Access control**: Open access, or require login (Google/GitHub/etc.)?
4. **Save config?**: One-time setup, or save for reuse?

Do NOT mention cloud endpoints, reserved domains, or internal endpoints — those are advanced concepts the user shouldn't need to think about.

Detecting the port: Check package.json scripts for --port, .env for PORT=, docker-compose.yml for port mappings.

Is it even HTTP? If the user says SSH, RDP, a database, or names a port like 22, 3389, 5432, or 3306, this is a raw TCP service — skip to Non-HTTP Services (TCP). The domain and access-control questions above do not apply there.

Domains: Most ngrok accounts have a free static dev domain (e.g., something.ngrok-free.dev). Running ngrok http PORT uses it automatically. Users can also provide a custom domain configured in the ngrok dashboard. Some accounts (especially new ones) may not have a dev domain yet — if ngrok fails with ERR_NGROK_15013, tell the user: "You don't have a dev domain yet. Claim your free one at https://dashboard.ngrok.com/domains — then we can try again."

If user requests OAuth, also ask: "Should only specific people be able to access it? I can restrict by email address or email domain."

After gathering answers, confirm and get a Y/n before proceeding.

Step 2: Start the Tunnel
No security (simplest)
ngrok http {PORT} &
sleep 3
curl -s http://localhost:4040/api/tunnels | grep -o '"public_url":"[^"]*"' | head -1

With a specific domain, add --url https://{DOMAIN}.

With security (Traffic Policy)

Create the traffic policy file first, then start ngrok with it.

OAuth-only (default when user requests auth):

on_http_request:
  - actions:
      - type: oauth
        config:
          provider: google

Replace google with the chosen provider (google, github, microsoft, gitlab, linkedin, twitch).

If the user requests OAuth, default to OAuth-only. Do NOT add OWASP or rate limiting unless explicitly asked — OAuth already blocks unauthenticated access.

OAuth with email restriction — use a separate rule with a CEL expression to deny non-matching emails. Do NOT add an allow field to the OAuth action.

Single email:

on_http_request:
  - actions:
      - type: oauth
        config:
          provider: google
  - expressions:
      - "actions.ngrok.oauth.identity.email != '[email protected]'"
    actions:
      - type: deny
        config:
          status_code: 403

Email domain:

on_http_request:
  - actions:
      - type: oauth
        config:
          provider: google
  - expressions:
      - "!actions.ngrok.oauth.identity.email.endsWith('@your-company.com')"
    actions:
      - type: deny
        config:
          status_code: 403

Multiple emails — name the identity explicitly on the left of in:

  - expressions:
      - "!(actions.ngrok.oauth.identity.email in ['[email protected]', '[email protected]'])"
    actions:
      - type: deny
        config:
          status_code: 403

Shared secret (when the person won't log in with an IdP — an external client, a reviewer without a company account):

on_http_request:
  - actions:
      - type: basic-auth
        config:
          credentials:
            - "reviewer:{PASSWORD}"

Hand them the URL and the password over a channel you already trust. If they have a stable IP, restrict-ips is an alternative with nothing to share.

Open-access hardening (no auth, but wants protection):

on_http_request:
  - actions:
      - type: rate-limit
        config:
          name: default-rate-limit
          algorithm: sliding_window
          capacity: 200
          rate: "60s"
          bucket_key:
            - conn.client_ip
      - type: owasp-crs-request
        config:
          on_error: halt

on_http_response:
  - actions:
      - type: owasp-crs-response
        config:
          on_error: halt

After writing the policy file, start ngrok:

ngrok http {PORT} --traffic-policy-file .ngrok/traffic-policy.yml &
sleep 3
curl -s http://localhost:4040/api/tunnels | grep -o '"public_url":"[^"]*"' | head -1

Add --url https://{DOMAIN} if using a specific domain.

Sharing a dev server? Vite, Next, and webpack reject requests whose Host header isn't localhost, and their hot-reload websockets need the ngrok domain allowed. Set this up before handing over the URL — see references/TROUBLESHOOTING.md.

Step 3: Handle Errors

If a traffic policy action fails due to plan limitations:

  1. Tell the user which specific action requires an upgrade
  2. Offer to remove that action from the policy and retry
  3. Do NOT suggest switching to cloud endpoints as a workaround
Step 4: Persistent Configuration (If Requested)

Save these files to the project:

  • .ngrok/traffic-policy.yml — the policy (if security was configured)
  • .ngrok/expose.sh:
#!/bin/bash
set -e
echo "Your service will be at: https://{DOMAIN}"
ngrok http {PORT} --url https://{DOMAIN} --traffic-policy-file .ngrok/traffic-policy.yml

Omit --traffic-policy-file if no policy. Omit --url if no specific domain.

Optionally add to package.json:

{ "scripts": { "tunnel": "bash .ngrok/expose.sh" } }

Teardown

pkill ngrok

Non-HTTP Services (TCP)

For SSH, RDP, or a database, use ngrok tcp instead of ngrok http.

ngrok tcp {PORT} &
sleep 3
curl -s http://localhost:4040/api/tunnels | grep -o '"public_url":"[^"]*"' | head -1

ngrok returns an address like tcp://1.tcp.ngrok.io:12345. The host and port are used separately by the client:

# local sshd on port 22, reached through the address above
ssh -p 12345 {USER}@1.tcp.ngrok.io

Common ports: SSH 22, RDP 3389, PostgreSQL 5432, MySQL 3306.

The address changes on every restart. For one that persists, reserve it first. ngrok assigns the hostname and port — unlike domains, you cannot choose them:

ngrok api reserved-addrs create --description "SSH for {NAME}" --region us
ngrok tcp {PORT} --url tcp://{ADDR}

Reserved TCP addresses need a paid plan and keep billing until deleted.

Access control is different here. OAuth and OWASP are HTTP-only and will not work. TCP policies run in the on_tcp_connect phase, where restrict-ips is the usual control:

on_tcp_connect:
  - actions:
      - type: restrict-ips
        config:
          enforce: true
          allow:
            - 203.0.113.0/24

Attach it the same way: ngrok tcp {PORT} --traffic-policy-file .ngrok/traffic-policy.yml

Cloud Endpoints (Advanced)

Only use if the user explicitly needs a URL that persists after the agent stops (e.g., webhooks, long-lived integrations).

Requires an API key: ngrok config add-api-key <KEY> (get at https://dashboard.ngrok.com/api-keys)

  1. Reserve domain: ngrok api reserved-domains create --domain "{DOMAIN}"
  2. Create cloud endpoint with a traffic policy that includes forward-internal as the last action:
ngrok api endpoints create --url "https://{DOMAIN}" --bindings public --traffic-policy "$(cat .ngrok/traffic-policy.yml)"

The traffic policy must end with:

- type: forward-internal
  config:
    url: https://{NAME}.internal
  1. Start the internal agent: ngrok http {PORT} --url https://{NAME}.internal
  2. Teardown: ngrok api endpoints delete {ENDPOINT_ID}

Related Skills

This skill covers giving a service a public URL. Neighbouring jobs have their own skills — hand off rather than reimplementing them here:

  • Auth, rate limits, IP rules, or a maintenance page in general — secure-endpoint. (Sharing a preview with one named person is handled here, in Step 2.)
  • It should receive webhooks — receive-webhooks verifies provider signatures at the edge and can keep the receiver off the public internet.
  • It is an MCP server — run-mcp-gateway handles per-provider credentials.
  • One endpoint per sandbox, tenant, or device, created by a controlplane — provision-sandbox-access.
  • Something is broken (ERR_NGROK_*, a 400 through the URL but not on localhost) — troubleshoot-ngrok.
  • Setup or auth problems — ngrok-setup.

For the underlying model — endpoint types, HTTP vs TCP, and the full Traffic Policy action set — see ngrok-engine. For applying config on a surface other than the CLI (SDKs, REST API, Terraform, Kubernetes), see ngrok-surfaces.

Comments

Markdown supported