Problem
Daemon.ExposureMode = "reverse-proxy" is fully supported by the runtime — config schema, daemon binding, ForwardedHeaders middleware, startup validation (DaemonExposureValidator.Validate), and ExposureModeDoctorCheck all handle it correctly. But netclaw init only offers four of the five modes (Local, Tailscale Serve, Tailscale Funnel, Cloudflare Tunnel).
Operators who want to run Netclaw behind nginx / Caddy / Traefik / IIS / ALB currently have to either hand-edit netclaw.json after init or pick a mode they don't actually want. This is a UX gap, not a missing capability.
Proposed change
Add reverse proxy as the second option in the exposure-mode selection list (right after Local, ordered by deployment friction) and introduce one new sub-step in the ExposureModeStepViewModel to collect:
- Bind address — text input, defaults to
0.0.0.0
- Trusted proxies — multi-line text input, one IP/CIDR per line, ≥1 required
After collection, show a medium informational notice ("Got it — continue") with the exact serving URL the operator should configure their proxy to forward to (e.g. http://0.0.0.0:5199), the trusted proxies they just entered, and the operator responsibilities (TLS termination, firewalling, forwarded-header setup).
Validation policy
The wizard enforces only the same trusted-proxy minimum the runtime already enforces (≥1 entry), and blocks advance from the config sub-step until that's satisfied. This matches DaemonExposureValidator.Validate (src/Netclaw.Configuration/DaemonConfig.cs:140-155) and prevents the wizard from producing a config the daemon would refuse to start with.
Bind-address loopback / IP-format validation stays in DaemonExposureValidator and ExposureModeDoctorCheck — duplicating that logic in the TUI would drift over time. The help text mentions the loopback rule so operators don't trip on it unintentionally.
No deferrable / "we'll fix it later" path. Operators who don't yet know their proxy IP should pick Local and re-run netclaw init --resume once they do.
TUI flow (4 sub-steps for reverse proxy)
Sub-step 0: Mode selection — Reverse Proxy added as 2nd option
Sub-step 1: Configure reverse proxy
┌──────────────────────────────────────┐
│ Bind address: [0.0.0.0 ] │
│ Trusted proxies (one per line, ≥1): │
│ [10.0.0.0/24 ] │
│ [192.168.1.5 ] │
└──────────────────────────────────────┘
[Enter] blocked until ≥1 trusted proxy entered.
Sub-step 2: Reverse proxy configured
Daemon will listen on: http://0.0.0.0:5199
Trusted proxies: 10.0.0.0/24, 192.168.1.5
You are responsible for: TLS termination, firewalling,
X-Forwarded-* header setup.
▸ Got it — continue
Sub-step 3: Webhook toggle (unchanged from today)
The wizard step-header chrome shows Network Exposure: Reverse Proxy → http://0.0.0.0:5199 through the remainder of the wizard, giving the operator a second sighting of the serving URL.
Files affected
src/Netclaw.Cli/Tui/Wizard/Steps/ExposureModeStepViewModel.cs — add Host + TrustedProxies fields, add 4th sub-step, update sub-step routing
src/Netclaw.Cli/Tui/Wizard/Steps/ExposureModeStepView.cs — add ReverseProxy to the selection list, add BuildReverseProxyConfig() with two TextInputNodes (pattern from IdentityStepView.cs:145-190), add BuildReverseProxyNotice()
src/Netclaw.Cli/Tui/Wizard/WizardConfigBuilder.cs — extend DaemonConfigSection (line 461) with Host + TrustedProxies, update the serializer at line 270 to emit them
src/Netclaw.Cli.Tests/Tui/Wizard/ExposureModeStepViewModelTests.cs — add reverse-proxy test cases
tests/smoke/tapes/init-wizard-reverse-proxy.tape (new) — drives the wizard through the reverse-proxy flow, anchored with Wait+Screen per tests/smoke/tapes/README.md
tests/smoke/assertions/init-wizard-reverse-proxy.sh (new) — jq-asserts Daemon.ExposureMode, Daemon.Host, Daemon.TrustedProxies[0]; runs netclaw doctor (exit 0 or 2)
feeds/skills/.system/files/netclaw-operations/SKILL.md — add reverse proxy to the exposure-mode guidance; bump metadata.version
docs/spec/SPEC-007-guided-onboarding.md — add reverse proxy to the wizard's exposure-mode list
No schema, validator, or daemon runtime changes needed. Those already do the right thing.
Acceptance criteria
Non-goals
- No changes to
DaemonExposureValidator, ExposureModeDoctorCheck, the JSON schema, the daemon's ForwardedHeaders middleware, or any other runtime path — they already handle reverse proxy correctly
- No automatic detection of common proxy IP ranges (Docker, k8s, etc.) — speculative
- No port input in the wizard — port stays at the
5199 default; can be hand-edited in netclaw.json after init
- No TLS / certificate management — TLS terminates at the proxy
- No deferrable trusted-proxies path — the daemon requires them at start, so the wizard does too
Problem
Daemon.ExposureMode = "reverse-proxy"is fully supported by the runtime — config schema, daemon binding,ForwardedHeadersmiddleware, startup validation (DaemonExposureValidator.Validate), andExposureModeDoctorCheckall handle it correctly. Butnetclaw initonly offers four of the five modes (Local, Tailscale Serve, Tailscale Funnel, Cloudflare Tunnel).Operators who want to run Netclaw behind nginx / Caddy / Traefik / IIS / ALB currently have to either hand-edit
netclaw.jsonafter init or pick a mode they don't actually want. This is a UX gap, not a missing capability.Proposed change
Add reverse proxy as the second option in the exposure-mode selection list (right after Local, ordered by deployment friction) and introduce one new sub-step in the
ExposureModeStepViewModelto collect:0.0.0.0After collection, show a medium informational notice ("Got it — continue") with the exact serving URL the operator should configure their proxy to forward to (e.g.
http://0.0.0.0:5199), the trusted proxies they just entered, and the operator responsibilities (TLS termination, firewalling, forwarded-header setup).Validation policy
The wizard enforces only the same trusted-proxy minimum the runtime already enforces (≥1 entry), and blocks advance from the config sub-step until that's satisfied. This matches
DaemonExposureValidator.Validate(src/Netclaw.Configuration/DaemonConfig.cs:140-155) and prevents the wizard from producing a config the daemon would refuse to start with.Bind-address loopback / IP-format validation stays in
DaemonExposureValidatorandExposureModeDoctorCheck— duplicating that logic in the TUI would drift over time. The help text mentions the loopback rule so operators don't trip on it unintentionally.No deferrable / "we'll fix it later" path. Operators who don't yet know their proxy IP should pick Local and re-run
netclaw init --resumeonce they do.TUI flow (4 sub-steps for reverse proxy)
The wizard step-header chrome shows
Network Exposure: Reverse Proxy → http://0.0.0.0:5199through the remainder of the wizard, giving the operator a second sighting of the serving URL.Files affected
src/Netclaw.Cli/Tui/Wizard/Steps/ExposureModeStepViewModel.cs— addHost+TrustedProxiesfields, add 4th sub-step, update sub-step routingsrc/Netclaw.Cli/Tui/Wizard/Steps/ExposureModeStepView.cs— addReverseProxyto the selection list, addBuildReverseProxyConfig()with twoTextInputNodes (pattern fromIdentityStepView.cs:145-190), addBuildReverseProxyNotice()src/Netclaw.Cli/Tui/Wizard/WizardConfigBuilder.cs— extendDaemonConfigSection(line 461) withHost+TrustedProxies, update the serializer at line 270 to emit themsrc/Netclaw.Cli.Tests/Tui/Wizard/ExposureModeStepViewModelTests.cs— add reverse-proxy test casestests/smoke/tapes/init-wizard-reverse-proxy.tape(new) — drives the wizard through the reverse-proxy flow, anchored withWait+Screenpertests/smoke/tapes/README.mdtests/smoke/assertions/init-wizard-reverse-proxy.sh(new) — jq-assertsDaemon.ExposureMode,Daemon.Host,Daemon.TrustedProxies[0]; runsnetclaw doctor(exit 0 or 2)feeds/skills/.system/files/netclaw-operations/SKILL.md— add reverse proxy to the exposure-mode guidance; bumpmetadata.versiondocs/spec/SPEC-007-guided-onboarding.md— add reverse proxy to the wizard's exposure-mode listNo schema, validator, or daemon runtime changes needed. Those already do the right thing.
Acceptance criteria
netclaw initshowsReverse Proxyas the second optionhttp://{Host}:{Port}and operator responsibilitiesnetclaw.jsoncontainsDaemon.Host,Daemon.ExposureMode = "reverse-proxy", andDaemon.TrustedProxiesdotnet test src/Netclaw.Cli.Testspasses./scripts/smoke/run-smoke.sh lightpasses, including the newinit-wizard-reverse-proxytapedotnet slopwatch analyzepasses (no new violations)./scripts/Add-FileHeaders.ps1 -Verifypassesfeeds/skills/.system/files/netclaw-operations/SKILL.mdversion bumpeddocs/spec/SPEC-007-guided-onboarding.mdupdatedNon-goals
DaemonExposureValidator,ExposureModeDoctorCheck, the JSON schema, the daemon'sForwardedHeadersmiddleware, or any other runtime path — they already handle reverse proxy correctly5199default; can be hand-edited innetclaw.jsonafter init