Skip to content

Remote access

By default the server only listens on localhost:3333. To reach it from another device or over the internet, you need a tunnel. The setup script (scripts/remote-setup.sh) automates both options below.

Terminal window
./scripts/remote-setup.sh tailscale # or: cloudflare

Tailscale creates a private mesh network between your own devices. No domain, no public exposure, real HTTPS via Let’s Encrypt (required for PWA install and Web Push).

Terminal window
./scripts/remote-setup.sh tailscale

The script installs Tailscale, authenticates, and configures tailscale serve to forward HTTPS to localhost:3333. Your hub becomes available at:

https://<machine-name>.<tailnet>.ts.net

One manual step: in the Tailscale admin panel, enable HTTPS certificates for your tailnet. Without this, tailscale serve uses a self-signed cert that browsers reject.

Cloudflare Tunnel

For public access under your own domain (free Cloudflare account, domain ~1 €/year).

Install cloudflared

Terminal window
brew install cloudflared

Create a tunnel

Terminal window
cloudflared tunnel login
cloudflared tunnel create penates

The second command prints a tunnel ID (format: abc123de-...). Keep it: you need it in the next steps.

Route DNS

Terminal window
cloudflared tunnel route dns penates code.yourdomain.xyz

Write the config file

Terminal window
mkdir -p ~/.cloudflared
nano ~/.cloudflared/config.yml

Paste the following, replacing TUNNEL-ID, YOUR-USERNAME, and yourdomain.xyz:

tunnel: TUNNEL-ID
credentials-file: /Users/YOUR-USERNAME/.cloudflared/TUNNEL-ID.json
ingress:
- hostname: code.yourdomain.xyz
service: http://localhost:3333
- service: http_status:404

Save with Ctrl+O, Enter, Ctrl+X.

Install as a LaunchAgent

Terminal window
cloudflared service install
launchctl start com.cloudflare.cloudflared

The hub is now reachable at https://code.yourdomain.xyz. The tunnel starts automatically after reboot.

Cloudflare Access (Zero Trust hardening)

The Cloudflare Tunnel alone leaves only the bearer token as the auth layer. Cloudflare Access adds an identity gate: every browser visit is redirected to a Cloudflare login (GitHub, Google, one-time email PIN, or others) before it can touch the hub. The hub then validates the resulting JWT. Requests that originate on the Mac itself (Claude Code hooks, API calls) bypass the tunnel and continue to authenticate with the bearer token only.

Prerequisites

  • Cloudflare Tunnel is already running (see above).
  • Zero Trust is activated on your Cloudflare account (free for personal use; dash.cloudflare.com/zero-trust).

Create an Access Application

  1. Open the Zero Trust dashboard → Access → Applications → Add an application → Self-hosted.
  2. Set Application name to Penates (or anything you like).
  3. Set Session duration to 24h (or longer).
  4. Set Application domain to your tunnel domain (e.g. code.yourdomain.xyz).
  5. Under Identity providers, enable at least one:
    • GitHub OAuth: one click if you already use GitHub.
    • One-Time PIN: sends a code to your email; no OAuth app needed.
  6. Create a Policy: Action = Allow, Include = one or both of:
    • Emails → your email address (for the PIN flow)
    • GitHub → your GitHub username (for GitHub OAuth)
  7. Save the application.
  8. On the application overview, copy the Application Audience (AUD) tag (a 64-character hex string).

Configure the hub

Edit ~/penates/.env and add both variables:

Terminal window
CF_ACCESS_TEAM_DOMAIN=yourteam.cloudflareaccess.com
CF_ACCESS_AUD=3c994b6913e0ee914f118337173aabdaa7a54a7c82f98e6f2b93b57fa7078db5

CF_ACCESS_TEAM_DOMAIN is the team URL shown in the top-left of the Zero Trust dashboard (without https://). CF_ACCESS_AUD is the tag from step 8.

Restart the server:

Terminal window
launchctl kickstart -k gui/$(id -u)/com.penates

Verify

Navigate to https://code.yourdomain.xyz in a browser. You should be redirected to a Cloudflare login page. After authenticating, you land on the hub dashboard.

Check the audit log:

Terminal window
tail -1 ~/.penates/audit.log

You should see an auth.login entry with your email address.

Troubleshoot 401 errors

Terminal window
tail -20 ~/.penates/audit.log | grep auth.fail | jq -c
reasonFix
bad-jwt:no-jwtBrowser did not go through Cloudflare Access. Clear cookies for the domain and reload; the login flow should restart.
bad-jwt:bad-audCF_ACCESS_AUD in .env does not match the AUD tag on the Access Application. Copy it again from the dashboard.
bad-jwt:bad-issCF_ACCESS_TEAM_DOMAIN is wrong. Must be the team URL without https://.
bad-jwt:expiredJWT has expired. Increase the Session Duration in the Access Application settings.
bad-bearerBearer token in the browser does not match AUTH_TOKEN in .env. Clear it: localStorage.removeItem('penates_token') in DevTools, then reload.

Disable Cloudflare Access

Remove both variables from .env (or set them to empty strings) and restart the server. No code changes needed; the feature is entirely env-gated.