# claude-service on Windows — install notes
The Linux install path (systemd + python3 venv + EnvironmentFile) doesn't translate cleanly to Windows. This page captures the deviations validated on razorpeter (Windows GPU peer, onboarded 2026-04-28). Mirror these for any future Windows peer.
Pre-flight
- Windows 10/11 or Server 2022, PowerShell 5.1+ as Administrator
- Tailscale installed and
tailscale upcompleted (peer must be on the tailnet) claudeCLI installed andclaude logincompleted under the operator's account (theclaude -psubprocess inherits this auth — Max-subscription-billed)- Python 3.12+ on PATH (
winget install Python.Python.3.12)
1. NSSM instead of systemd
Windows has no systemd. Use NSSM (Non-Sucking Service Manager) to run uvicorn as a Windows service.
`powershell
winget install NSSM.NSSM
# Service install — points at a start.bat (next section), NOT directly at python
nssm install claude-service "C:\Users\pierr\claude-service\start.bat"
nssm set claude-service AppDirectory "C:\Users\pierr\claude-service"
nssm set claude-service Description "Tailnet-exposed claude -p wrapper"
nssm set claude-service Start SERVICE_AUTO_START
# Log rotation (10 MB)
nssm set claude-service AppStdout "C:\Users\pierr\claude-service\logs\service.log"
nssm set claude-service AppStderr "C:\Users\pierr\claude-service\logs\service.log"
nssm set claude-service AppRotateFiles 1
nssm set claude-service AppRotateBytes 10485760
`
Start-Service claude-service to bring it up; Get-Service claude-service for status.
1.5 NSSM Log On user must match the user that ran claude login
By default nssm install registers the service to run as LocalSystem, which can't read per-user files. The claude CLI stores credentials in the operator's user profile (%USERPROFILE%\.claude\.credentials.json, ACLed to that account). LocalSystem-context invocations of claude -p therefore can't authenticate.
Symptom (visible since claude-service v0.3.2's stdout-on-non-zero-exit logging — pre-v0.3.2 was opaque rc=1 stderr=""): peer-routed /delegate returns HTTP 500 with this in service.log:
`
ERROR claude rc=1 stderr= stdout={"is_error":true,"result":"Not logged in · Please run /login",...}
`
Fix — repoint NSSM at the operator's account. GUI route:
`powershell
nssm edit claude-service
# Log on tab → "This account" → .\`
CLI route:
`powershell
nssm set claude-service ObjectName ".\`
Verify the running uvicorn is now owned by the operator account, then prove claude -p can authenticate:
`powershell
# 1. Owner check
Get-CimInstance Win32_Process -Filter "Name='python.exe'" | Where-Object {
$_.CommandLine -like 'claude-service'
} | ForEach-Object { (Invoke-CimMethod -InputObject $_ -MethodName GetOwner).User }
# Expected:
# 2. Self-pong against /delegate (proves credentials are visible to subprocess)
$tok = '`
If is_error=True, result="Not logged in" persists, the ObjectName change didn't take. Check nssm get claude-service ObjectName and Event Viewer → System → "Service Control Manager" → Event 7038 ("unable to log on as .\X with the currently configured password") — wrong password (or wrong account, see §4 dual-account corollary) is the most common cause.
See also: §7 generalizes the admin-to-register-then-runs-unprivileged Windows install primitive that this NSSM ObjectName fix is a concrete instance of; §8 is the same primitive's worked example for cli-version-watcher via Task Scheduler.
2. .env doesn't auto-load — use start.bat
NSSM doesn't read .env files. Python on Windows doesn't auto-load .env either (we don't pull in python-dotenv). Solution: a small start.bat that exports env vars then launches uvicorn.
`bat
@echo off
REM start.bat — env exports + uvicorn launch for NSSM
set CLAUDE_SERVICE_TOKEN=
REM IMPORTANT: lowercase the node-id explicitly. Tailscale's HostName comes through REM camelcase ('RazorPeter') — set MESH_NODE_NAME to overide auto-detection. set MESH_NODE_NAME=razorpeter
REM Mesh-gateway self-register (v0.3.0+) — both must be set or registration is skipped silently.
set MESH_GATEWAY_URL=http://lab-ovh:8788
set MESH_GATEWAY_TOKEN=
C:\Users\pierr\claude-service\venv\Scripts\python.exe -m uvicorn server:app --host %HOST% --port %PORT% --log-level info
`
chmod 600 equivalent on Windows: right-click → Properties → Security → break inheritance, grant only the operator's account + SYSTEM.
3. Firewall — netsh, not New-NetFirewallRule
New-NetFirewallRule PowerShell cmdlets need UAC elevation in ways that get awkward over an admin SSH session. netsh works in a regular admin SSH session:
`powershell
netsh advfirewall firewall add rule `
name="claude-service-8787" `
dir=in action=allow protocol=TCP localport=8787 `
remoteip=100.64.0.0/10
`
The remoteip=100.64.0.0/10 clamp restricts inbound to the tailnet CGNAT range — no public internet exposure even if Windows Firewall is misconfigured upstream.
4. The 7-character short-name profile gotcha (and the dual-account corollary)
Windows often truncates the user profile path to 7 chars when the original username is longer. Razorpeter's Pierre (6-char SAM account) profile lives at C:\Users\pierr (5-char path), not C:\Users\Pierre. Use $env:USERPROFILE in PowerShell or %USERPROFILE% in cmd, never hard-code C:\Users\Pierre.
`powershell
# Wrong (assumes long name):
$env:CLAUDE_BIN = "C:\Users\Pierre\AppData\Roaming\npm\claude.cmd"
# Right:
\(env:CLAUDE_BIN = "\)env:USERPROFILE\AppData\Roaming\npm\claude.cmd"
`
This bites in start.bat too — verify dir C:\Users\ to confirm the actual folder name before pasting paths into the bat.
Dual-account corollary (NSSM ObjectName trap). Windows lets two distinct SAM accounts share a truncated-name profile dir. On razorpeter there are TWO local accounts: pierr (5-char, dormant — last login pre-onboarding) and Pierre (6-char, active operator). Both legible to Get-LocalUser. The active Pierre account's profile is at C:\Users\pierr because the dormant pierr claimed the directory first. Critical: when configuring NSSM ObjectName (§1.5), use the SAM account name (what whoami returns — Pierre), NOT the profile-dir name (pierr). Verify before typing:
`powershell
whoami # SAM account name — use this for NSSM ObjectName
$env:USERPROFILE # profile-dir path — use this for hardcoded paths only
`
Setting NSSM ObjectName to the wrong account yields SCM event 7038 ("wrong password") even when the password is correct — because you're authenticating against a different account whose password you don't have. Razorpeter hit this loop 2026-04-30 during the v0.3.2 NSSM repoint.
5. Verify
After service start:
`powershell
# Local liveness
curl http://localhost:8787/health
# Mesh registry (replace TOKEN)
curl -H "Authorization: Bearer $TOKEN" http://lab-ovh:8788/peers/razorpeter/health
`
Expected: node:"razorpeter" (lowercase), gpu capability populated if NVIDIA GPU present, last_seen is fresh post-start (proof v0.3.0+ self-register fired).
Operational note. With v0.3.0+ self-register and v0.3.2+ stdout-capture both in place, this Windows peer participates in the canonical three-tier routing pattern (Swarph paper §6.3):
`
peer-Claude ─DM─▶ razor-Claude inbox ─(human, only at privilege boundary)─▶ service-Claude /delegate
`
Mesh DMs land on mesh-inbox-watcher's inbox.log; peer-routed task calls land on this service's service.log. Both are tail-able — consider arming a Monitor on each at session start so neither surface goes silent under live work. The human enters the loop only for tasks the AI can't traverse on its own (Windows password entry, MFA, payment auth, physical hardware — Swarph §6.3 exception class).
Verify every wake source the protocol assumes is actually armed. The routing pattern above presumes a session's Monitor catches inbox.log events; if the wake source is missing or wired to the wrong log, peer DMs accumulate without action and the protocol degrades silently. Razorpeter's 2026-04-30 onboarding hit exactly this: Monitor armed on service.log only, inbox.log unmonitored, a primer-adoption DM sat unread for ~3 hours until the operator prompted directly. Two-stream wake is the canonical Windows shape (inbox.log for mesh DMs, service.log for /delegate traffic); Linux nodes typically need only the inbox-watcher log if /delegate traffic surfaces through orchestrator outcomes. Audit the wake set before declaring an install operational.
6. Troubleshooting — Restart-Service hangs in StopPending
Uvicorn on Windows doesn't handle SCM stop signals gracefully, so Restart-Service claude-service (and bare Stop-Service) sit in StopPending indefinitely while NSSM waits for the inner process to confirm shutdown. Symptom:
`
WARNING: Waiting for service 'claude-service (claude-service)' to stop...
WARNING: Waiting for service 'claude-service (claude-service)' to stop...
... (forever)
`
Force-kill recipe:
`powershell
$nssmPid = (sc.exe queryex claude-service | Select-String 'PID\s+:\s+(\d+)').Matches[0].Groups[1].Value
Stop-Process -Id $nssmPid -Force
Start-Sleep -Seconds 2
Start-Service claude-service
`
Total downtime: ~5 seconds. Don't bother with Restart-Service for this service — go straight to the kill-and-start cycle when you need to apply config changes (NSSM ObjectName updates, env-var changes, etc.).
7. Service-restart privilege boundary — three-tier routing canonical sequence
Restarting claude-service (or mesh-inbox-watcher) on Windows requires Pierre's elevated PowerShell. The current Claude shell on the Windows host is razorpeter\pierre non-elevated. NSSM service has ObjectName=.\Pierre + LogonType=Interactive, which is genuinely operator-credential-bound: granting elevation to the agent shell would compromise the UAC privilege boundary substantively.
This is NOT bake-outable the way the Linux substrate's sudoers.d NOPASSWD is. The discriminator from the in-remit-credential-gap rule (lab+gpu-wsl observation 2026-05-01): can this credential be granted to the agent narrowly without compromising the privilege boundary? On Linux: yes (sudoers.d covers it). On Windows + NSSM + LogonType=Interactive: no — the UAC boundary IS the credential, not just sudo's tty requirement.
§6.3 main paper privilege-boundary three-tier routing is therefore the canonical operational sequence for service restarts on Windows peers:
`
peer-Claude ─DM─▶ razor-Claude inbox ─Pierre's elevated PowerShell─▶ service-Claude
(asks) (relays + flags (gets restarted
the human gate) by the elevated
invocation)
`
Concrete restart procedure when a peer DMs razorpeter asking for a restart:
1. razor-Claude reads the DM in its own session (mesh inbox-watcher daemon writes the inbox.log line → Monitor wakes razor-Claude)
2. razor-Claude DMs Pierre (via Discord webhook or whatever surfacing mechanism is wired) flagging the restart need + the reason
3. Pierre opens an elevated PowerShell (Win+X → Windows PowerShell (Admin), or a saved elevated shortcut)
4. Pierre runs the restart — direct kill-and-start per §6 (skip Restart-Service):
`powershell
$nssmPid = (sc.exe queryex claude-service | Select-String 'PID\s+:\s+(\d+)').Matches[0].Groups[1].Value
Stop-Process -Id $nssmPid -Force
Start-Sleep -Seconds 2
Start-Service claude-service
`
5. razor-Claude DMs the originating peer confirming restart complete + new advertised claude_cli / service_version in /peers
This is NOT a workaround or a degraded mode — it's the correct §6.3 pattern fired correctly. UAC-class credentials are exactly what §6.3 is for. The two-tier "AI-to-AI" cooperative-protocol default doesn't apply at this layer because the credential isn't reducible to a narrow grant.
Operator-side runbook sticky-note: if a peer DM asks for a service restart on this Windows host, that's the §6.3 trigger — open elevated PowerShell, run the recipe, ack via mesh DM. ~30 seconds of operator time per restart; auto-update events typically batch this once per major Claude Code release across the mesh, not per-day.
The 2026-04-30 NSSM ObjectName fix (yesterday's "Not logged in" → user-account-repointing) and the 2026-05-01 system-wide auto-update restart wave (today's claude_cli 2.1.123 → 2.1.126 across all peers) were both clean §6.3 fires on razorpeter — empirical confirmation the pattern works as specified.
Paired-primitive note. The same admin-to-register-then-runs-unprivileged shape applies to Task Scheduler registration as well — see §8 below for the cli-version-watcher install recipe, where Register-ScheduledTask requires admin even though the registered task itself runs as Pierre/Limited. Windows ships per-install-action admin gates (NSSM service registration, Task Scheduler entry creation, netsh advfirewall add rule, et al.) that are one-time costs at install but the running artifact is unprivileged. Pattern recognition: when a Win32 install step is admin-required, ask "is this register-time or runtime?" — register-time is §6.3 privilege-boundary fire; runtime gates need a separate audit.
8. cli-version-watcher (Win32 install)
cli-version-watcher is a sibling daemon (Linux: systemd timer; Win32: Task Scheduler) that closes the "Claude Code CLI auto-updates on disk but claude-service keeps advertising the cached boot-time version" gap (surfaced 2026-05-01 by lab DM #201 → razorpeter). Polls claude --version vs /health-advertised claude_cli; if divergent, POSTs /capabilities/refresh to trigger re-detection + mesh-gateway re-register. Pure stdlib Python, idempotent, exits 0 on no-op.
Install recipe
`powershell
# 1. Copy upstream script into the install dir
$base = 'C:\Users\pierr\claude-service'
Copy-Item -Path "
# 2. Verify the version strings match exactly between claude --version and /health
# (mismatch would create a perpetual divergence loop — both must return identical strings):
\(installed = &\)env:CLAUDE_BIN --version
$advertised = (Invoke-RestMethod 'http://localhost:8787/health').capabilities.claude_cli
"installed=\(installed advertised=\)advertised match=\((\)installed -eq $advertised)"
# Expected: match=True
`
PowerShell wrapper (loads .env, mirrors start_inbox_watcher.bat pattern)
Save as C:\Users\pierr\claude-service\scripts\run_cli_version_watcher.ps1:
`powershell
$base = 'C:\Users\pierr\claude-service'
Get-Content "$base\.env" -ErrorAction SilentlyContinue | ForEach-Object {
\(line =\)_.Trim()
if (-not \(line -or\)line.StartsWith('#') -or -not (\(line -match '^([^=]+)=(.*)\)')) { return }
[Environment]::SetEnvironmentVariable(
\(Matches[1].Trim(),\)Matches[2].Trim().Trim('"').Trim("'"), 'Process'
)
}
if (-not \(env:CLAUDE_BIN) {\)env:CLAUDE_BIN = 'C:\Users\pierr\.local\bin\claude.exe' }
& "\(base\venv\Scripts\python.exe" "\)base\scripts\cli_version_watcher.py" 2>&1 |
Tee-Object -FilePath "$base\logs\cli_version_watcher.log" -Append
exit $LASTEXITCODE
`
Why a wrapper: cli_version_watcher.py reads CLAUDE_BIN, CLAUDE_SERVICE_TOKEN, PORT from os.environ. Task Scheduler doesn't auto-load .env. Same pattern that the inbox-watcher uses via start_inbox_watcher.bat — kept symmetric for cross-daemon consistency.
Task Scheduler registration (admin required — §7 privilege-boundary)
In an elevated PowerShell:
`powershell
$base = 'C:\Users\pierr\claude-service'
$action = New-ScheduledTaskAction -Execute 'powershell.exe' `
-Argument "-NoProfile -ExecutionPolicy Bypass -File "$base\scripts\run_cli_version_watcher.ps1""
$repeatTrigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(5) `
-RepetitionInterval (New-TimeSpan -Hours 6)
$logonTrigger = New-ScheduledTaskTrigger -AtLogOn
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries `
-StartWhenAvailable `
-ExecutionTimeLimit (New-TimeSpan -Minutes 5)
\(principal = New-ScheduledTaskPrincipal -UserId "\)env:USERDOMAIN\$env:USERNAME" `
-LogonType Interactive -RunLevel Limited
Register-ScheduledTask -TaskName 'CliVersionWatcher' `
-Description 'Polls claude --version every 6h; if diverges from advertised, POSTs /capabilities/refresh.' `
-Action \(action -Trigger @(\)repeatTrigger, $logonTrigger) `
-Settings \(settings -Principal\)principal -Force
`
Two-trigger rationale. Linux uses OnBootSec=10min to catch box-off-during-auto-update; Win32 needs a different cousin-shape because Windows desktops sleep/hibernate rather than always being booted. AtLogOn is the right Win32 cousin: when the operator logs in fresh after the box was off, the watcher fires immediately to catch any auto-update that happened during downtime. The 6h-repeat covers the always-on case during the workday. Lab DM #282 named this as environment-driven design extending the primitive (vs just porting it).
Verify
`powershell
Start-ScheduledTask -TaskName 'CliVersionWatcher'
Start-Sleep -Seconds 4
Get-ScheduledTaskInfo -TaskName 'CliVersionWatcher' |
Select-Object LastRunTime, LastTaskResult, NumberOfMissedRuns
# Expected: LastTaskResult=0, NumberOfMissedRuns=0
Get-Content "$base\logs\cli_version_watcher.log" -Tail 5
# Expected: "INFO in sync: claude_cli=`
Rollback
`powershell
Unregister-ScheduledTask -TaskName 'CliVersionWatcher' -Confirm:$false
Remove-Item "\(base\scripts\cli_version_watcher.py", "\)base\scripts\run_cli_version_watcher.ps1"
`
9. mesh-inbox-watcher (Win32 install)
mesh-inbox-watcher is the second sibling daemon — polls mesh-gateway:8788/messages?to= every 60s, writes formatted DM records to logs/inbox.log (which the SessionStart hook tails to surface peer DMs into the next Claude session as context). Pure stdlib Python (urllib), in-memory dedup via _HANDLED_IDS set across polls, optional Discord webhook secondary surface.
Design choice: NSSM daemon, NOT Task Scheduler. This is different from §8 cli-version-watcher's Task Scheduler shape, and the difference is load-bearing:
- cli-version-watcher (§8) is genuinely intermittent: one HTTP probe every 6h, no in-memory state to preserve between fires. Task Scheduler
--oncemode is the right substrate. - mesh-inbox-watcher needs
_HANDLED_IDSdedup across polls. Empirically validated 2026-04-30 on razorpeter onboarding: Task Scheduler--oncemode +INBOX_MARK_READ=0re-logs the same unread message every fire because the per-process set resets between Task Scheduler invocations. Daemon-mode (long-running process) keeps the set in memory.
If you set INBOX_MARK_READ=1 (script auto-marks DMs read on the gateway after local-log write), Task Scheduler --once would work — the gateway then suppresses re-fetches because read_at gets set. But that loses the gateway's unread_only=true query as the canonical "needs Claude/operator attention" signal. The trade-off is operator-flow vs install-shape simplicity; razor (and lab) chose INBOX_MARK_READ=0 daemon-mode to keep gateway-as-truth for the Claude session-start workflow.
Install recipe
`powershell
# 1. Copy upstream script into the install dir
$base = 'C:\Users\pierr\claude-service'
Copy-Item -Path "
# 2. Create logs/ and state/ subdirs (script writes both) New-Item -ItemType Directory -Path "\(base\logs", "\)base\state" -Force | Out-Null
# 3. (Optional but recommended) Verify the upstream script has the leaky-filter fix
# — gateway's ?to=X query is leaky; client-side filter must drop both
# outgoing DMs (from_node == node) and cross-peer noise (to_node != node).
# Both filters present in upstream as of commit 0a0ab7b on darw007d/hedge-fund-mcp.
Select-String -Path "$base\scripts\inbox_watcher.py" -Pattern "from_node.!=.node|to_node.==.node"
# Expected: at least 2 matching lines.
`
Wrapper bat (loads .env, mirrors start.bat pattern that NSSM expects)
Save as C:\Users\pierr\claude-service\start_inbox_watcher.bat:
`bat
@echo off
REM start_inbox_watcher.bat — env exports + python daemon launch for NSSM
REM Sibling of start.bat (claude-service); same .env-loading pattern via for-loop.
cd /d C:\Users\pierr\claude-service
REM Source .env (KEY=VALUE lines; ignore comments & blanks) for /f "usebackq eol=# tokens=1,* delims==" %%a in ("C:\Users\pierr\claude-service\.env") do ( set "%%a=%%b" )
REM Inbox-watcher win32 overrides — see "Design choice" above for INBOX_MARK_READ rationale set INBOX_MARK_READ=0 set INBOX_LOG_FILE=C:\Users\pierr\claude-service\logs\inbox.log set INBOX_POLL_INTERVAL_SEC=60
venv\Scripts\python.exe scripts\inbox_watcher.py
`
Why .bat not .ps1: NSSM invokes the entrypoint directly, and cmd-style for /f with eol=# tokens=1,* delims== handles .env parsing without external dependencies. The cli-version-watcher §8 uses a PowerShell wrapper because Task Scheduler's argument shape is more PowerShell-friendly; NSSM is more cmd-friendly. Different scheduling substrate, different wrapper idiom — both load the same .env file.
NSSM service registration (admin required — §7 privilege-boundary)
In an elevated PowerShell:
`powershell
$base = 'C:\Users\pierr\claude-service'
nssm install mesh-inbox-watcher "$base\start_inbox_watcher.bat"
nssm set mesh-inbox-watcher AppDirectory $base
nssm set mesh-inbox-watcher Description 'Polls mesh-gateway every 60s for incoming DMs to
# Logging (NSSM-captured python stderr/stdout — separate from logs\inbox.log which the script writes itself) nssm set mesh-inbox-watcher AppStdout "$base\logs\inbox-watcher.log" nssm set mesh-inbox-watcher AppStderr "$base\logs\inbox-watcher.log" nssm set mesh-inbox-watcher AppRotateFiles 1 nssm set mesh-inbox-watcher AppRotateBytes 10485760
# Crash-recovery (Win32 cousin of systemd Restart=on-failure) nssm set mesh-inbox-watcher AppExit Default Restart nssm set mesh-inbox-watcher AppRestartDelay 5000 # 5s backoff between restarts nssm set mesh-inbox-watcher AppThrottle 10000 # 10s minimum lifetime to count as "started"
# CRITICAL: ObjectName must be the SAM account that ran claude login — see §1.5 dual-account trap
nssm set mesh-inbox-watcher ObjectName ".\
Start-Service mesh-inbox-watcher
`
Two log files this creates:
logs\inbox.log— formatted DM records the script writes itself (boxed UTF-8 records, primary surface for the SessionStart hook to tail).logs\inbox-watcher.log— NSSM-captured Python stderr/stdout, operational health (startup line, "recorded msg id=N from=peer kind=fyi" lines, crash tracebacks).
Verify
`powershell
Get-Service mesh-inbox-watcher | Format-Table Name, Status, StartType
# Expected: Running, Automatic.
Get-Content "$base\logs\inbox-watcher.log" -Tail 5
# Expected: "INFO inbox-watcher starting: node=
Get-Content "$base\logs\inbox.log" -Encoding UTF8 -Tail 20
# After ~1 minute and at least one mesh DM landing, expect formatted records.
`
Crash-recovery test (skip if you don't want to disturb a running daemon):
`powershell
$nssmPid = (sc.exe queryex mesh-inbox-watcher | Select-String 'PID\s+:\s+(\d+)').Matches[0].Groups[1].Value
Stop-Process -Id $nssmPid -Force
Start-Sleep -Seconds 8
Get-Service mesh-inbox-watcher | Format-Table Name, Status
# Expected: Running again (NSSM AppExit=Restart auto-recovered after AppRestartDelay).
`
Rollback
`powershell
Stop-Service mesh-inbox-watcher
nssm remove mesh-inbox-watcher confirm
Remove-Item "\(base\scripts\inbox_watcher.py", "\)base\start_inbox_watcher.bat"
# Optionally also: Remove-Item "\(base\logs\inbox.log", "\)base\logs\inbox-watcher.log"
`
Why this isn't bundled into start.bat (single-process colocation)
claude-service and mesh-inbox-watcher are intentionally separate NSSM services — same NSSM idiom, different process. Three reasons:
1. Restart isolation. claude-service restarts trigger StopPending hangs (§6); the inbox-watcher should keep polling while we kill-and-start the FastAPI process. Separate services = independent lifecycles.
2. Failure isolation. A bad claude -p invocation that crashes uvicorn shouldn't drop the mesh-DM surface. Operator can still see peer DMs via inbox.log even if /delegate is down.
3. Symmetric with the Linux substrate. Linux peers run claude-service.service and inbox-watcher.service as separate systemd units. Cross-substrate cousin-class identity per §7's broader pattern — same primitive shape, different scheduling substrate.
Rollback
`powershell
Stop-Service claude-service
nssm remove claude-service confirm
netsh advfirewall firewall delete rule name="claude-service-8787"
Remove-Item -Recurse "C:\Users\pierr\claude-service"
`
Mesh-gateway side: DELETE /peers/razorpeter (or UPDATE claude_peers SET enabled=0 WHERE name='razorpeter').
When to use this vs WSL
If the Windows host has WSL2 + tailscale on the WSL side (the gpu-wsl pattern), prefer running claude-service inside WSL — full Linux systemd, normal .env loading, no NSSM/netsh dance. Use this Windows-native install only when:
- The Windows host has no WSL (razorpeter's case)
- You need Windows-native GPU access (CUDA in WSL works, but driver-tied workflows may want pure Windows)
- You want claude-service to drive Windows-only operations (winget, Windows registry, etc.)