WSL CLAUDE + VPS EGRESS ARCHITECTURE REVIEW
Date: 2026-09-30. Mode: plan review only. Nothing was installed, edited, restarted or deleted.
One throwaway probe was run: claude -p with proxy env vars pointing at a dead loopback port (section 7). It changed no settings and wrote only to the scratchpad and normal session logs.
Limitation up front: this session runs on the VPS (Debian 12, uname shows no Microsoft kernel). I could not inspect WSL, Windows, the WSL-side SSH config, or the WSL Claude install. Every WSL item below is marked UNVERIFIED and is covered by P0 (re-run there).
1. Verdict
PASS_WITH_CHANGES
The topology is sound and the smallest reasonable way to get "Claude-only egress via VPS". Five changes are needed before implementation:
- Proxy variables in
settings.jsonenvwill probably also reach the git/pnpm/curl commands Claude runs in its Bash tool. The docs do not say either way (verified "not found"). If they do, "git/pnpm stay on the local network" only holds for your own terminals, not for commands Claude spawns. This needs a P4 test and aNO_PROXYdecision (section 8). - WSL does not keep background agents alive. Claude's supervisor "outlives the terminal", but WSL's own idle shutdown can still kill it when no Windows-side session is open (section 10).
- Use a dedicated, restricted SSH key for the tunnel (section 6). The WSL box would otherwise hold a full-shell key to the production VPS.
- Re-check the VPS resource story. The VPS is at 92% disk and about 85 MB free RAM; the biggest consumers are Claude sessions, which supports the move (section 16).
- Add a launcher preflight. A dead proxy fails closed but slowly (about 90 s of retries with no output), so a fast port and egress check is a usability requirement, not decoration.
2. Verified Current Environment
VPS (this machine) - verified by command
| Item | Value |
|---|---|
| OS / kernel | Debian 12, Linux 6.1.0-44-amd64, systemd 252 (running) |
| CPU / RAM | 1 vCPU; 1965 MB total, ~85 MB free, ~409 MB available (rest is cache) |
| Disk | / 20 GB, 18 GB used, 1.6 GB free (92%); /var/log 336 MB; journal 12.8 MB |
| Largest processes (RSS) | three claude processes 341 + 235 + 231 MB (~0.8 GB total), two python ~120 MB each, node 90 MB, chrome-headless 86 MB |
| SSH | OpenSSH 9.2p1; PasswordAuthentication no; PermitRootLogin prohibit-password; ClientAliveInterval 60, ClientAliveCountMax 200 (a dead client session can linger ~3.3 h); no AllowTcpForwarding/PermitOpen restriction (forwarding is allowed by default) |
| Listeners | all loopback except 22, 80, 443. 8080 is taken (also 2019, 3001-3002, 4183, 4191, 5230, 8010-8020, 8441-8442, 20241). 18080, 18888, 8888, 3128 are free |
| Proxy packages installed | none (tinyproxy, privoxy, squid, autossh all absent) |
| Firewall | nft and ufw not present or empty output; not verified either way |
| Direct egress IP | 64.186.237.76 (the value the VPS proxy should show in P3) |
| Claude user linger | Linger=no (matters only if a VPS-side user unit is ever used) |
Claude Code (VPS install) - verified
- Version 2.1.285, native install:
~/.local/bin/claude->~/.local/share/claude/versions/2.1.285;autoUpdatesChannel: latest. ~/.claude/settings.json(redacted, keys only):model,statusLine,enabledPlugins,extraKnownMarketplaces,modelSettings,autoUpdatesChannel,skipDangerousModePermissionPrompt,theme.- No
envblock, noprocessWrapper, nosettings.local.json, no/etc/claude-codemanaged settings, no proxy variables in the process environment. - Background supervisor:
claude daemon status-> not running, 0 bg workers. No systemd user unit for it. processWrapperstring is present in the binary. Docs say theprocessWrapperkey needs v2.1.210 or later, so 2.1.285 qualifies.
WSL / Windows - UNVERIFIED (not reachable from here)
Needs P0 on the WSL side: wsl --version, /etc/wsl.conf ([boot] systemd=true?), .wslconfig (networkingMode, autoProxy, instanceIdleTimeout, vmIdleTimeout), WSL-side Claude version and settings, ~/.ssh/config (any ControlMaster), free port 18080, any inherited *_PROXY variables.
Remote Dev repo (VPS) - verified
/home/claude/dev/remote-dev: HEAD 697d72cc3de4aa95a382d5e16c1d16f1b0921c92, tag m1-vps-ready points to the same commit, branch main, 2 commits, working tree clean, no git remote configured, .git 864 KB, working dir 72 MB (71 MB is node_modules, ignored). Only .env.example is tracked; .env and .env.* are gitignored. AGENTS.md and docs/DECISIONS.md exist.
3. Corrected Architecture
Private Git remote (canonical)
^ push ^ clone/pull
| |
VPS checkout (fallback, kept) ----push---> [remote] <---- WSL workspace
|
+-----------------------------+-----------------------+-+---------------+
| | | |
Claude Code (primary) Codex (optional) local tools Windows
env: HTTPS_PROXY= direct network build/test Android
http://127.0.0.1:18080 (native)
NO_PROXY=localhost,127.0.0.1,::1
|
v (only Claude-process traffic; incl. its spawned children unless proven otherwise)
127.0.0.1:18080 (WSL, loopback only)
|
| ssh -N -L 127.0.0.1:18080:127.0.0.1:18888 (dedicated restricted key)
v
VPS 127.0.0.1:18888 tinyproxy, CONNECT to port 443 only, loopback only
|
v
Claude / Anthropic hosts (+ optional: plugins, GitHub, telemetry)
Unchanged direct-from-WSL: git, pnpm/npm, apt, browser, Codex, other apps.
Browser OAuth on Windows: not proxied (see section 9).
Fail-closed rests on one fact: when 127.0.0.1:18080 has no listener, the connection is refused and Claude has no other path.
4. Network Path (per component)
| Component | Path | Status |
|---|---|---|
| Foreground Claude | Reads HTTPS_PROXY/HTTP_PROXY/NO_PROXY (lowercase variants also; order https_proxy, HTTPS_PROXY, http_proxy, HTTP_PROXY) from process env or the settings.json env block. Docs: no SOCKS. Startup only validates that the proxy URL parses. |
Docs verified; P4 confirms |
| Background Claude | Hosted by a per-user supervisor that starts on demand and outlives the shell. It inherits the env of whichever shell cold-started it, or no shell env if OS-installed. Docs: shell exports reach it only by luck, so put vars in settings.json env. Sessions re-read settings when they start; already-running sessions keep old env. |
Docs verified; P6 confirms |
| Local tools (git, pnpm, apt, curl, browser, Codex) | Direct, in your own shells. Caveat: commands Claude runs through its Bash tool, hooks and stdio MCP servers are its children. The docs do not state whether env from settings is inherited by them (searched: not found). Assume yes until P4 proves otherwise. |
Open risk |
| Browser / OAuth | Windows browser is outside WSL env, so it goes direct (section 9). | Verified by design |
| MCP / localhost | Docs: Claude never sends its WebSocket connections to localhost, ::1, 127.0.0.0/8 through the proxy. That covers Claude's own sockets only. Curl/Node/Playwright children calling localhost need NO_PROXY. Remote MCP (claude.ai connectors via mcp-proxy.anthropic.com) goes through the proxy. |
Docs verified for WebSocket only |
| Telemetry / updates / plugins | Same process, same proxy (probe showed Datadog telemetry and bootstrap calls all trying the proxy). Hosts differ by feature: see section 15. | Probe verified |
5. Proxy Recommendation
MVP choice: tinyproxy (Debian 12 package 1.11.1-2.1+deb12u1).
| Option | Disk | RAM | Config | CONNECT | Logging | Verdict |
|---|---|---|---|---|---|---|
| tinyproxy | apt-get -s install adds exactly 2 packages (tinyproxy, tinyproxy-bin); apt metadata shows well under 1 MB installed (exact figure to confirm in P1) |
est. 1-3 MB per process (estimate, not measured) | ~5 directives | yes, ConnectPort allowlist |
metadata only, no bodies; level configurable | Recommended |
| privoxy | ~2.3 MB | est. a few MB | filter-oriented, more knobs than needed | yes | ok | works, oversized for the job |
| squid | ~8.5 MB + cache dirs | tens of MB | large | yes | verbose, cache dirs | reject: wrong tool, disk-tight VPS |
ssh -D (SOCKS) |
0 | 0 | trivial | n/a | none | not usable, Claude Code has no SOCKS support |
HTTP-to-SOCKS bridge in WSL over ssh -D |
0 on VPS | small, on WSL | second daemon on WSL | yes | n/a | valid fallback if you refuse any VPS install; adds a moving part |
| custom Python CONNECT script | 0 | ~20 MB | you own the security code | yes | you write it | reject |
| Caddy (already running, admin on 2019) | 0 | 0 | needs forward_proxy plugin, i.e. a custom build |
n/a | n/a | reject |
Expected tinyproxy config (conceptual): Listen 127.0.0.1, Port 18888, Allow 127.0.0.1, ConnectPort 443, LogLevel Warning, low worker counts (check the shipped tinyproxy.conf for the 1.11 directive names), MaxClients around 20. Complexity: low.
Port note: pick 18888. Port 8080, tinyproxy-adjacent habit, is already used on this VPS.
6. SSH Tunnel Design (conceptual, not to be applied)
WSL side command shape:
ssh -N \
-o ExitOnForwardFailure=yes \
-o ServerAliveInterval=15 -o ServerAliveCountMax=3 \
-o BatchMode=yes \
-o StrictHostKeyChecking=yes \
-o IdentitiesOnly=yes -i <dedicated-key> \
-o ControlMaster=no -o ControlPath=none \
-L 127.0.0.1:18080:127.0.0.1:18888 \
<vps-alias>
- Needed:
-N,ExitOnForwardFailure=yes(otherwise ssh stays up with no forward),ServerAlive*(detects a dead path in ~45 s; the VPSClientAlivesettings do not help WSL notice),BatchMode=yes(fail instead of prompting), strict host key (host key pinned inknown_hostsfirst), key auth. - Needed if the WSL user has connection sharing:
ControlMaster=no/ControlPath=none, or-Ncan hand the forward to an existing master and exit. - Bind explicitly to
127.0.0.1in-L; never0.0.0.0(noGatewayPorts). - Dedicated key, restricted on the VPS
authorized_keys:restrict,port-forwarding,permitopen="127.0.0.1:18888". Stolen WSL key then gets a single loopback port, not a shell. autossh: not needed. OpenSSH plus systemd restart is sufficient and one less package.- Reconnect storm control (Stage B):
Restart=on-failure,RestartSec=5,StartLimitIntervalSec=300,StartLimitBurst=10. Auth failure exits 255 immediately;BatchModeprevents prompts; the unit lands infailedand the launcher preflight (andjournalctl --user -u) shows why. - Stage A is manual, foreground
sshin a spare terminal, Ctrl-C to stop.
7. Fail-Closed Design
Why direct fallback should not happen: with HTTPS_PROXY set, Claude's HTTP agent connects to the proxy address. If nothing listens there the connection is refused; no documented code path retries directly.
Evidence gathered (VPS, actual binary 2.1.285): with HTTPS_PROXY=HTTP_PROXY=http://127.0.0.1:18999 (no listener), claude -p "say ok" produced no output and was killed by my 90 s timeout (exit 124). The debug log shows connect ECONNREFUSED 127.0.0.1:18999 for bootstrap, org fast-mode status, MCP registry, Grove settings, claude.ai MCP fetch (retried 3x), and the final Datadog log flush; 1P telemetry logged "3 failure(s) this session". No request succeeded. Lines matching "direct|fallback" were unrelated (filesystem and CA-config text).
What that does and does not prove: it proves no observed request bypassed the proxy in one run. It is not a packet-level proof. P5 closes that gap by adding a host-level check (see P5).
Practical consequence: fail-closed is slow (retry loop, ~90 s+, no message in -p mode). Hence the launcher preflight.
Ways fail-closed could be defeated (to test or avoid):
- Setting
NO_PROXY=*or a broadNO_PROXY(keep it to loopback names only). - Editing
envin a lower-precedence file while a higher one overrides it (managed >--settings> local > project > user; only user scope is used here, so keep other scopes free of proxy keys). - Running Claude without the variables (a plain
claude), which the launcher and settings both address. - Lowercase variants set to something else:
https_proxywins overHTTPS_PROXY. Check that WSLautoProxy(Windows system proxy) is not injecting a different lowercase value.
8. Claude Settings Design
| Layer | What it does | Needed when |
|---|---|---|
Shell env (claude-vps exports) |
Guards the foreground process you launch. Read once at startup. Cannot reliably reach the supervisor. | P4 (foreground PoC), then optional |
~/.claude/settings.json env |
Only mechanism that reaches every background session (docs). Applied at session start; restart needed for already-running sessions. | P6 onward, and for permanent use |
processWrapper |
Wraps the supervisor, its workers, and self-spawns with a launcher (must end exec "$@"; documented for sandbox/network-control/credential injection). Needs claude daemon stop --any to take effect. Ignored on native Windows. |
Not needed for M1. It is for policies that require every process to start via a wrapper. |
| Background supervisor | Currently not running here. On WSL, run claude daemon status at P0. After changing env: claude daemon stop --any so the next claude agents/--bg starts fresh. Auto-update also restarts it. |
P6 |
Proposed future env block (merge minimally into the existing file; never overwrite it; the current file has no env key, so add only this key):
"env": {
"HTTPS_PROXY": "http://127.0.0.1:18080",
"HTTP_PROXY": "http://127.0.0.1:18080",
"NO_PROXY": "localhost,127.0.0.1,::1"
}
NO_PROXYis worth having because children of Claude (curl, Node, Playwright tests hittinglocalhost) would otherwise be sent to the VPS proxy. Claude's own loopback WebSockets are exempt already; children are not.- The unresolved question: if
envreaches Bash-tool children, then git/pnpm/npm run by Claude use the VPS proxy (allowed only on port 443 by the tinyproxy config, sogit@github.comover SSH would fail andpnpm installwould go via the VPS and consume its bandwidth, not its disk). Options if that is unacceptable: (a) tell pnpm/git to ignore it per project (NO_PROXYfor their hosts), (b) useCLAUDE_CODE_SHELL_PREFIXtounsetthe variables for Bash commands (documented for Bash calls, hooks and stdio MCP startup), (c) accept it. Decide after P4 measurement. - The binary contains strings
CLAUDE_CODE_HTTPS_PROXY/CLAUDE_CODE_HTTP_PROXY, but they are not documented; do not use them. Ask if Anthropic docs add them (a Claude-only proxy variable would solve the leakage). - Wrapper worth keeping:
claude-vpsis still useful as a foreground preflight (fast fail, egress check) even when settings.json holds the env. It is not a background-agent control.
9. OAuth / Authentication Boundary
- Browser: claude.ai sign-in opens a
claude.compage which redirects toclaude.ai, in the Windows browser (no proxy env). This is a one-time or occasional action. - CLI-side: the OAuth code/token exchange, refresh and revocation go to
platform.claude.comfrom the CLI process (docs), so they use the proxy. - Consequence: the login page sees your local IP; API calls see the VPS IP. Whether that mix matters is a question about Anthropic account rules, not something I can answer or that a proxy changes. I am not promising any effect on eligibility, region availability, or risk controls. Keep use within supported regions and the service terms.
- Recommendation: do not proxy the browser. If you later need consistent egress for a documented reason, the simplest supported approach is a browser profile launched with an explicit proxy flag; that is out of scope for M1. Decide only if P4 shows a problem.
- Auth from WSL: the WSL CLI needs its own login (the VPS
.credentials.jsonis not to be copied around).
10. WSL Lifecycle
Verified from Microsoft docs (updated 2026): "systemd services will NOT keep your WSL instance alive." .wslconfig has instanceIdleTimeout (default 15000 ms, -1 disables auto shutdown) and vmIdleTimeout (default 60000 ms, Windows 11). wsl --shutdown stops all distros. I did not verify these on your machine.
| Event | What happens | Recovery |
|---|---|---|
| Normal | Tunnel up; launcher check passes | n/a |
| Tunnel crash / kill | Port 18080 closes; Claude requests fail closed (slowly); in-flight streams abort | Stage A: rerun by hand. Stage B: systemd restarts in ~5 s |
| VPS proxy stopped | Tunnel stays up but forwards nowhere; Claude fails (connection reset), launcher egress check fails | Start tinyproxy |
wsl --shutdown |
All WSL processes die, including tunnel, Claude and background agents | Start WSL; user service restarts tunnel (needs linger or system unit); launcher re-verifies |
| Windows sleep | VM suspended; nothing runs. Nothing can keep developing during sleep | On wake, stale TCP is detected by ServerAlive (~45 s), ssh exits, systemd restarts; in-flight Claude requests fail and retry |
| All terminals closed, no Windows session | WSL may idle-stop even with systemd enabled, taking the supervisor and background agents with it | Keep a wsl.exe session open (Windows Task Scheduler at logon, running sleep infinity), or set instanceIdleTimeout=-1 (a Windows-side .wslconfig change, your decision) |
11. PoC Plan
Every stage stops at its pass criteria; nothing proceeds on "it seems to work".
P0 - Read-only audit (WSL + VPS)
- Goal: fill the UNVERIFIED items.
- Changes: none.
- Verification (WSL):
wsl --version(from Windows),cat /etc/wsl.conf,.wslconfigcontents,claude --version, settings keys (redacted),env | grep -i proxy,claude daemon status,ss -ltn | grep 18080,ssh -V,~/.ssh/configforControlMaster,git --version,nproc; free -m; df -h. VPS: already done above; addss -ltnre-check just before P1 and confirm firewall state with root (nft list ruleset), which I could not read. - Pass: WSL has systemd active, port 18080 free, no unexpected proxy env, and network path WSL->VPS SSH works with the existing key.
- Rollback: n/a.
P1 - Manual VPS localhost proxy (needs approval; touches production VPS)
- Goal: tinyproxy on
127.0.0.1:18888only. - Changes:
apt-get install tinyproxy(2 packages), edit/etc/tinyproxy/tinyproxy.conf(Listen, Port, Allow, ConnectPort 443, LogLevel), start service. Check disk first: only 1.6 GB free, 92% used. - Verification:
ss -ltn | grep 18888shows 127.0.0.1 only; from outside (or via a public IP scan from WSL) port 18888 is closed;curl -x http://127.0.0.1:18888 https://api.ipify.orgon the VPS returns64.186.237.76;ps -o rssfor tinyproxy;df -h /delta. - Pass: loopback-only listener, egress IP correct, RSS under ~10 MB, disk delta under ~2 MB.
- Rollback:
systemctl disable --now tinyproxy && apt-get purge tinyproxy(autoremove optional), remove the config.
P2 - Manual SSH local forward
- Goal:
127.0.0.1:18080on WSL reaches VPS proxy. - Changes: WSL: create dedicated key; VPS: append the restricted
authorized_keysline; run the manual ssh command in a foreground terminal. - Verification:
ss -ltn | grep 18080shows127.0.0.1only;curl -x http://127.0.0.1:18080 https://api.ipify.orgworks; wrong key or wrong host key makes ssh exit non-zero immediately. - Pass: works with restricted key; the restricted key cannot get a shell (
ssh <alias> truefails) or forward to another port (-L ...:127.0.0.1:22refused). - Rollback: Ctrl-C the tunnel; delete the
authorized_keysline and the key.
P3 - Generic HTTP proxy verification
- Goal: prove direct != proxied.
- Verification:
curl -s https://api.ipify.org(WSL local exit, record X) versuscurl -s -x http://127.0.0.1:18080 https://api.ipify.org(must equal64.186.237.76, must differ from X).git ls-remote <private-remote>andpnpm viewwithout proxy variables: unaffected. Also confirmcurlto alocalhostservice with the plannedNO_PROXYbypasses the proxy. - Pass: two different IPs; proxied IP equals VPS IP; non-proxy tools unaffected.
- Rollback: none needed.
P4 - Foreground Claude Code
- Goal: one controlled session through the proxy.
- Changes: none permanent.
HTTPS_PROXY=... NO_PROXY=... claude --debug-file <path>from the shell (no settings edit yet). - Verification:
/statusshows the Proxy row with the expected URL; tinyproxy log (metadata only) shows CONNECT lines forapi.anthropic.com:443and friends; confirm on the VPS which hosts appeared. In the session, run a Bash tool callenv | grep -i proxyto answer the child-inheritance question (for shell exports this is expected to show them; repeat after the settings.json phase). Also runcurl -s https://api.ipify.orgin that Bash call and record which IP it returns. - Pass: session works; proxy log shows Anthropic hosts; the child-inheritance result is recorded.
- Rollback: exit; unset variables.
P5 - Fail-closed
- Goal: prove no direct fallback.
- Changes: none.
- Verification: kill the tunnel, then (a)
HTTPS_PROXY=... claude -p "say ok"must fail with no model output; (b) in parallel, on WSL run a packet check:sudo tcpdump -ni any 'host api.anthropic.com or host platform.claude.com'(orss -tnp | grep claude) must show no connections from the claude PID except to127.0.0.1:18080. Repeat with only tinyproxy stopped. - Pass: no completion, no direct connection; the launcher preflight also refuses.
- Rollback: restart tunnel/proxy.
P6 - Background agent
- Goal: background sessions use the proxy too.
- Changes: add the minimal
envblock to~/.claude/settings.json(backup first, merge, do not overwrite);claude daemon stop --any. - Verification:
claude daemon status;claude --bg "run: env | grep -i proxy; curl -s https://api.ipify.org; stop", thenclaude logs <id>; VPS tinyproxy log shows the connections from this session's time window; then kill the tunnel and repeat: the background task must fail. Also test a cold start from a shell without the exports, which is the case the docs warn about. - Pass: background session reports proxy, IP equals the VPS IP, and fails closed with tunnel down.
- Rollback: remove the
envkey (restore backup),claude daemon stop --any.
P7 - WSL lifecycle
- Goal: recovery behaves as section 10 says.
- Changes: Stage B unit (user service,
Restart=on-failure, start-limit, enable-linger) only after P0-P6 pass. - Verification: kill ssh -> restarts within ~10 s;
wsl --shutdown-> restart WSL -> tunnel back without manual steps; close all terminals for 5 min -> check whether WSL stopped (wsl --list --running); optional sleep/wake -> tunnel recovers within ~1 min; break the key -> unitfailed, launcher refuses, no restart storm (journal shows at most 10 starts per 5 min). - Pass: each row of the section 10 table matches observation.
- Rollback:
systemctl --user disable --now, delete the unit.
P8 - Git migration
See section 12. Pass: WSL clone at tag m1-vps-ready, existing light and full scripts pass on WSL.
P9 - Development trial
- Goal: compare on one small milestone.
- Verification: record time-to-first-answer, retries, disconnects, failed requests, WSL RAM/disk, and number of manual tunnel interventions, against the same kind of task on the VPS.
- Pass: you decide the migration. No numeric threshold is proposed here; that is your call.
12. Migration Plan (Remote Dev M1)
Order matters; the VPS checkout stays untouched throughout.
- Now (no network PoC needed): create an empty private remote (any host you already use; this is a "decision needed" item). Confirm
.gitignorecovers.env*(verified) and that no secrets are in history (2 commits; run a quick scan on the tracked file list, only.env.exampleis present). - On the VPS:
git remote add origin <url>;git push origin main;git push origin m1-vps-ready. Never--force. - Confirm
git ls-remote originshows697d72cc...formainand the tag. - On WSL:
git clone <url>;git checkout m1-vps-ready(detached) to verify identical commit697d72cc3de4aa95a382d5e16c1d16f1b0921c92, then back tomain. - Set
DEV_ENV=wsl; run the existing light and full scripts;pnpm installetc. happen on WSL, not the VPS. - Keep the VPS checkout as fallback until P9 passes; from then on WSL is where new commits are made and the VPS pulls only when deploying.
.envfiles and Remote Dev credentials are not in git: recreate them on WSL deliberately (do not copy from the VPS blindly).
Note: this migration does not depend on the proxy, so it can run in parallel with P0-P3. The instruction to do it only after the network PoC is a conservative ordering, not a technical dependency; your call.
13. Revised Hybrid Development Standard
| Task | WSL Claude | WSL Codex | VPS | Windows |
|---|---|---|---|---|
| Architecture / design / DECISIONS.md | primary | second opinion | no | no |
| Coding + debugging | primary | mechanical tasks / fallback | no (fallback checkout only) | no |
| Light checks (lint, unit) | yes | yes | yes (light only) | no |
| Full build / integration / Playwright / Runner tests | yes | verifier | no (refused by guard) | no |
| Git canonical remote push/pull | yes | yes | pull for deploy | no |
| Production runtime, deploy, backup | no | no | yes | no |
| Claude network egress | client | n/a | tiny proxy | n/a |
| Android SDK / emulator / native GUI | no | no | no | yes |
Unchanged from the existing standard: Git remote is canonical; AGENTS.md; docs/DECISIONS.md; check/test/build scripts; production isolation; agent replaceability; VPS/WSL guards.
14. Impact on Remote Dev
Only what this topology changes:
- The primary agent and its Runner now live on WSL, which is intermittently available (sleep, shutdown, idle stop). Server on the VPS (~77 MB) stays; Runner must connect outbound to it and the product must treat "runner offline" as a normal state, not an error.
- Session survival across PC sleep is impossible; sessions should be resumable/re-attachable rather than assumed continuous.
- The Runner's own traffic to your VPS server should not be forced through the Claude proxy: keep the VPS host in
NO_PROXY, or run Runner without the proxy env. - Remote Dev remains useful for mobile control, monitoring, approval, artifacts. No M2 redesign is proposed here.
15. Security Risks
HIGH
- WSL holds an SSH key to the production VPS. Mitigation: dedicated key with
restrict,port-forwarding,permitopen="127.0.0.1:18888"; passphrase or agent if practical. - VPS proxy accidentally bound to a non-loopback address (public open proxy). Mitigation:
Listen 127.0.0.1, verify withssand an external scan in P1.
MEDIUM
- Proxy variables leaking into Claude's child processes (git/pnpm/curl/localhost tests) or, conversely, private hosts (your Remote Dev server) going through the proxy. Mitigation:
NO_PROXY, P4 measurement. - Any process on the VPS can use
127.0.0.1:18888(no proxy auth). Low incremental risk (they already have direct egress), butConnectPort 443keeps it narrow. Same on WSL: withlocalhostForwarding(default true) Windows apps can reach WSL's127.0.0.1:18080. - Silent misrouting from WSL
autoProxyinjecting Windows proxy settings or lowercasehttps_proxyoverriding uppercase. Check in P0. - Background agents dying on WSL idle-stop, then sessions "resurrecting" from a shell that lacks proxy env (docs warn about this). Mitigation: settings-based
env. - Compliance: using a proxy does not change account eligibility or region rules; do not rely on it for that.
LOW
- Proxy logs contain hostnames/timestamps of Claude activity. Mitigation:
LogLevel Warning, logrotate (tinyproxy depends onlogrotate). - Stale sshd sessions on VPS for ~3.3 h after a hard WSL death (
ClientAliveCountMax 200). Harmless for a forward that listens on the WSL side. - Settings-scope drift (project or managed settings overriding
env).
16. Resource Impact
| OLD: Claude on VPS | NEW: Claude on WSL + tinyproxy on VPS | |
|---|---|---|
| VPS RAM | each Claude session ~230-340 MB RSS (three observed now, ~0.8 GB of 1.97 GB), plus node/chrome/tooling for tests | tinyproxy est. 1-3 MB per process (not measured) + sshd per tunnel connection |
| VPS disk | Claude data (~/.claude incl. sessions, debug, file history, plugins), 71 MB node_modules, dev tooling; currently 1.6 GB free |
tinyproxy under ~1 MB installed + small logs; and dev checkout/caches can later be pruned (only after fallback is retired; not recommended yet) |
| VPS CPU | 1 vCPU shared by dev sessions and production | negligible |
| Failure mode | OOM/disk-full risk at the current 92% | dependency on WSL uptime and a tunnel |
Estimated saving: the roughly 0.6-0.8 GB RAM currently used by Claude sessions, and freedom to reclaim disk once the fallback is retired. The increment is a few MB. These are estimates from this snapshot; P1 and P9 give measured numbers.
Also relevant: the VPS already runs a 92%-full disk, so any P1 install should be preceded by checking df and clearing nothing without asking.
17. What Not To Build
WireGuard, Tailscale exit node, full-WSL VPN, transparent iptables/nftables interception, Docker or Kubernetes proxy stacks, LLM gateways, PKI/mTLS, multi-hop proxies, Windows-wide proxy settings, processWrapper for M1, autossh, proxy authentication for the MVP, browser proxying, a hostname allowlist for Claude endpoints, a custom proxy daemon, Squid.
18. Decisions Needed From User
Only items I cannot learn by inspection:
- Which private Git host for the remote (and its URL).
- Are you OK approving P1 on the production VPS (one small package; disk is at 92%)?
- WSL keep-alive: accept manual open-terminal discipline, or allow a Windows-side change (
instanceIdleTimeout=-1in.wslconfigor a logon task)? - If P4 shows proxy env leaking into Claude's Bash children: prefer
NO_PROXY-only,CLAUDE_CODE_SHELL_PREFIXto unset, or accept it? - Do you accept that browser sign-in stays on your local IP (section 9)?
- Whether you are comfortable hosting the M1 history (2 commits, no secrets tracked) on the chosen third-party Git host.
19. Recommended First Implementation Prompt (do not run yet)
Task: WSL + VPS-egress PoC, stages P0-P3 only. Work in stages; stop and report after each; do not touch Remote Dev source, Git remotes, or Claude
settings.json.
- P0 (read-only, on WSL): report
wsl --version,/etc/wsl.conf,.wslconfig(idle timeouts, networking mode, autoProxy),claude --version, redacted settings keys,claude daemon status,env | grep -i proxy,~/.ssh/config(ControlMaster), port 18080 usage,nproc/free/df. On the VPS, additionally confirm firewall state as root.- P1 (needs my explicit "go"): on the VPS, check
df, then installtinyproxy, configureListen 127.0.0.1,Port 18888,Allow 127.0.0.1,ConnectPort 443,LogLevel Warning; verify loopback-only withssand an external port probe; record RSS and disk delta; show the exact rollback command.- P2: create a dedicated ed25519 key on WSL; show me the
authorized_keysline withrestrict,port-forwarding,permitopen="127.0.0.1:18888"and wait for approval before adding it; pin the host key; run the tunnel manually in the foreground with the option set in section 6; prove the key cannot get a shell or forward elsewhere.- P3: compare
curldirect versus via127.0.0.1:18080; expected proxied IP64.186.237.76; confirm git/pnpm without proxy variables are unaffected.
Rules: no systemd units, nosettings.jsonedits, no launcher script yet, no browser proxying; each stage ends with PASS/FAIL against explicit criteria and a rollback note.