Repository navigation
Description
Summary
Adding a shadow home to a Codex instance that selects its shared home through an environment variable silently switches its shared sessions and configuration to the default home. The shadow account should continue using the selected shared home. This was reproduced through the production driver with disposable directories and no Codex process, credentials, or inference; direct mode and an explicit shared-home setting both selected the intended home.
Before submitting
- I searched existing issues and did not find a duplicate. Related home-selection history appears under References.
- I included enough detail to reproduce or investigate the problem. Credentials, tokens, private paths, and sensitive data have been removed.
Area
apps/server
Actual behavior
At the pinned commit, create an existing/manual Codex instance with blank homePath, a nonempty shadowHomePath, and a per-instance CODEX_HOME environment value naming an existing custom directory. CodexDriver.create assigns codex:home:<user-home>/.codex as its continuation key and links <shadow>/sessions to <user-home>/.codex/sessions. A config.toml that exists only in the custom home is absent from the shadow home. The same instance environment in direct mode selects the custom home, and explicitly setting homePath to that custom home makes the shadow mode select it too.
Steps to reproduce
- Use a clean checkout of the commit below with its lockfile dependencies installed (
vp install --frozen-lockfile --ignore-scriptswas used here). - Run the script below. It uses the existing driver test fixture, disables the provider to prohibit spawning, and supplies disposable user, Codex, XDG, temporary, and T3 state directories.
- Observe that the direct-home and explicit-shadow control passes, while the environment-selected shadow case points at the default home and fails the continuation-key assertion.
The focused production-driver case failed once after the harness was corrected, with its control passing in the same run. Earlier harness attempts were inconclusive: an absolute /tmp filter did not match the checkout's canonical /private/tmp path; a subsequent run exposed the same failed assertion but its result classifier did not recognize ANSI-colored booleans. The included script is the corrected relative-path, no-color version that printed RESULT: defect reproduced.
Save this as repro-shadow-env.sh and run sh repro-shadow-env.sh /tmp/t3code. Prerequisites are the checked-out commit, Node, and its pinned installed Vite+ dependencies. The script adds and removes one temporary test file beside the existing driver tests, places every fixture under mktemp -d, and clears inherited environment variables for the test process. It prints RESULT: expected behavior with exit 0 after restoration, RESULT: defect reproduced with exit 1 for the observed failure with a passing control, or RESULT: inconclusive with exit 2 for setup/control failure. It does not modify production code.
Reproduction script: repro-shadow-env.sh
#!/bin/sh
set -u
repo=$(cd "$1" && pwd) || exit 2
node --version
node "$repo/node_modules/vite-plus/bin/vp" --version
scratch=$(mktemp -d /tmp/codex-home-repro.XXXXXX) || exit 2
spec="$repo/apps/server/src/provider/Drivers/CodexHomeEnvironment.repro.test.ts"
cleanup() { rm -f "$spec"; rm -rf "$scratch"; }
if [ -e "$spec" ]; then rm -rf "$scratch"; echo 'RESULT: inconclusive'; exit 2; fi
trap cleanup EXIT HUP INT TERM
mkdir -p "$scratch/home" "$scratch/tmp" "$scratch/xdg-config" "$scratch/xdg-data" "$scratch/xdg-cache" "$scratch/xdg-state" "$scratch/t3"
cp "$repo/apps/server/src/provider/Drivers/CodexDriver.test.ts" "$spec"
cat >> "$spec" <<'TS'
it.layer(layerTest)("CODEX_HOME shadow resolution reproduction", (it) => {
it.effect("control: direct and explicit shadow homes retain the configured state root", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const root = yield* fs.makeTempDirectoryScoped({ prefix: "codex-home-control-" });
const home = NodePath.join(root, "home");
const customHome = NodePath.join(root, "custom-codex");
yield* fs.makeDirectory(home, { recursive: true });
for (const [name, config] of [
["direct", CodexDriver.defaultConfig()],
["explicit-shadow", { ...CodexDriver.defaultConfig(), homePath: customHome, shadowHomePath: NodePath.join(root, "shadow") }],
] as const) {
const instance = yield* CodexDriver.create({
instanceId: ProviderInstanceId.make(name), displayName: name, enabled: false,
environment: [{ name: "CODEX_HOME", value: customHome, sensitive: false }], config,
}).pipe(Effect.provideService(HostProcess.HomeDirectory, home), Effect.provideService(HostProcess.Environment, {}));
expect(instance.continuationIdentity.continuationKey).toBe(`codex:home:${customHome}`);
}
console.log("CONTROL: direct env home and explicit shadow home select custom CODEX_HOME");
}).pipe(Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, noSpawn), Effect.scoped),
);
it.effect("defect: implicit shadow shared home retains CODEX_HOME from the instance environment", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const root = yield* fs.makeTempDirectoryScoped({ prefix: "codex-home-shadow-" });
const home = NodePath.join(root, "home");
const customHome = NodePath.join(root, "custom-codex");
const shadowHome = NodePath.join(root, "shadow");
yield* fs.makeDirectory(home, { recursive: true });
yield* fs.makeDirectory(customHome, { recursive: true });
yield* fs.writeFileString(NodePath.join(customHome, "config.toml"), 'model = "custom-marker"\n');
const instance = yield* CodexDriver.create({
instanceId: ProviderInstanceId.make("env-shadow"), displayName: "env-shadow", enabled: false,
environment: [{ name: "CODEX_HOME", value: customHome, sensitive: false }],
config: { ...CodexDriver.defaultConfig(), shadowHomePath: shadowHome },
}).pipe(Effect.provideService(HostProcess.HomeDirectory, home), Effect.provideService(HostProcess.Environment, {}));
console.log("OBSERVED: shadow sessions points to default home:", (yield* fs.readLink(NodePath.join(shadowHome, "sessions"))) === NodePath.join(home, ".codex", "sessions"));
console.log("OBSERVED: custom config visible in shadow:", yield* fs.exists(NodePath.join(shadowHome, "config.toml")));
expect(instance.continuationIdentity.continuationKey).toBe(`codex:home:${customHome}`);
expect(yield* fs.readFileString(NodePath.join(shadowHome, "config.toml"))).toBe('model = "custom-marker"\n');
}).pipe(Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, noSpawn), Effect.scoped),
);
});
TS
cd "$repo" || exit 2
env -i PATH="$PATH" NO_COLOR=1 FORCE_COLOR=0 HOME="$scratch/home" TMPDIR="$scratch/tmp" CODEX_HOME="$scratch/ambient-codex" XDG_CONFIG_HOME="$scratch/xdg-config" XDG_DATA_HOME="$scratch/xdg-data" XDG_CACHE_HOME="$scratch/xdg-cache" XDG_STATE_HOME="$scratch/xdg-state" T3CODE_HOME="$scratch/t3" node "$repo/node_modules/vite-plus/bin/vp" test run apps/server/src/provider/Drivers/CodexHomeEnvironment.repro.test.ts -t 'CODEX_HOME shadow resolution reproduction' > "$scratch/output" 2>&1
status=$?
cat "$scratch/output"
if [ "$status" -eq 0 ]; then echo 'RESULT: expected behavior'; exit 0; fi
if grep -q 'CONTROL: direct env home and explicit shadow home select custom CODEX_HOME' "$scratch/output" && grep -q 'OBSERVED: shadow sessions points to default home: true' "$scratch/output" && grep -q 'OBSERVED: custom config visible in shadow: false' "$scratch/output"; then
echo 'RESULT: defect reproduced'; exit 1
fi
echo 'RESULT: inconclusive'; exit 2
Expected behavior
A shadow home must share state from the instance's selected Codex home, whether selected through the home field or the instance environment. The setting itself says that the account-specific home keeps authentication separate while sharing state from CODEX_HOME (packages/contracts/src/settings.ts#L715-L736). The existing driver test establishes environment CODEX_HOME as the home-selection contract for blank configured homes (apps/server/src/provider/Drivers/CodexDriver.test.ts#L565-L582). Upstream Codex 0.166.0 and 0.167.2 resolve the environment value before considering the default directory (codex-rs/utils/home-dir/src/lib.rs#L5-L61); the state-location documentation describes the same convention.
Impact
Minor bug or occasional failure.
Impact: P2 - The affected manual-instance configuration selects the wrong shared sessions, continuation identity, and config directory; explicitly setting the shared-home field is a demonstrated practical workaround.
The reproduction directly verifies filesystem links, the missing custom config, and the continuation key. It makes no claim of data loss or an authenticated conversation failure. The condition is a nonempty shadow path combined with blank homePath and a nonempty instance environment CODEX_HOME; direct instances and explicit shared-home shadow instances pass the control. NOT_ENOUGH_INFO: effects on a real authenticated thread and other operating systems were not exercised; those require their respective runtime checks.
Version or commit
T3 Code commit 59e64eef881632a23f89c4138bcf37f23d92bf93. Installed Codex is codex-cli 0.167.2, matching the latest stable release checked during the investigation, but the reproduction does not launch it; the failure occurs inside T3 before a provider spawn.
Environment
macOS arm64; Node v26.12.1; repository-pinned Vite+ 1.4.0, Vitest 5.0.5, and Effect 4.1.3. NOT_ENOUGH_INFO: Windows and Linux were not exercised.
Interface
Library: production CodexDriver.create, the provider instance entry point used for manual Codex setup.
Earliest failure stage
Setup or configuration.
Logs or stack traces
The corrected script produced the following output. Temporary fixture paths are replaced with <scratch> and the assertion is summarized; durations, timestamps, skipped unrelated cases, and runner formatting are omitted.
CONTROL: direct env home and explicit shadow home select custom CODEX_HOME
OBSERVED: shadow sessions points to default home: true
OBSERVED: custom config visible in shadow: false
AssertionError: expected default-home continuation key to equal custom-home continuation key
Expected: codex:home:<scratch>/custom-codex
Received: codex:home:<scratch>/home/.codex
Tests: 1 failed | 1 passed | 17 skipped
RESULT: defect reproduced
Source evidence: the merged environment value is substituted only when both home fields are blank, so setting a shadow path suppresses that substitution (apps/server/src/provider/Drivers/CodexDriver.ts#L188-L198). The layout resolver then defaults a blank shared path to the user directory's .codex and builds the shadow continuation identity from it (apps/server/src/provider/Drivers/CodexHomeLayout.ts#L44-L82).
Expected outcome or recovery
The shadow case should expose the custom home's marker config, link sessions into that home, and retain codex:home:<scratch>/custom-codex. The direct and explicit-shadow controls should continue passing. Setting the instance's CODEX_HOME path field to the same custom directory is the verified workaround.
References
- fix(server): Codex switches respect each instance's CODEX_HOME variable #24664 — establishes environment-home selection for direct instances; its stated scope preserves shadow-home behavior, which is the distinct case reproduced here.
- refactor(server): one helper turns CODEX_HOME into a Codex home #29507 — centralizes the same both-fields-blank substitution and does not extend it to a configured shadow home.
- [Bug]: CODEX_HOME path override is ignored in some parts of the app #1158 — historical custom-home healthcheck mismatch; its discussion reports that fixed, while this report concerns shared-state selection during shadow materialization.
Activity