authorgravatar for 4023423+altjx@users.noreply.github.comAlton Johnson, OSCP, OSCE <4023423+altjx@users.noreply.github.com> 2026-04-19 20:06:19-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-04-20 00:06:19+00:00
log20f346d8ef58b21c339bc956c1728f4f16d09a87
treea2b19b9e72680d2c76f24503650fd2e2fd0d630a
parentc83bc5d48a2bf983acb1c8aaff3d34f86c14032e
signature Signed by PGP key B5690EEEBB952194

Expand leading ~ in Codex home paths before exporting CODEX_HOME (#2210)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: = <=>

6 files changed, 67 insertions(+), 5 deletions(-)

apps/server/src/codexAppServerManager.ts+3-2
...@@ -32,6 +32,7 @@ import {...@@ -32,6 +32,7 @@ import {
32 type CodexAccountSnapshot,32 type CodexAccountSnapshot,
33} from "./provider/codexAccount.ts";33} from "./provider/codexAccount.ts";
34import { buildCodexInitializeParams, killCodexChildProcess } from "./provider/codexAppServer.ts";34import { buildCodexInitializeParams, killCodexChildProcess } from "./provider/codexAppServer.ts";
35import { expandHomePath } from "./pathExpansion.ts";
3536
36export { buildCodexInitializeParams } from "./provider/codexAppServer.ts";37export { buildCodexInitializeParams } from "./provider/codexAppServer.ts";
37export { readCodexAccountSnapshot, resolveCodexModelForAccount } from "./provider/codexAccount.ts";38export { readCodexAccountSnapshot, resolveCodexModelForAccount } from "./provider/codexAccount.ts";
...@@ -493,7 +494,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve...@@ -493,7 +494,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve
493 cwd: resolvedCwd,494 cwd: resolvedCwd,
494 env: {495 env: {
495 ...process.env,496 ...process.env,
496 ...(codexHomePath ? { CODEX_HOME: codexHomePath } : {}),497 ...(codexHomePath ? { CODEX_HOME: expandHomePath(codexHomePath) } : {}),
497 },498 },
498 stdio: ["pipe", "pipe", "pipe"],499 stdio: ["pipe", "pipe", "pipe"],
499 shell: process.platform === "win32",500 shell: process.platform === "win32",
...@@ -1571,7 +1572,7 @@ function assertSupportedCodexCliVersion(input: {...@@ -1571,7 +1572,7 @@ function assertSupportedCodexCliVersion(input: {
1571 cwd: input.cwd,1572 cwd: input.cwd,
1572 env: {1573 env: {
1573 ...process.env,1574 ...process.env,
1574 ...(input.homePath ? { CODEX_HOME: input.homePath } : {}),1575 ...(input.homePath ? { CODEX_HOME: expandHomePath(input.homePath) } : {}),
1575 },1576 },
1576 encoding: "utf8",1577 encoding: "utf8",
1577 shell: process.platform === "win32",1578 shell: process.platform === "win32",
apps/server/src/git/Layers/CodexTextGeneration.ts+4-1
...@@ -8,6 +8,7 @@ import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/shar...@@ -8,6 +8,7 @@ import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/shar
88
9import { resolveAttachmentPath } from "../../attachmentStore.ts";9import { resolveAttachmentPath } from "../../attachmentStore.ts";
10import { ServerConfig } from "../../config.ts";10import { ServerConfig } from "../../config.ts";
11import { expandHomePath } from "../../pathExpansion.ts";
11import { TextGenerationError } from "@t3tools/contracts";12import { TextGenerationError } from "@t3tools/contracts";
12import {13import {
13 type BranchNameGenerationInput,14 type BranchNameGenerationInput,
...@@ -184,7 +185,9 @@ const makeCodexTextGeneration = Effect.gen(function* () {...@@ -184,7 +185,9 @@ const makeCodexTextGeneration = Effect.gen(function* () {
184 {185 {
185 env: {186 env: {
186 ...process.env,187 ...process.env,
187 ...(codexSettings?.homePath ? { CODEX_HOME: codexSettings.homePath } : {}),188 ...(codexSettings?.homePath
189 ? { CODEX_HOME: expandHomePath(codexSettings.homePath) }
190 : {}),
188 },191 },
189 cwd,192 cwd,
190 shell: process.platform === "win32",193 shell: process.platform === "win32",
apps/server/src/pathExpansion.test.ts created+33
...@@ -0,0 +1,33 @@
1import { homedir } from "node:os";
2import { join } from "node:path";
3import { describe, expect, it } from "vitest";
4
5import { expandHomePath } from "./pathExpansion.ts";
6
7describe("expandHomePath", () => {
8 it("returns an empty string unchanged", () => {
9 expect(expandHomePath("")).toBe("");
10 });
11
12 it("returns paths without a leading tilde unchanged", () => {
13 expect(expandHomePath("/absolute/path")).toBe("/absolute/path");
14 expect(expandHomePath("relative/path")).toBe("relative/path");
15 expect(expandHomePath("some~weird~path")).toBe("some~weird~path");
16 });
17
18 it("expands a lone tilde to the home directory", () => {
19 expect(expandHomePath("~")).toBe(homedir());
20 });
21
22 it("expands ~/ to a subpath of the home directory", () => {
23 expect(expandHomePath("~/.codex-work")).toBe(join(homedir(), ".codex-work"));
24 });
25
26 it("expands a Windows-style ~\\ prefix", () => {
27 expect(expandHomePath("~\\.codex")).toBe(join(homedir(), ".codex"));
28 });
29
30 it("does not expand ~user paths", () => {
31 expect(expandHomePath("~alice/foo")).toBe("~alice/foo");
32 });
33});
apps/server/src/pathExpansion.ts created+23
...@@ -0,0 +1,23 @@
1import { homedir } from "node:os";
2import { join } from "node:path";
3
4/**
5 * Expand a leading `~` (or `~/…`, `~\…`) in a user-supplied path to the
6 * current user's home directory. Spawned processes don't get shell
7 * expansion, so env vars like `CODEX_HOME=~/.codex-work` would be passed
8 * verbatim and treated as relative paths by the receiver.
9 *
10 * Matches the behavior of the other `expandHomePath` helpers in the
11 * workspace layers and CLI bootstrap: `~` alone and both `~/` and `~\`
12 * separators are handled. Returns the input unchanged if it doesn't
13 * start with `~` or is empty. Does not handle `~user` (other-user)
14 * expansion.
15 */
16export function expandHomePath(value: string): string {
17 if (!value) return value;
18 if (value === "~") return homedir();
19 if (value.startsWith("~/") || value.startsWith("~\\")) {
20 return join(homedir(), value.slice(2));
21 }
22 return value;
23}
apps/server/src/provider/Layers/CodexProvider.ts+2-1
...@@ -47,6 +47,7 @@ import {...@@ -47,6 +47,7 @@ import {
47} from "../codexAccount.ts";47} from "../codexAccount.ts";
48import { probeCodexDiscovery } from "../codexAppServer.ts";48import { probeCodexDiscovery } from "../codexAppServer.ts";
49import { CodexProvider } from "../Services/CodexProvider.ts";49import { CodexProvider } from "../Services/CodexProvider.ts";
50import { expandHomePath } from "../../pathExpansion.ts";
50import { ServerSettingsService } from "../../serverSettings.ts";51import { ServerSettingsService } from "../../serverSettings.ts";
51import { ServerSettingsError } from "@t3tools/contracts";52import { ServerSettingsError } from "@t3tools/contracts";
5253
...@@ -325,7 +326,7 @@ const runCodexCommand = Effect.fn("runCodexCommand")(function* (args: ReadonlyAr...@@ -325,7 +326,7 @@ const runCodexCommand = Effect.fn("runCodexCommand")(function* (args: ReadonlyAr
325 shell: process.platform === "win32",326 shell: process.platform === "win32",
326 env: {327 env: {
327 ...process.env,328 ...process.env,
328 ...(codexSettings.homePath ? { CODEX_HOME: codexSettings.homePath } : {}),329 ...(codexSettings.homePath ? { CODEX_HOME: expandHomePath(codexSettings.homePath) } : {}),
329 },330 },
330 });331 });
331 return yield* spawnAndCollect(codexSettings.binaryPath, command);332 return yield* spawnAndCollect(codexSettings.binaryPath, command);
apps/server/src/provider/codexAppServer.ts+2-1
...@@ -2,6 +2,7 @@ import { spawn, spawnSync, type ChildProcessWithoutNullStreams } from "node:chil...@@ -2,6 +2,7 @@ import { spawn, spawnSync, type ChildProcessWithoutNullStreams } from "node:chil
2import readline from "node:readline";2import readline from "node:readline";
3import type { ServerProviderSkill } from "@t3tools/contracts";3import type { ServerProviderSkill } from "@t3tools/contracts";
4import { readCodexAccountSnapshot, type CodexAccountSnapshot } from "./codexAccount.ts";4import { readCodexAccountSnapshot, type CodexAccountSnapshot } from "./codexAccount.ts";
5import { expandHomePath } from "../pathExpansion.ts";
56
6interface JsonRpcProbeResponse {7interface JsonRpcProbeResponse {
7 readonly id?: unknown;8 readonly id?: unknown;
...@@ -115,7 +116,7 @@ export async function probeCodexDiscovery(input: {...@@ -115,7 +116,7 @@ export async function probeCodexDiscovery(input: {
115 const child = spawn(input.binaryPath, ["app-server"], {116 const child = spawn(input.binaryPath, ["app-server"], {
116 env: {117 env: {
117 ...process.env,118 ...process.env,
118 ...(input.homePath ? { CODEX_HOME: input.homePath } : {}),119 ...(input.homePath ? { CODEX_HOME: expandHomePath(input.homePath) } : {}),
119 },120 },
120 stdio: ["pipe", "pipe", "pipe"],121 stdio: ["pipe", "pipe", "pipe"],
121 shell: process.platform === "win32",122 shell: process.platform === "win32",