Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions src/main/claude/agent-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,11 +145,14 @@ function resolveBundledToolsBinDir(): string | null {
* 3. Deduplicates all entries
* 4. Writes the result back to `process.env.PATH`
*
* Called once before the first `createCodingTools()` — subsequent calls are no-ops.
* Idempotent: enriches once and caches the result, so it is safe to call from
* multiple entry points. Call at app startup (before the sandbox bootstrap /
* Lima detection) and before the first `createCodingTools()` — subsequent calls
* are no-ops.
*/
let pathEnriched = false;

async function enrichProcessPathForBuild(): Promise<void> {
export async function enrichProcessPathForBuild(): Promise<void> {
if (pathEnriched) return;
pathEnriched = true;

Expand Down
9 changes: 9 additions & 0 deletions src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { SandboxSync } from './sandbox/sandbox-sync';
import { WSLBridge } from './sandbox/wsl-bridge';
import { LimaBridge } from './sandbox/lima-bridge';
import { getSandboxBootstrap } from './sandbox/sandbox-bootstrap';
import { enrichProcessPathForBuild } from './claude/agent-runner';
import type { MCPServerConfig } from './mcp/mcp-manager';
import type {
ClientEvent,
Expand Down Expand Up @@ -759,6 +760,14 @@ function sendToRenderer(event: ServerEvent) {
app
.whenReady()
.then(async () => {
// Enrich PATH before anything else so Homebrew-installed CLIs (notably
// `limactl`) are discoverable by the sandbox bootstrap and the
// `sandbox.checkLima` IPC. In the packaged macOS app, GUI processes inherit
// a stripped launchd PATH that excludes /opt/homebrew/bin and
// /usr/local/bin, which otherwise makes `which limactl` fail and reports
// the sandbox as unavailable even when Lima is installed.
await enrichProcessPathForBuild();

// Apply dev logs setting from config
const enableDevLogs = configStore.get('enableDevLogs');
setDevLogsEnabled(enableDevLogs);
Expand Down
46 changes: 46 additions & 0 deletions tests/startup-path-enrichment.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { describe, expect, it } from 'vitest';
import { readFileSync } from 'node:fs';
import path from 'node:path';

const read = (rel: string): string => readFileSync(path.resolve(process.cwd(), rel), 'utf8');

/**
* Regression guard for the macOS sandbox-detection bug.
*
* In the packaged macOS app, GUI processes inherit a stripped launchd PATH that
* excludes /opt/homebrew/bin and /usr/local/bin. The sandbox/Lima detection
* (`which limactl`) runs at startup and from the `sandbox.checkLima` IPC, so it
* must run AFTER the login-shell PATH has been restored — otherwise Lima is
* reported as unavailable even when it is installed. The PATH enrichment used to
* run only before the first `createCodingTools()` call (an agent task), which is
* too late for the sandbox detection.
*/
describe('PATH enrichment is wired into app startup, before sandbox detection', () => {
const agentRunner = read('src/main/claude/agent-runner.ts');
const indexTs = read('src/main/index.ts');

it('exports enrichProcessPathForBuild so the startup path can reuse it', () => {
expect(agentRunner).toMatch(/export\s+async\s+function\s+enrichProcessPathForBuild/);
});

it('keeps the enrichment idempotent so repeated calls are safe', () => {
expect(agentRunner).toContain('let pathEnriched = false;');
expect(agentRunner).toContain('if (pathEnriched) return;');
});

it('imports the enrichment into the main entrypoint', () => {
expect(indexTs).toMatch(
/import\s*\{\s*enrichProcessPathForBuild\s*\}\s*from\s*['"]\.\/claude\/agent-runner['"]/
);
});

it('awaits the enrichment during the whenReady startup sequence', () => {
const readyIdx = indexTs.indexOf('.whenReady()');
const enrichIdx = indexTs.indexOf('await enrichProcessPathForBuild()');
expect(readyIdx).toBeGreaterThan(-1);
expect(enrichIdx).toBeGreaterThan(-1);
// The enrichment must be invoked from the startup sequence, not only from
// the lazy agent-task path, so it precedes any Lima detection.
expect(enrichIdx).toBeGreaterThan(readyIdx);
});
});
Loading