Skip to content

Exposure Modes

Exposure mode controls how the daemon is reachable over the network. Most setups use local mode. You only need a non-local mode when something external (GitHub webhooks, CI, a second machine, or a reverse proxy) needs to reach the daemon.

ModeConfig valueRequired processReachabilityRisk
LocallocalNoneLoopback onlyLowest
Reverse Proxyreverse-proxyYour reverse proxyWhatever the proxy exposesMedium
Tailscale Servetailscale-servetailscaledYour Tailscale network (called a tailnet), HTTPSLow
Tailscale Funneltailscale-funneltailscaledPublic internet via TailscaleHigh
Cloudflare Tunnelcloudflare-tunnelcloudflaredPublic internet via CloudflareHigh

The netclaw init wizard covers this at step 9:

Exposure mode selection in the netclaw init wizard, with Local highlighted as the recommended option

Options marked with a warning triangle expose the daemon to the public internet. Tailscale Serve is the recommended remote mode: tailnet-only access, no public exposure.

{
"Daemon": {
"Host": "10.0.0.5",
"ExposureMode": "reverse-proxy",
"TrustedProxies": ["10.0.0.10"]
}
}

Use this when nginx, Caddy, Traefik, HAProxy, or another reverse proxy is the public edge.

In this mode:

  • Daemon.Host must be a non-loopback internal address. 127.0.0.1, ::1, and localhost are rejected in reverse-proxy mode.
  • TrustedProxies must list the proxy’s source IP or CIDR. Forwarded headers are only honored from those peers.

If the proxy runs on the same machine, the final hop into netclaw still needs to target a non-loopback internal IP. A same-host reverse proxy is fine, but the final hop into netclaw still cannot use loopback.

Set the mode in the Daemon section of ~/.netclaw/config/netclaw.json.

No Daemon section needed. The daemon binds to 127.0.0.1:5199 and is only reachable from the same machine.

{}
{
"Daemon": {
"ExposureMode": "tailscale-serve"
}
}

Tailscale Serve creates an HTTPS endpoint on your tailnet that proxies to the daemon’s local port. Only devices on your tailnet can reach it. Then run tailscale serve to proxy your tailnet hostname to 127.0.0.1:5199.

{
"Daemon": {
"ExposureMode": "tailscale-funnel"
}
}

Funnel extends Serve to the public internet. Anyone with the URL can reach the daemon, though netclaw’s device authentication still applies. Configure it with tailscale funnel. Only use Funnel when you need public internet access — Tailscale Serve covers everything else.

{
"Daemon": {
"ExposureMode": "cloudflare-tunnel"
}
}

Routes traffic through Cloudflare’s network to the daemon. Set up cloudflared with a Cloudflare Tunnel pointed at 127.0.0.1:5199, and pair it with a Cloudflare Access policy to restrict who can connect.

If cloudflared runs outside the daemon’s process namespace, set Daemon.SkipTunnelProcessCheck to true so startup validation skips the local process probe without skipping the rest of the auth checks.

FieldTypeDefaultNotes
Hoststring127.0.0.1IP address the daemon binds to
Portint5199TCP port
ExposureModestringlocallocal, reverse-proxy, tailscale-serve, tailscale-funnel, or cloudflare-tunnel
TrustedProxiesstring[][]Required in reverse-proxy; literal IPs or CIDRs only
SkipTunnelProcessCheckboolfalseSkip local tunnel process detection for sidecar or host-managed tunnel topologies
{
"Daemon": {
"Host": "127.0.0.1",
"Port": 5199,
"ExposureMode": "tailscale-serve"
}
}

Case-insensitive — reverse-proxy, ReverseProxy, and REVERSE-PROXY all work, same as the tunnel modes.

Docker users: if you switch to a tunnel mode, update your container’s port binding to match the Host and Port values here. See Docker Deployment for details.

Override any field with NETCLAW_Daemon__ prefixed env vars:

Terminal window
NETCLAW_Daemon__ExposureMode=reverse-proxy
NETCLAW_Daemon__Host=10.0.0.5
NETCLAW_Daemon__Port=5199
NETCLAW_Daemon__TrustedProxies__0=10.0.0.10

