Skip to content

[Bug]: Codex shadow home ignores the instance environment CODEX_HOME #35856

Closed

Description

@coygeek

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

  1. Use a clean checkout of the commit below with its lockfile dependencies installed (vp install --frozen-lockfile --ignore-scripts was used here).
  2. 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.
  3. 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions