[GH-ISSUE #5104] [Bug] Empty APPDATA makes Windows client registration write into the working directory #3140

Closed
opened 2026-10-31 00:37:06 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @coygeek on GitHub (Oct 18, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/5104

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Setup, packaging, or upgrade

Observed behavior

Observed at the production application/filesystem boundary on macOS with an injected win32 path context: supportedClients(home, "win32", { APPDATA: "" }) selects relative configuration paths for VS Code, Trae and Devin. Passing those client records to the actual configureClientConfiguration writer creates their registrations beneath the working directory and returns configured. The corresponding home/AppData/Roaming files remain absent. With APPDATA omitted, the same writer creates the home-root files.

This is an injected-platform boundary reproduction, not a native Windows CLI or real client run. Native Windows consequences are SOURCE-ONLY reasoning from the same path expressions.

Reproduction

From the source checkout at the revision below with its locked dependencies already installed, save this probe as $WORK/reproduce.mts, where WORK is a fresh scratch directory outside the checkout:

import assert from 'node:assert/strict';
import { mkdir, readFile, access, realpath } from 'node:fs/promises';
import { join, resolve, relative, isAbsolute } from 'node:path';
const checkout = process.env.REA_CHECKOUT!;
const root = await realpath(process.env.REA_PROBE_ROOT!);
console.log(JSON.stringify({node:process.version,platform:process.platform,arch:process.arch,os:(await import('node:os')).release()}));
const { supportedClients } = await import(join(checkout, 'src/application/SupportedClients.ts'));
const { configureClientConfiguration } = await import(join(checkout, 'src/application/SetupClientConfiguration.ts'));
const { windowsAppDataRoots } = await import(join(checkout, 'src/application/MsixPackageStorage.ts'));
for (const kind of ['empty', 'unset']) {
  const base = join(root, kind), home = join(base, 'home'), cwd = join(base, 'cwd');
  await mkdir(home, { recursive: true }); await mkdir(cwd, { recursive: true });
  process.chdir(cwd);
  const env = kind === 'empty' ? { APPDATA: '' } : {};
  const clients = supportedClients(home, 'win32', env);
  const fallback = windowsAppDataRoots(home, env).roaming;
  for (const name of ['vscode', 'trae', 'devin']) {
    const client = clients.find((c: any) => c.name === name)!;
    assert.equal(client.configPathError, undefined);
    assert.equal(isAbsolute(client.configPath), kind === 'unset');
    const result = await configureClientConfiguration(client, {}, ['/fixture/rea', 'mcp']);
    assert.equal(result.status, 'configured', JSON.stringify(result));
    const actual = resolve(client.configPath);
    assert.ok(actual.startsWith(base + '/'));
    const config = JSON.parse(await readFile(actual, 'utf8'));
    assert.ok((name === 'vscode' ? config.servers : config.mcpServers).rea);
    const suffix = name === 'vscode' ? ['Code','User','mcp.json'] : name === 'trae' ? ['Trae','User','mcp.json'] : ['devin','mcp_config.json'];
    const expected = join(fallback, ...suffix);
    const expectedExists = await access(expected).then(() => true, () => false);
    assert.equal(expectedExists, kind === 'unset');
    console.log(JSON.stringify({kind,name,path:relative(base,actual),relative:!isAbsolute(client.configPath),result:result.status,defaultFileExists:expectedExists}));
  }
}

Run the same isolated boundary procedure used for verification:

mkdir -p "$WORK/home" "$WORK/temp" "$WORK/fixtures"
env -i PATH="$PATH" HOME="$WORK/home" USERPROFILE="$WORK/home" \
  TMPDIR="$WORK/temp" REA_CHECKOUT="$PWD" REA_PROBE_ROOT="$WORK/fixtures" \
  node node_modules/tsx/dist/cli.mjs "$WORK/reproduce.mts"

The script creates only fixture files, supplies an explicit synthetic environment to path selection, and writes configuration text containing an inert launcher. It never executes that launcher. Use a fresh fixtures directory for each run. All six placement and readback cases passed in the completed verification runs.

Expected behavior

An empty APPDATA should select the default home/AppData/Roaming root, as REA's shared Windows AppData contract explicitly defines and its empty/unset test asserts. These three clients already select that root when APPDATA is omitted. Setup should configure the intended user location or report a specific path failure before writing a working-directory registration.

Impact and scope

The observed impact is bounded registration misplacement: three first-run configuration transactions returned configured at working-directory paths, while the default home-root targets stayed absent. Omitting APPDATA restored home-root placement for all three controls. Existing misplaced files require separate cleanup; unsetting the variable does not remove them.

NOT_ENOUGH_INFO: affected-user frequency, native Windows environment delivery, actual client loading, and full CLI readiness/doctor/uninstall outcomes were not measured. No data loss or exposure was observed.

REA version

Source package metadata 6.7.0; tested source SHA 3537e58c1ab29309f06eabf50bc52adba9817a0c. The relevant path and writer sources match stable tag rea-agents-6.7.0; the published package was not executed.

Runtime and platform

Probe process: Node.js v24.22.0, macOS (darwin), arm64, kernel 27.0.0. Path-selection argument: win32. The reproduction therefore prints host-style forward slashes. Native Windows execution is NOT_ENOUGH_INFO. No analysis engine or provider was involved.

Target and artifact context

Synthetic client registrations, isolated home and working directory; no analysis target.

Interface

Package or setup

Earliest failure stage

Setup or provider admission

Relevant output or evidence

Observed placement and return values, reduced from the probe's JSON output:

empty vscode cwd/Code/User/mcp.json configured defaultFileExists=false
empty trae cwd/Trae/User/mcp.json configured defaultFileExists=false
empty devin cwd/devin/mcp_config.json configured defaultFileExists=false
unset vscode home/AppData/Roaming/Code/User/mcp.json configured defaultFileExists=true
unset trae home/AppData/Roaming/Trae/User/mcp.json configured defaultFileExists=true
unset devin home/AppData/Roaming/devin/mcp_config.json configured defaultFileExists=true
exit=0

The script also inspected the written JSON for the rea registration and checked whether each path was absolute.

The supported setup caller reaches this code: CLI setup passes the environment to systemSetupHost, whose client catalog uses supportedClients. Explicit client selection uses all supported clients, independently of a detected marker. Its configuration operation calls the production writer exercised by the probe. This caller trace is source-derived; full CLI setup was not run.

Typed outcome or recovery expected

Resolve empty APPDATA to the existing default root, or return a path-specific failure before creating the relative configuration. The successful unset controls establish the default-root recovery at the exercised boundary.

Analysis or suspected cause

The VS Code and Trae helpers and Devin helper use nullish coalescing, so an empty string survives. The shared Windows AppData owner instead uses an empty-aware fallback. The transaction-path check accepts a missing relative path unchanged, and publication creates its relative parent before writing. The probe directly observed the resulting files.

Additional context

The issue search found related empty-root reports for other variables and clients, including empty CLAUDE_CONFIG_DIR, empty HOME, and empty XDG_CONFIG_HOME for OpenCode. Their triggers and client selectors differ. The MSIX AppData whitespace report covers preservation of nonempty values, rather than this empty-value fallback.

Originally created by @coygeek on GitHub (Oct 18, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5104 ## Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ## Area Setup, packaging, or upgrade ## Observed behavior Observed at the production application/filesystem boundary on macOS with an injected `win32` path context: `supportedClients(home, "win32", { APPDATA: "" })` selects relative configuration paths for VS Code, Trae and Devin. Passing those client records to the actual `configureClientConfiguration` writer creates their registrations beneath the working directory and returns `configured`. The corresponding home/AppData/Roaming files remain absent. With APPDATA omitted, the same writer creates the home-root files. This is an injected-platform boundary reproduction, not a native Windows CLI or real client run. Native Windows consequences are SOURCE-ONLY reasoning from the same path expressions. ## Reproduction From the source checkout at the revision below with its locked dependencies already installed, save this probe as `$WORK/reproduce.mts`, where `WORK` is a fresh scratch directory outside the checkout: ```ts import assert from 'node:assert/strict'; import { mkdir, readFile, access, realpath } from 'node:fs/promises'; import { join, resolve, relative, isAbsolute } from 'node:path'; const checkout = process.env.REA_CHECKOUT!; const root = await realpath(process.env.REA_PROBE_ROOT!); console.log(JSON.stringify({node:process.version,platform:process.platform,arch:process.arch,os:(await import('node:os')).release()})); const { supportedClients } = await import(join(checkout, 'src/application/SupportedClients.ts')); const { configureClientConfiguration } = await import(join(checkout, 'src/application/SetupClientConfiguration.ts')); const { windowsAppDataRoots } = await import(join(checkout, 'src/application/MsixPackageStorage.ts')); for (const kind of ['empty', 'unset']) { const base = join(root, kind), home = join(base, 'home'), cwd = join(base, 'cwd'); await mkdir(home, { recursive: true }); await mkdir(cwd, { recursive: true }); process.chdir(cwd); const env = kind === 'empty' ? { APPDATA: '' } : {}; const clients = supportedClients(home, 'win32', env); const fallback = windowsAppDataRoots(home, env).roaming; for (const name of ['vscode', 'trae', 'devin']) { const client = clients.find((c: any) => c.name === name)!; assert.equal(client.configPathError, undefined); assert.equal(isAbsolute(client.configPath), kind === 'unset'); const result = await configureClientConfiguration(client, {}, ['/fixture/rea', 'mcp']); assert.equal(result.status, 'configured', JSON.stringify(result)); const actual = resolve(client.configPath); assert.ok(actual.startsWith(base + '/')); const config = JSON.parse(await readFile(actual, 'utf8')); assert.ok((name === 'vscode' ? config.servers : config.mcpServers).rea); const suffix = name === 'vscode' ? ['Code','User','mcp.json'] : name === 'trae' ? ['Trae','User','mcp.json'] : ['devin','mcp_config.json']; const expected = join(fallback, ...suffix); const expectedExists = await access(expected).then(() => true, () => false); assert.equal(expectedExists, kind === 'unset'); console.log(JSON.stringify({kind,name,path:relative(base,actual),relative:!isAbsolute(client.configPath),result:result.status,defaultFileExists:expectedExists})); } } ``` Run the same isolated boundary procedure used for verification: ```sh mkdir -p "$WORK/home" "$WORK/temp" "$WORK/fixtures" env -i PATH="$PATH" HOME="$WORK/home" USERPROFILE="$WORK/home" \ TMPDIR="$WORK/temp" REA_CHECKOUT="$PWD" REA_PROBE_ROOT="$WORK/fixtures" \ node node_modules/tsx/dist/cli.mjs "$WORK/reproduce.mts" ``` The script creates only fixture files, supplies an explicit synthetic environment to path selection, and writes configuration text containing an inert launcher. It never executes that launcher. Use a fresh fixtures directory for each run. All six placement and readback cases passed in the completed verification runs. ## Expected behavior An empty APPDATA should select the default home/AppData/Roaming root, as REA's [shared Windows AppData contract](https://github.com/morluto/rea/blob/3537e58c1ab29309f06eabf50bc52adba9817a0c/src/application/MsixPackageStorage.ts#L28-L35) explicitly defines and its [empty/unset test](https://github.com/morluto/rea/blob/3537e58c1ab29309f06eabf50bc52adba9817a0c/src/application/ClaudeDesktopConfigLocation.test.ts#L42-L49) asserts. These three clients already select that root when APPDATA is omitted. Setup should configure the intended user location or report a specific path failure before writing a working-directory registration. ## Impact and scope The observed impact is bounded registration misplacement: three first-run configuration transactions returned `configured` at working-directory paths, while the default home-root targets stayed absent. Omitting APPDATA restored home-root placement for all three controls. Existing misplaced files require separate cleanup; unsetting the variable does not remove them. NOT_ENOUGH_INFO: affected-user frequency, native Windows environment delivery, actual client loading, and full CLI readiness/doctor/uninstall outcomes were not measured. No data loss or exposure was observed. ## REA version Source package metadata 6.7.0; tested source SHA `3537e58c1ab29309f06eabf50bc52adba9817a0c`. The relevant path and writer sources match stable tag `rea-agents-6.7.0`; the published package was not executed. ## Runtime and platform Probe process: Node.js v24.22.0, macOS (`darwin`), arm64, kernel 27.0.0. Path-selection argument: `win32`. The reproduction therefore prints host-style forward slashes. Native Windows execution is NOT_ENOUGH_INFO. No analysis engine or provider was involved. ## Target and artifact context Synthetic client registrations, isolated home and working directory; no analysis target. ## Interface Package or setup ## Earliest failure stage Setup or provider admission ## Relevant output or evidence Observed placement and return values, reduced from the probe's JSON output: ```text empty vscode cwd/Code/User/mcp.json configured defaultFileExists=false empty trae cwd/Trae/User/mcp.json configured defaultFileExists=false empty devin cwd/devin/mcp_config.json configured defaultFileExists=false unset vscode home/AppData/Roaming/Code/User/mcp.json configured defaultFileExists=true unset trae home/AppData/Roaming/Trae/User/mcp.json configured defaultFileExists=true unset devin home/AppData/Roaming/devin/mcp_config.json configured defaultFileExists=true exit=0 ``` The script also inspected the written JSON for the `rea` registration and checked whether each path was absolute. The supported setup caller reaches this code: [CLI setup](https://github.com/morluto/rea/blob/3537e58c1ab29309f06eabf50bc52adba9817a0c/src/cli/setupCommands.ts#L212-L225) passes the environment to [systemSetupHost](https://github.com/morluto/rea/blob/3537e58c1ab29309f06eabf50bc52adba9817a0c/src/application/SetupHost.ts#L145-L151), whose [client catalog](https://github.com/morluto/rea/blob/3537e58c1ab29309f06eabf50bc52adba9817a0c/src/application/SetupHost.ts#L266-L268) uses `supportedClients`. [Explicit client selection](https://github.com/morluto/rea/blob/3537e58c1ab29309f06eabf50bc52adba9817a0c/src/application/SetupPlan.ts#L148-L164) uses all supported clients, independently of a detected marker. Its [configuration operation](https://github.com/morluto/rea/blob/3537e58c1ab29309f06eabf50bc52adba9817a0c/src/application/SetupHost.ts#L320-L333) calls the production writer exercised by the probe. This caller trace is source-derived; full CLI setup was not run. ## Typed outcome or recovery expected Resolve empty APPDATA to the existing default root, or return a path-specific failure before creating the relative configuration. The successful unset controls establish the default-root recovery at the exercised boundary. ## Analysis or suspected cause The [VS Code and Trae helpers](https://github.com/morluto/rea/blob/3537e58c1ab29309f06eabf50bc52adba9817a0c/src/application/SupportedClients.ts#L114-L143) and [Devin helper](https://github.com/morluto/rea/blob/3537e58c1ab29309f06eabf50bc52adba9817a0c/src/application/SupportedClients.ts#L480-L483) use nullish coalescing, so an empty string survives. The shared Windows AppData owner instead uses an empty-aware fallback. The [transaction-path check](https://github.com/morluto/rea/blob/3537e58c1ab29309f06eabf50bc52adba9817a0c/src/application/ClientConfigPath.ts#L20-L33) accepts a missing relative path unchanged, and [publication](https://github.com/morluto/rea/blob/3537e58c1ab29309f06eabf50bc52adba9817a0c/src/application/SetupClientConfiguration.ts#L316-L324) creates its relative parent before writing. The probe directly observed the resulting files. ## Additional context The issue search found related empty-root reports for other variables and clients, including [empty CLAUDE_CONFIG_DIR](https://github.com/morluto/rea/issues/4262), [empty HOME](https://github.com/morluto/rea/issues/2325), and [empty XDG_CONFIG_HOME for OpenCode](https://github.com/morluto/rea/issues/2168). Their triggers and client selectors differ. [The MSIX AppData whitespace report](https://github.com/morluto/rea/issues/4496) covers preservation of nonempty values, rather than this empty-value fallback.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#3140
No description provided.