Repository navigation
[Bug]: Packaged builds set server cwd to $HOME; ClaudeProvider auxiliary spawns leak it past project bindings #2867
Description
Activity
This can be broadened beyond
ClaudeProvider.tsandcwd: the same class of
auxiliary spawns also leaks the backend's environment, and each driver currently
handles these differently. There are details and a decision point for maintainers
below, but in short, I believe both leaks (env and cwd) across all providers can
be handled with one fairly simple pattern.On Linux AppImage builds, provider sessions inherit
ELECTRON_RUN_AS_NODE=1,
APPDIR,LD_LIBRARY_PATH, and/tmp/.mount_*PATHentries — details in
#1162 (comment).
ClaudeProvider.tsfalls back toenvironment ?? process.envon its probe path,
andmergeProviderInstanceEnvironment(used by all five drivers) likewise defaults
to rawprocess.env.On the cwd side the drivers have drifted apart:
ClaudeProvider.tspasses nocwdon auxiliary spawns (inherits the server's —
this issue).CursorProvider.tsexplicitly passescwd: process.cwd()— the same$HOMEin
a packaged build, just spelled out.GrokDriver.tsandOpenCodeDriver.tspassServerConfig.cwd(the server's
startup cwd).CodexProvider.tsthreads acwdparameter through its status-check plumbing.
Env construction diverges the same way (
environment ?? process.envvs
...(environment ? { env } : { extendEnv: true })vs the driver-layer merge).So beyond fix (B) there's a decision point for the maintainers: patch the Claude
call sites and leave each driver hand-rolling its spawn options, or consolidate —
one shared helper that builds provider child spawn options (explicit scopedcwd,
plus an env sanitized with the logic already interminal/Manager.ts), applied at
the driver boundary. Consolidation closes both leak classes for all five providers
at one chokepoint while keeping the blast radius small: it touches neither
backendCwd(fix A), nor the terminal path, nor the session path, which the
issue description already notes plumbsworkspaceRootcorrectly.I'd be happy to test Linux/AppImage.
Reacted by Xian ZhangOn WSL,
homeDirectoryis the Windows profile, sobackendCwdbecomesC:\Users\<user>andwsl.exestarts the Linux backend in/mnt/c/Users/<user>. That value reaches provider probes and the servers they spawn:DesktopEnvironment.ts:224setsbackendCwdfromhomeDirectorymain.ts:80suppliesNodeOS.homedir()DesktopBackendConfiguration.ts:746passes it ascwdtowsl.exeOpenCodeDriver.ts:142passesserverConfig.cwdas the OpenCodedirectory, andOpenCodeServerOwnergives the same value to the spawnedopencode serve
A T3-spawned OpenCode server was running here with
cwd=/mnt/c/Users/<user>. Same value as #8955, which has a clean Codex repro. I could not reproduce the OpenCode timeout directly, so I am not claiming this explains every case.Fix: resolve
backendCwdto the distro's Linux home in WSL mode.DesktopWslEnvironment.getUserHomeatDesktopWslEnvironment.ts:1271already does this for the folder picker. Doing it atbackendCwdcovers every provider instead of one, and keeps$HOMEsemantics since the Linux home is the backend's$HOME.
Summary
In packaged desktop builds, the Node server child process is deliberately spawned with
cwd = $HOME. Two functions inClaudeProvider.tsthen spawn theclaudebinary without specifying anycwdof their own, so they inherit$HOME— regardless of whether the user has a Project bound to the current thread.For users on stock
claude, this is mostly invisible. For any third-party wrapper that replaces or shims theclaudebinary and treats its cwd as the project boundary (containerized agent runtimes, mount-scoped sandboxes, governance shims), every capability probe and ad-hoc CLI call comes in with cwd =$HOMEand either exposes the user's entire home directory to the agent or fails outright. The session path itself plumbs the project'sworkspaceRootcorrectly, but the probe-driven provider snapshot runs first, fails, and marks the provider Unavailable — so the user never reaches a working session even with the binding set.Where it happens
1. Server cwd is set to
$HOMEin packaged builds.apps/desktop/src/app/DesktopEnvironment.ts:backendCwdflows throughDesktopBackendConfiguration.cwdintoDesktopBackendManager'sChildProcess.make(..., { cwd }). So the server'sprocess.cwd()is$HOMEin any installed build.2. Claude binary spawns inherit it.
apps/server/src/provider/Layers/ClaudeProvider.ts:probeClaudeCapabilitiescalls the SDK'sclaudeQuerywithout anycwdoption — the SDK then spawnsclaudewith the parent process's cwd.runClaudeCommandusesChildProcess.make(claudeSettings.binaryPath, args, { env, shell })with nocwdoption — same inheritance.Both inherit
$HOMEfrom the server process.3. Session path is fine, but it's gated behind the probe.
apps/server/src/orchestration/Layers/ProviderCommandReactor.tscorrectly resolveseffectiveCwdfrom the thread's bound project and passes it toproviderService.startSession({ cwd }). That path is well-formed. The problem is the snapshot-refresh probe runs on a 5-minute interval (and on provider events) and uses the broken path, which is what surfaces the provider as "Unavailable" in the UI before a session even starts.Why it matters
~/code/myprojecthas no way to know that periodic probes are still firing from$HOME. Cwd leakage past an explicit binding is surprising and hard to debug from outside.$HOMEhappens to contain dot dirs the kernel blocks from being mount-overlaid (.Trashetc.), so for container-based wrappers the symptom is a cryptic runc EPERM rather than "we exposed your home folder." Either outcome is bad.Related but distinct: #316 (closed) tightened client-supplied cwd on WebSocket RPCs. The surface here is server-side: the server's own
process.cwd()and the unscoped spawns insideClaudeProvider.Repro
/Applications.~/code/myproject). Start a new thread inside it — confirm the thread header shows the Project name.claudeonPATHwith a wrapper that logs its cwd and exits, or use any wrapper that scopes to cwd.$HOME, not the bound Project'sworkspaceRoot.Suggested fix
Two parts:
A. Decouple the file-picker default-open path from the server's
process.cwd().The packaged-build
backendCwd = homeDirectorylooks like it exists so the "Add Project" picker opens at$HOME. That UX default should be sourced from a config field (it already has a UI surface — "Add project starts in") and read by the picker directly, not by setting the server's process cwd. SetbackendCwdto something neutral (e.g. the app's user-data directory, orappRoot).B. Pass an explicit
cwdto everyclaudespawn inClaudeProvider.ts.Both
probeClaudeCapabilities(via the SDK'scwdoption) andrunClaudeCommand(viaChildProcess.make'scwdoption) should pass an explicit, scoped path — at minimum, the user-data directory; ideally, the bound project'sworkspaceRootwhen one is available in context. Falling through to the parent process's cwd is the bug.Doing only (B) is sufficient to unbreak third-party wrappers. Doing (A) as well removes the underlying source of surprise and is the more honest fix.
Environment
.Trashinteraction makes this most visible here, but the cwd leak itself is OS-independent)isPackaged === true)