Skip to content

Commit 26e6c11

Browse files
craigamcwclaude
andcommitted
fix(sandbox): enrich PATH before Lima detection so packaged macOS app finds limactl
In the packaged macOS app, GUI processes inherit a stripped launchd PATH (/usr/bin:/bin:/usr/sbin:/sbin) that excludes /opt/homebrew/bin and /usr/local/bin, where Homebrew installs `limactl`. The sandbox/Lima detection (`LimaBridge.checkLimaStatus` -> `which limactl`) runs at startup and from the `sandbox.checkLima` IPC, but `enrichProcessPathForBuild()` only ran lazily before the first `createCodingTools()` call. As a result `which limactl` failed and the sandbox was reported as unavailable ("Lima not installed") even when Lima was installed via `brew install lima`. Export `enrichProcessPathForBuild()` and await it once at the top of the `app.whenReady()` handler, before the sandbox bootstrap and any IPC can run. The function is idempotent (guarded by `pathEnriched`), so the existing call before `createCodingTools()` becomes a no-op. Dev mode and the Windows registry-based PATH restore are unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 8e60460 commit 26e6c11

3 files changed

Lines changed: 60 additions & 2 deletions

File tree

src/main/claude/agent-runner.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -295,11 +295,14 @@ function resolveBundledToolsBinDir(): string | null {
295295
* 3. Deduplicates all entries
296296
* 4. Writes the result back to `process.env.PATH`
297297
*
298-
* Called once before the first `createCodingTools()` — subsequent calls are no-ops.
298+
* Idempotent: enriches once and caches the result, so it is safe to call from
299+
* multiple entry points. Call at app startup (before the sandbox bootstrap /
300+
* Lima detection) and before the first `createCodingTools()` — subsequent calls
301+
* are no-ops.
299302
*/
300303
let pathEnriched = false;
301304

302-
async function enrichProcessPathForBuild(): Promise<void> {
305+
export async function enrichProcessPathForBuild(): Promise<void> {
303306
if (pathEnriched) return;
304307
pathEnriched = true;
305308

src/main/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import { SandboxSync } from './sandbox/sandbox-sync';
4141
import { WSLBridge } from './sandbox/wsl-bridge';
4242
import { LimaBridge } from './sandbox/lima-bridge';
4343
import { getSandboxBootstrap } from './sandbox/sandbox-bootstrap';
44+
import { enrichProcessPathForBuild } from './claude/agent-runner';
4445
import type { MCPServerConfig } from './mcp/mcp-manager';
4546
import type {
4647
ClientEvent,
@@ -804,6 +805,14 @@ app
804805
process.exit(0);
805806
}
806807

808+
// Enrich PATH before anything else so Homebrew-installed CLIs (notably
809+
// `limactl`) are discoverable by the sandbox bootstrap and the
810+
// `sandbox.checkLima` IPC. In the packaged macOS app, GUI processes inherit
811+
// a stripped launchd PATH that excludes /opt/homebrew/bin and
812+
// /usr/local/bin, which otherwise makes `which limactl` fail and reports
813+
// the sandbox as unavailable even when Lima is installed.
814+
await enrichProcessPathForBuild();
815+
807816
// Apply dev logs setting from config
808817
const enableDevLogs = configStore.get('enableDevLogs');
809818
setDevLogsEnabled(enableDevLogs);
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { readFileSync } from 'node:fs';
3+
import path from 'node:path';
4+
5+
const read = (rel: string): string => readFileSync(path.resolve(process.cwd(), rel), 'utf8');
6+
7+
/**
8+
* Regression guard for the macOS sandbox-detection bug.
9+
*
10+
* In the packaged macOS app, GUI processes inherit a stripped launchd PATH that
11+
* excludes /opt/homebrew/bin and /usr/local/bin. The sandbox/Lima detection
12+
* (`which limactl`) runs at startup and from the `sandbox.checkLima` IPC, so it
13+
* must run AFTER the login-shell PATH has been restored — otherwise Lima is
14+
* reported as unavailable even when it is installed. The PATH enrichment used to
15+
* run only before the first `createCodingTools()` call (an agent task), which is
16+
* too late for the sandbox detection.
17+
*/
18+
describe('PATH enrichment is wired into app startup, before sandbox detection', () => {
19+
const agentRunner = read('src/main/claude/agent-runner.ts');
20+
const indexTs = read('src/main/index.ts');
21+
22+
it('exports enrichProcessPathForBuild so the startup path can reuse it', () => {
23+
expect(agentRunner).toMatch(/export\s+async\s+function\s+enrichProcessPathForBuild/);
24+
});
25+
26+
it('keeps the enrichment idempotent so repeated calls are safe', () => {
27+
expect(agentRunner).toContain('let pathEnriched = false;');
28+
expect(agentRunner).toContain('if (pathEnriched) return;');
29+
});
30+
31+
it('imports the enrichment into the main entrypoint', () => {
32+
expect(indexTs).toMatch(
33+
/import\s*\{\s*enrichProcessPathForBuild\s*\}\s*from\s*['"]\.\/claude\/agent-runner['"]/
34+
);
35+
});
36+
37+
it('awaits the enrichment during the whenReady startup sequence', () => {
38+
const readyIdx = indexTs.indexOf('.whenReady()');
39+
const enrichIdx = indexTs.indexOf('await enrichProcessPathForBuild()');
40+
expect(readyIdx).toBeGreaterThan(-1);
41+
expect(enrichIdx).toBeGreaterThan(-1);
42+
// The enrichment must be invoked from the startup sequence, not only from
43+
// the lazy agent-task path, so it precedes any Lima detection.
44+
expect(enrichIdx).toBeGreaterThan(readyIdx);
45+
});
46+
});

0 commit comments

Comments
 (0)