Skip to content

Latest commit

 

History

History
165 lines (115 loc) · 7.66 KB

exposure.md

File metadata and controls

165 lines (115 loc) · 7.66 KB

Getting a permanent URL

skillhook listens on 127.0.0.1:8787 and never terminates TLS itself. A tunnel on the same machine gives it a stable HTTPS URL that webhook senders can reach. The default is Tailscale Funnel: free, no domain to buy, and the configuration survives reboots. Tailscale Serve (tailnet only), Cloudflare Tunnel and ngrok also work. A machine paired with Skillhook Cloud needs no tunnel to receive webhooks: a hosted webhook URL on the cloud takes the delivery, asleep or awake, behind NAT or not, and the machine collects it over its outbound link and verifies the signature with its own secret (cloud.md, https://skillhook.dev/docs/hosted-urls?ref=skillhook-docs).

Related: security.md (why the server stays on loopback), operations.md (running the server as a service).

Tailscale Funnel (default)

Prerequisites: Tailscale installed and signed in on the machine (tailscale status reports Running); HTTPS certificates enabled for the tailnet; the funnel node attribute granted in the tailnet policy. The last two are a one-time approval in the admin console, and the CLI prints the link.

skillhook expose tailscale

What it does:

  1. Finds the tailscale binary (on PATH, then /usr/local/bin/tailscale, /opt/homebrew/bin/tailscale, /Applications/Tailscale.app/Contents/MacOS/Tailscale, /usr/bin/tailscale) and checks that tailscale status --json reports BackendState: Running.
  2. Runs tailscale funnel --bg --yes <port>, where <port> is port from skillhook.json (--port N overrides it). --bg stores the mapping inside tailscaled, so it persists across reboots and does not depend on skillhook running.
  3. Reads tailscale serve status --json back, derives the URL (https://<machine>.<tailnet>.ts.net) and writes it to public_url in skillhook.json.
  4. If a local server is running, polls <url>/health for up to 60 seconds and reports verified, or a hint to check again (a node's first TLS certificate can take a while).
  5. Prints the webhook URL of every skill.
✓ Tailscale Funnel (public internet) → http://127.0.0.1:8787
  https://jonathans-mbp.tail1234.ts.net
  Persistent: survives reboots (stored by tailscaled). Turn off with: skillhook expose off
  ✓ verified: https://jonathans-mbp.tail1234.ts.net/health answered after 4s

Webhook URLs:
  hello                    https://jonathans-mbp.tail1234.ts.net/hooks/hello

The server stays bound to 127.0.0.1; only Tailscale's TLS proxy reaches it.

The hostname is your Tailscale node name; it changes only if you rename the machine or move tailnets. Funnel accepts connections on ports 443, 8443 and 10000; skillhook uses 443, so URLs have no port suffix.

First-time approval: when Funnel is not yet allowed for the node, the command exits 1 and prints

Funnel needs a one-time approval in the Tailscale admin console: https://login.tailscale.com/f/funnel?node=…
Approve it, then run this command again.

Open the link, approve, and re-run skillhook expose tailscale (--json output carries the link as approval_url).

Status and teardown:

skillhook expose status

shows the Tailscale state, every serve/funnel mapping on the node (marking the one that targets skillhook's port) and the configured public_url.

skillhook expose off

runs tailscale funnel --https=443 off and clears public_url.

Tailscale Serve (tailnet only)

Same mechanics, but the URL answers only for devices on your tailnet: right for skills triggered by your own scripts, iOS Shortcuts or internal services, and for reaching the admin API from another machine.

skillhook expose tailscale --serve
skillhook expose off

Pair it with an IP allow-list so that even a leaked bearer token is useless from outside the tailnet (Tailscale forwards the client's tailnet address in X-Forwarded-For):

skillhook:
  auth:
    type: bearer
    allow_ips: ["100.64.0.0/10"]

Cloudflare Tunnel

Free; needs a domain on Cloudflare. skillhook expose cloudflare prints the recipe:

brew install cloudflared
cloudflared tunnel login
cloudflared tunnel create skillhook
cloudflared tunnel route dns skillhook hooks.example.com
cloudflared tunnel run --url http://127.0.0.1:8787 skillhook      # or: cloudflared service install
skillhook config set public_url https://hooks.example.com

ngrok

The free tier includes one static domain. skillhook expose ngrok prints the recipe:

brew install ngrok && ngrok config add-authtoken <token>
# claim a static domain at https://dashboard.ngrok.com/domains
ngrok http --url=<your-name>.ngrok-free.app 8787
skillhook config set public_url https://<your-name>.ngrok-free.app

public_url

public_url in skillhook.json is informational: skillhook url, skills list, send --public, doctor and the MCP tools use it to print and test webhook URLs, and serve logs it at start. expose tailscale sets it and expose off clears it; for other tunnels set it yourself with skillhook config set public_url https://… (and skillhook config unset public_url to remove it).

URL resolution order used by url, skills list and the MCP tools: public_url; an active Tailscale mapping that targets the configured port; the running server's local address; the configured host:port. send defaults to the local server; --public forces the first two.

Client IPs behind a proxy

With trust_proxy: true (default), when the TCP peer is loopback and the request carries X-Forwarded-For (or X-Real-IP / CF-Connecting-IP), the first address becomes the client IP used for rate limits, allow_ips, {{source_ip}} and job records. Tailscale, cloudflared and ngrok all set one of these. A request carrying any proxy header is treated as "via proxy" and cannot use the token-less local admin access (security.md). Set trust_proxy: false only if something other than a local proxy connects to the port directly.

Verifying

skillhook url
curl -i https://<node>.ts.net/hooks/hello        # GET → 200 "skillhook: POST your webhook to this URL."
skillhook send hello --public --wait 60
skillhook doctor                                  # includes a "public url" check that fetches <public_url>/health

Troubleshooting

Symptom Cause and fix
tailscale CLI not found Install Tailscale (https://tailscale.com/download) and sign in. skillhook also looks inside /Applications/Tailscale.app.
Tailscale is NeedsLogin / Stopped / not running Open the Tailscale app and sign in; tailscale status must say Running.
Funnel needs a one-time approval in the Tailscale admin console Open the printed link, enable Funnel for the node (this also enables HTTPS certificates when needed), re-run the command.
Command succeeded, but the URL times out or fails TLS The first certificate for a node can take up to a minute. skillhook expose status, then retry; send prints the same hint.
doctor says port 8787 not exposed after you changed port The mapping targets a fixed local port. Re-run skillhook expose tailscale.
Works from the tailnet, not from the internet Serve is enabled instead of Funnel: skillhook expose off, then skillhook expose tailscale (without --serve).
Admin routes answer 401 through the tunnel Expected. Send Authorization: Bearer $SKILLHOOK_ADMIN_TOKEN.
expose off says it could not disable Pass --serve if you exposed with --serve; check tailscale serve status.