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", () => {...@@ -367,6 +367,27 @@ describe("DesktopShellEnvironment", () => {
367 }),367 }),
368 );368 );
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
370 it.effect("falls back to launchctl PATH on macOS when shell probing does not return one", () =>391 it.effect("falls back to launchctl PATH on macOS when shell probing does not return one", () =>
371 Effect.gen(function* () {392 Effect.gen(function* () {
372 const env: NodeJS.ProcessEnv = {393 const env: NodeJS.ProcessEnv = {
apps/desktop/src/shell/DesktopShellEnvironment.ts+5
...@@ -87,6 +87,7 @@ const LOGIN_SHELL_ENV_NAMES = [...@@ -87,6 +87,7 @@ const LOGIN_SHELL_ENV_NAMES = [
87 "XDG_SESSION_TYPE",87 "XDG_SESSION_TYPE",
88 "WAYLAND_DISPLAY",88 "WAYLAND_DISPLAY",
89 "T3CODE_TELEMETRY_ENABLED",89 "T3CODE_TELEMETRY_ENABLED",
90 "__MISE_DIFF",
90] as const;91] as const;
91const WINDOWS_PROFILE_ENV_NAMES = ["PATH", "FNM_DIR", "FNM_MULTISHELL_PATH"] as const;92const WINDOWS_PROFILE_ENV_NAMES = ["PATH", "FNM_DIR", "FNM_MULTISHELL_PATH"] as const;
92const LOCALE_ENV_NAMES = ["LANG", "LC_ALL", "LC_CTYPE"] as const;93const LOCALE_ENV_NAMES = ["LANG", "LC_ALL", "LC_CTYPE"] as const;
...@@ -433,6 +434,10 @@ const installPosixEnvironment = Effect.fn("desktop.shellEnvironment.installPosix...@@ -433,6 +434,10 @@ const installPosixEnvironment = Effect.fn("desktop.shellEnvironment.installPosix
433 "XDG_CURRENT_DESKTOP",434 "XDG_CURRENT_DESKTOP",
434 "XDG_SESSION_DESKTOP",435 "XDG_SESSION_DESKTOP",
435 "XDG_SESSION_TYPE",436 "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",
436 ] as const;441 ] as const;
437 for (const name of shellPreferredEnvNames) {442 for (const name of shellPreferredEnvNames) {
438 if (shellEnvironment[name]) {443 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", () =>...@@ -98,6 +98,38 @@ effectIt.effect("hydrates PATH when the desktop handoff is absent", () =>
98 ),98 ),
99);99);
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
101effectIt.effect("repairs Windows PATH even when the desktop marks it prepared", () =>133effectIt.effect("repairs Windows PATH even when the desktop marks it prepared", () =>
102 Effect.gen(function* () {134 Effect.gen(function* () {
103 const env: NodeJS.ProcessEnv = { PATH: "C:\\Windows" };135 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";...@@ -2,7 +2,7 @@ import * as HostProcess from "@t3tools/shared/HostProcess";
2import {2import {
3 listLoginShellCandidates,3 listLoginShellCandidates,
4 mergePathEntries,4 mergePathEntries,
5 readPathFromLoginShell,5 readEnvironmentFromLoginShell,
6 readPathFromLaunchctl,6 readPathFromLaunchctl,
7 resolveWindowsEnvironment,7 resolveWindowsEnvironment,
8} from "@t3tools/shared/shell";8} from "@t3tools/shared/shell";
...@@ -18,22 +18,29 @@ function logPathHydrationWarning(message: string, error?: unknown): void {...@@ -18,22 +18,29 @@ function logPathHydrationWarning(message: string, error?: unknown): void {
18}18}
1919
20function hydratePosixPath(env: NodeJS.ProcessEnv, platform: NodeJS.Platform): void {20function hydratePosixPath(env: NodeJS.ProcessEnv, platform: NodeJS.Platform): void {
21 let shellPath: string | undefined;21 let shellEnvironment: Partial<Record<string, string>> = {};
22 for (const shell of listLoginShellCandidates(platform, env.SHELL)) {22 for (const shell of listLoginShellCandidates(platform, env.SHELL)) {
23 try {23 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"]);
25 } catch (error) {28 } catch (error) {
26 logPathHydrationWarning(`Failed to read PATH from login shell ${shell}.`, error);29 logPathHydrationWarning(`Failed to read PATH from login shell ${shell}.`, error);
27 }30 }
2831
29 if (shellPath) break;32 if (shellEnvironment.PATH) break;
30 }33 }
3134
35 const shellPath = shellEnvironment.PATH;
32 const launchctlPath = platform === "darwin" && !shellPath ? readPathFromLaunchctl() : undefined;36 const launchctlPath = platform === "darwin" && !shellPath ? readPathFromLaunchctl() : undefined;
33 const mergedPath = mergePathEntries(shellPath ?? launchctlPath, env.PATH, platform);37 const mergedPath = mergePathEntries(shellPath ?? launchctlPath, env.PATH, platform);
34 if (mergedPath) {38 if (mergedPath) {
35 env.PATH = mergedPath;39 env.PATH = mergedPath;
36 }40 }
41 if (shellPath && shellEnvironment.__MISE_DIFF) {
42 env.__MISE_DIFF = shellEnvironment.__MISE_DIFF;
43 }
37}44}
3845
39export function hydratePosixHome(46export function hydratePosixHome(
packages/shared/src/shell.test.ts+4-3
...@@ -22,7 +22,6 @@ import {...@@ -22,7 +22,6 @@ import {
22 readEnvironmentFromLoginShell,22 readEnvironmentFromLoginShell,
23 readEnvironmentFromWindowsShell,23 readEnvironmentFromWindowsShell,
24 readPathFromLaunchctl,24 readPathFromLaunchctl,
25 readPathFromLoginShell,
26 resolveCommandPath,25 resolveCommandPath,
27 resolveKnownWindowsCliDirs,26 resolveKnownWindowsCliDirs,
28 resolveSpawnCommand,27 resolveSpawnCommand,
...@@ -57,7 +56,7 @@ const recordCommandTrace = Effect.fnUntraced(function* <A, E, R>(effect: Effect....@@ -57,7 +56,7 @@ const recordCommandTrace = Effect.fnUntraced(function* <A, E, R>(effect: Effect.
57 return { result, spans };56 return { result, spans };
58});57});
5958
60describe("readPathFromLoginShell", () => {59describe("readEnvironmentFromLoginShell probe", () => {
61 it("uses a shell-agnostic printenv PATH probe", () => {60 it("uses a shell-agnostic printenv PATH probe", () => {
62 const execFile = vi.fn<61 const execFile = vi.fn<
63 (62 (
...@@ -67,7 +66,9 @@ describe("readPathFromLoginShell", () => {...@@ -67,7 +66,9 @@ describe("readPathFromLoginShell", () => {
67 ) => string66 ) => string
68 >(() => "__T3CODE_ENV_PATH_START__\n/a:/b\n__T3CODE_ENV_PATH_END__\n");67 >(() => "__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 );
71 expect(execFile).toHaveBeenCalledTimes(1);72 expect(execFile).toHaveBeenCalledTimes(1);
7273
73 const firstCall = execFile.mock.calls[0] as74 const firstCall = execFile.mock.calls[0] as
packages/shared/src/shell.ts-7
...@@ -172,13 +172,6 @@ export function listLoginShellCandidates(...@@ -172,13 +172,6 @@ export function listLoginShellCandidates(
172 return candidates;172 return candidates;
173}173}
174174
175export function readPathFromLoginShell(
176 shell: string,
177 execFile: ExecFileSyncLike = NodeChildProcess.execFileSync,
178): string | undefined {
179 return readEnvironmentFromLoginShell(shell, ["PATH"], execFile).PATH;
180}
181
182export function readPathFromLaunchctl(175export function readPathFromLaunchctl(
183 execFile: ExecFileSyncLike = NodeChildProcess.execFileSync,176 execFile: ExecFileSyncLike = NodeChildProcess.execFileSync,
184): string | undefined {177): string | undefined {