authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-10-14 02:25:39-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-14 02:25:39-07:00
log7ee2839cb58263742301895a6be826505daa64ea
tree62c09653213bfc76a9440d42451fbe93a9b8811e
parent9a01524f3faca628699cdf65b43ad020b40b86d8
signature Signed by PGP key B5690EEEBB952194

fix: Claude and Codex no longer time out behind mise wrappers (#19340)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

6 files changed, 73 insertions(+), 14 deletions(-)

apps/desktop/src/shell/DesktopShellEnvironment.test.ts+21
......@@ -367,6 +367,27 @@ describe("DesktopShellEnvironment", () => {
367367 }),
368368 );
369369
370 it.effect("keeps mise's PATH record with the login shell PATH", () =>
371 Effect.gen(function* () {
372 const env: NodeJS.ProcessEnv = {
373 SHELL: "/bin/bash",
374 PATH: "/usr/bin",
375 };
376
377 yield* runShellEnvironment({
378 env,
379 platform: "linux",
380 handler: () =>
381 envOutput({
382 PATH: "/home/me/.local/bin:/mise/installs/claude/latest:/mise/shims",
383 __MISE_DIFF: "mise-diff",
384 }),
385 });
386
387 assert.equal(env.__MISE_DIFF, "mise-diff");
388 }),
389 );
390
370391 it.effect("falls back to launchctl PATH on macOS when shell probing does not return one", () =>
371392 Effect.gen(function* () {
372393 const env: NodeJS.ProcessEnv = {
apps/desktop/src/shell/DesktopShellEnvironment.ts+5
......@@ -87,6 +87,7 @@ const LOGIN_SHELL_ENV_NAMES = [
8787 "XDG_SESSION_TYPE",
8888 "WAYLAND_DISPLAY",
8989 "T3CODE_TELEMETRY_ENABLED",
90 "__MISE_DIFF",
9091] as const;
9192const WINDOWS_PROFILE_ENV_NAMES = ["PATH", "FNM_DIR", "FNM_MULTISHELL_PATH"] as const;
9293const LOCALE_ENV_NAMES = ["LANG", "LC_ALL", "LC_CTYPE"] as const;
......@@ -433,6 +434,10 @@ const installPosixEnvironment = Effect.fn("desktop.shellEnvironment.installPosix
433434 "XDG_CURRENT_DESKTOP",
434435 "XDG_SESSION_DESKTOP",
435436 "XDG_SESSION_TYPE",
437 // mise's record of the PATH entries it added. It must travel with the
438 // shell's PATH, or `mise x` can rebuild PATH behind a same-named wrapper
439 // (`exec mise x claude -- claude`) that then re-runs itself forever.
440 "__MISE_DIFF",
436441 ] as const;
437442 for (const name of shellPreferredEnvNames) {
438443 if (shellEnvironment[name]) {
apps/server/src/os-jank.test.ts+32
......@@ -98,6 +98,38 @@ effectIt.effect("hydrates PATH when the desktop handoff is absent", () =>
9898 ),
9999);
100100
101effectIt.effect("keeps mise's PATH record with the login shell PATH", () =>
102 Effect.acquireUseRelease(
103 Effect.sync(() =>
104 vi
105 .spyOn(NodeChildProcess, "execFileSync")
106 .mockReturnValue(
107 [
108 "__T3CODE_ENV_PATH_START__",
109 "/home/me/.local/bin:/mise/installs/claude/latest:/mise/shims",
110 "__T3CODE_ENV_PATH_END__",
111 "__T3CODE_ENV___MISE_DIFF_START__",
112 "mise-diff",
113 "__T3CODE_ENV___MISE_DIFF_END__",
114 ].join("\n"),
115 ),
116 ),
117 () =>
118 Effect.gen(function* () {
119 const env: NodeJS.ProcessEnv = { SHELL: "/bin/bash", PATH: "/usr/bin" };
120
121 yield* fixPath().pipe(
122 Effect.provideService(HostProcess.Platform, "linux"),
123 Effect.provideService(HostProcess.Environment, env),
124 Effect.provide(NodeServices.layer),
125 );
126
127 assert.equal(env.__MISE_DIFF, "mise-diff");
128 }),
129 (probe) => Effect.sync(() => probe.mockRestore()),
130 ),
131);
132
101133effectIt.effect("repairs Windows PATH even when the desktop marks it prepared", () =>
102134 Effect.gen(function* () {
103135 const env: NodeJS.ProcessEnv = { PATH: "C:\\Windows" };
apps/server/src/os-jank.ts+11-4
......@@ -2,7 +2,7 @@ import * as HostProcess from "@t3tools/shared/HostProcess";
22import {
33 listLoginShellCandidates,
44 mergePathEntries,
5 readPathFromLoginShell,
5 readEnvironmentFromLoginShell,
66 readPathFromLaunchctl,
77 resolveWindowsEnvironment,
88} from "@t3tools/shared/shell";
......@@ -18,22 +18,29 @@ function logPathHydrationWarning(message: string, error?: unknown): void {
1818}
1919
2020function hydratePosixPath(env: NodeJS.ProcessEnv, platform: NodeJS.Platform): void {
21 let shellPath: string | undefined;
21 let shellEnvironment: Partial<Record<string, string>> = {};
2222 for (const shell of listLoginShellCandidates(platform, env.SHELL)) {
2323 try {
24 shellPath = readPathFromLoginShell(shell);
24 // mise's record of the PATH entries it added. It must travel with the
25 // shell's PATH, or `mise x` can rebuild PATH behind a same-named wrapper
26 // (`exec mise x claude -- claude`) that then re-runs itself forever.
27 shellEnvironment = readEnvironmentFromLoginShell(shell, ["PATH", "__MISE_DIFF"]);
2528 } catch (error) {
2629 logPathHydrationWarning(`Failed to read PATH from login shell ${shell}.`, error);
2730 }
2831
29 if (shellPath) break;
32 if (shellEnvironment.PATH) break;
3033 }
3134
35 const shellPath = shellEnvironment.PATH;
3236 const launchctlPath = platform === "darwin" && !shellPath ? readPathFromLaunchctl() : undefined;
3337 const mergedPath = mergePathEntries(shellPath ?? launchctlPath, env.PATH, platform);
3438 if (mergedPath) {
3539 env.PATH = mergedPath;
3640 }
41 if (shellPath && shellEnvironment.__MISE_DIFF) {
42 env.__MISE_DIFF = shellEnvironment.__MISE_DIFF;
43 }
3744}
3845
3946export function hydratePosixHome(
packages/shared/src/shell.test.ts+4-3
......@@ -22,7 +22,6 @@ import {
2222 readEnvironmentFromLoginShell,
2323 readEnvironmentFromWindowsShell,
2424 readPathFromLaunchctl,
25 readPathFromLoginShell,
2625 resolveCommandPath,
2726 resolveKnownWindowsCliDirs,
2827 resolveSpawnCommand,
......@@ -57,7 +56,7 @@ const recordCommandTrace = Effect.fnUntraced(function* <A, E, R>(effect: Effect.
5756 return { result, spans };
5857});
5958
60describe("readPathFromLoginShell", () => {
59describe("readEnvironmentFromLoginShell probe", () => {
6160 it("uses a shell-agnostic printenv PATH probe", () => {
6261 const execFile = vi.fn<
6362 (
......@@ -67,7 +66,9 @@ describe("readPathFromLoginShell", () => {
6766 ) => string
6867 >(() => "__T3CODE_ENV_PATH_START__\n/a:/b\n__T3CODE_ENV_PATH_END__\n");
6968
70 expect(readPathFromLoginShell("/opt/homebrew/bin/fish", execFile)).toBe("/a:/b");
69 expect(readEnvironmentFromLoginShell("/opt/homebrew/bin/fish", ["PATH"], execFile).PATH).toBe(
70 "/a:/b",
71 );
7172 expect(execFile).toHaveBeenCalledTimes(1);
7273
7374 const firstCall = execFile.mock.calls[0] as
packages/shared/src/shell.ts-7
......@@ -172,13 +172,6 @@ export function listLoginShellCandidates(
172172 return candidates;
173173}
174174
175export function readPathFromLoginShell(
176 shell: string,
177 execFile: ExecFileSyncLike = NodeChildProcess.execFileSync,
178): string | undefined {
179 return readEnvironmentFromLoginShell(shell, ["PATH"], execFile).PATH;
180}
181
182175export function readPathFromLaunchctl(
183176 execFile: ExecFileSyncLike = NodeChildProcess.execFileSync,
184177): string | undefined {