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 {
3232 type CodexAccountSnapshot,
3333} from "./provider/codexAccount.ts";
3434import { buildCodexInitializeParams, killCodexChildProcess } from "./provider/codexAppServer.ts";
35import { expandHomePath } from "./pathExpansion.ts";
3536
3637export { buildCodexInitializeParams } from "./provider/codexAppServer.ts";
3738export { readCodexAccountSnapshot, resolveCodexModelForAccount } from "./provider/codexAccount.ts";
......@@ -493,7 +494,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve
493494 cwd: resolvedCwd,
494495 env: {
495496 ...process.env,
496 ...(codexHomePath ? { CODEX_HOME: codexHomePath } : {}),
497 ...(codexHomePath ? { CODEX_HOME: expandHomePath(codexHomePath) } : {}),
497498 },
498499 stdio: ["pipe", "pipe", "pipe"],
499500 shell: process.platform === "win32",
......@@ -1571,7 +1572,7 @@ function assertSupportedCodexCliVersion(input: {
15711572 cwd: input.cwd,
15721573 env: {
15731574 ...process.env,
1574 ...(input.homePath ? { CODEX_HOME: input.homePath } : {}),
1575 ...(input.homePath ? { CODEX_HOME: expandHomePath(input.homePath) } : {}),
15751576 },
15761577 encoding: "utf8",
15771578 shell: process.platform === "win32",
apps/server/src/git/Layers/CodexTextGeneration.ts+4-1
......@@ -8,6 +8,7 @@ import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/shar
88
99import { resolveAttachmentPath } from "../../attachmentStore.ts";
1010import { ServerConfig } from "../../config.ts";
11import { expandHomePath } from "../../pathExpansion.ts";
1112import { TextGenerationError } from "@t3tools/contracts";
1213import {
1314 type BranchNameGenerationInput,
......@@ -184,7 +185,9 @@ const makeCodexTextGeneration = Effect.gen(function* () {
184185 {
185186 env: {
186187 ...process.env,
187 ...(codexSettings?.homePath ? { CODEX_HOME: codexSettings.homePath } : {}),
188 ...(codexSettings?.homePath
189 ? { CODEX_HOME: expandHomePath(codexSettings.homePath) }
190 : {}),
188191 },
189192 cwd,
190193 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 {
4747} from "../codexAccount.ts";
4848import { probeCodexDiscovery } from "../codexAppServer.ts";
4949import { CodexProvider } from "../Services/CodexProvider.ts";
50import { expandHomePath } from "../../pathExpansion.ts";
5051import { ServerSettingsService } from "../../serverSettings.ts";
5152import { ServerSettingsError } from "@t3tools/contracts";
5253
......@@ -325,7 +326,7 @@ const runCodexCommand = Effect.fn("runCodexCommand")(function* (args: ReadonlyAr
325326 shell: process.platform === "win32",
326327 env: {
327328 ...process.env,
328 ...(codexSettings.homePath ? { CODEX_HOME: codexSettings.homePath } : {}),
329 ...(codexSettings.homePath ? { CODEX_HOME: expandHomePath(codexSettings.homePath) } : {}),
329330 },
330331 });
331332 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
22import readline from "node:readline";
33import type { ServerProviderSkill } from "@t3tools/contracts";
44import { readCodexAccountSnapshot, type CodexAccountSnapshot } from "./codexAccount.ts";
5import { expandHomePath } from "../pathExpansion.ts";
56
67interface JsonRpcProbeResponse {
78 readonly id?: unknown;
......@@ -115,7 +116,7 @@ export async function probeCodexDiscovery(input: {
115116 const child = spawn(input.binaryPath, ["app-server"], {
116117 env: {
117118 ...process.env,
118 ...(input.homePath ? { CODEX_HOME: input.homePath } : {}),
119 ...(input.homePath ? { CODEX_HOME: expandHomePath(input.homePath) } : {}),
119120 },
120121 stdio: ["pipe", "pipe", "pipe"],
121122 shell: process.platform === "win32",