Double underscores separate path segments, following the .NET configuration convention.

Host, Port, and ExposureMode require a daemon restart — they aren’t hot-reloaded. Other config changes trigger an automatic restart; the daemon drains active sessions and restarts itself.

Terminal window
# systemd
systemctl --user restart netclaw
# Docker
docker restart netclaw

Tunnel modes make inbound webhooks possible. External services like GitHub or CI systems can trigger autonomous runs via HTTP POST. The netclaw init wizard asks about this right after exposure mode selection:

Inbound webhook toggle in the init wizard

They do nothing in local mode.

Non-local modes require at least one paired device or an alternative remote authentication scheme. Without one, the daemon refuses to start because remote clients have no way to authenticate.

On fresh setup-owned installs, netclaw automatically creates a one-time bootstrap path for the first non-local start. Before the first successful non-local daemon start, it seeds one local paired device and matching local client token when no paired devices already exist. That covers the init wizard, first Docker boot, and manual setups that still use the daemon’s local state directory.

After the first successful non-local start, that auto-seeding stops. Normal CLI use is paired-device auth from that point on.

If the daemon is already running and you need to add another device, pair from the daemon host:

Terminal window
netclaw daemon pair

If the mode requires tailscaled or cloudflared and that process isn’t running, you’ll see this in the daemon logs (~/.netclaw/logs/daemon.log or journalctl --user -u netclaw for systemd):

Daemon startup aborted: ExposureMode is 'tailscale-serve' but the required
tunnel process 'tailscaled' is not running. Start 'tailscaled' before starting
Netclaw, or set ExposureMode to 'local' in netclaw.json.

If no paired devices exist and no alternative auth scheme is configured:

Daemon startup aborted: ExposureMode is 'tailscale-serve' but no paired devices
exist and no alternative remote authentication scheme is configured. Pair a device
with 'netclaw daemon pair' or configure another remote auth scheme before starting
Netclaw.

Both are fatal. The daemon won’t start until you fix the underlying issue.

In reverse-proxy mode, startup also fails if the daemon is still bound to loopback or if TrustedProxies is empty or malformed.

Run netclaw doctor first — it has a dedicated exposure-mode check.

Netclaw doctor output showing health check diagnostics

Startup aborted with “required tunnel process is not running.”

Start the required process first:

Terminal window
# Tailscale modes
sudo systemctl start tailscaled
# Cloudflare Tunnel
sudo systemctl start cloudflared

Or switch to local mode if you don’t need remote access:

{
"Daemon": {
"ExposureMode": "local"
}
}

Startup aborted with “no paired devices exist and no alternative remote authentication scheme is configured.”

If this is a brand-new non-local install, check whether the daemon actually completed its first successful start. The one-shot bootstrap only happens before that first successful non-local startup.

If the daemon is already running, pair from the daemon host:

Terminal window
netclaw daemon pair

If the device store was lost after the first successful non-local start and the daemon will not start anymore, temporarily switch Daemon.ExposureMode to local or restore devices.json and secrets.json from backup. Then start the daemon, run netclaw daemon pair, and switch back.

You can also re-run the init wizard, which still cooperates with the runtime bootstrap path:

Terminal window
netclaw init

Startup aborted because Daemon.ExposureMode is reverse-proxy but Daemon.Host is 127.0.0.1, ::1, or localhost.

Bind netclaw to a non-loopback internal IP and point the proxy at that address instead of loopback:

{
"Daemon": {
"Host": "10.0.0.5",
"ExposureMode": "reverse-proxy",
"TrustedProxies": ["10.0.0.10"]
}
}

netclaw doctor reports a warning: “ExposureMode is ‘local’ but bind address is not loopback.”

The daemon is reachable beyond localhost without tunnel protection. Either bind to loopback:

{
"Daemon": {
"Host": "127.0.0.1"
}
}

Or switch to the exposure mode that reflects how the daemon is actually reachable:

{
"Daemon": {
"Host": "127.0.0.1",
"ExposureMode": "tailscale-serve"
}
}