Skip to content

Commit 852b86e

Browse files
suj1eclaude
andcommitted
fix: cross-platform claude binary detection for Windows
- Add `claude.path` config field for manual override - `findClaudeBinary()`: use `where` on Windows, `which` on POSIX - `ensureEnv()`: skip bash PATH injection on Windows - `ensureClaudeInPath()`: Windows common paths (APPDATA/npm), PATH separator (`;` vs `:`), `where` command, `.cmd` extension Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 6f5b98e commit 852b86e

3 files changed

Lines changed: 44 additions & 16 deletions

File tree

src/agent.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,15 @@ const TOOL_LABELS: Record<string, string> = {
3434

3535
const SILENT_TOOLS = new Set(["ExitPlanMode", "TodoWrite", "TodoRead"]);
3636

37+
function findClaudeBinary(): string | undefined {
38+
const cmd = process.platform === "win32" ? "where claude 2>nul" : "which claude 2>/dev/null || command -v claude 2>/dev/null";
39+
try {
40+
const result = execSync(cmd, { encoding: "utf8", timeout: 5000 }).trim();
41+
if (result) return result.split(/[\r\n]/)[0];
42+
} catch {}
43+
return undefined;
44+
}
45+
3746
function formatInput(name: string, input: Record<string, unknown>): string {
3847
if (["Read", "Write", "Edit"].includes(name))
3948
return String(input.file_path ?? input.path ?? "");
@@ -210,7 +219,7 @@ export async function runAgent(
210219
thinking: config.claude.thinking,
211220
abortController,
212221
agentProgressSummaries: true,
213-
pathToClaudeCodeExecutable: execSync("which claude 2>/dev/null || echo ''", { encoding: "utf8" }).trim() || undefined,
222+
pathToClaudeCodeExecutable: config.claude.path || findClaudeBinary(),
214223
...(systemPrompt ? { systemPrompt: { type: 'preset' as const, preset: 'claude_code' as const, append: systemPrompt } } : {}),
215224
},
216225
})) {

src/app.ts

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -208,36 +208,53 @@ function ensureClaudeOnboarding(): void {
208208
} catch (err) { logger.warn(`Failed to write ~/.claude.json: ${String(err)}`); }
209209
}
210210

211+
const IS_WIN = process.platform === "win32";
212+
const PATH_SEP = IS_WIN ? ";" : ":";
213+
211214
function ensureEnv(): void {
212215
if (!process.env.HOME) process.env.HOME = os.homedir();
216+
if (IS_WIN) return;
213217
try {
214218
const shellPath = execSync("bash -lc 'echo $PATH' 2>/dev/null", { timeout: 3000 }).toString().trim();
215219
if (shellPath) process.env.PATH = shellPath;
216220
} catch {}
217221
}
218222

219223
function ensureClaudeInPath(): void {
220-
const commonPaths = [
221-
"/usr/local/bin", "/usr/bin",
222-
`${os.homedir()}/.npm-global/bin`, `${os.homedir()}/.local/bin`,
223-
"/opt/homebrew/bin", "/home/linuxbrew/.linuxbrew/bin",
224-
];
224+
const commonPaths = IS_WIN
225+
? [
226+
path.join(process.env.APPDATA ?? "", "npm"),
227+
path.join(process.env.LOCALAPPDATA ?? "", "Programs", "claude"),
228+
]
229+
: [
230+
"/usr/local/bin", "/usr/bin",
231+
`${os.homedir()}/.npm-global/bin`, `${os.homedir()}/.local/bin`,
232+
"/opt/homebrew/bin", "/home/linuxbrew/.linuxbrew/bin",
233+
];
234+
235+
const findCmd = IS_WIN ? "where claude 2>nul" : "which claude 2>/dev/null || command -v claude 2>/dev/null";
236+
const shellOpt = IS_WIN ? {} : { shell: "/bin/bash" };
237+
225238
try {
226-
const claudePath = execSync("which claude 2>/dev/null || command -v claude 2>/dev/null", {
227-
shell: "/bin/bash",
228-
env: { ...process.env, PATH: [...commonPaths, process.env.PATH ?? ""].join(":") },
229-
}).toString().trim();
239+
const claudePath = execSync(findCmd, {
240+
...shellOpt,
241+
encoding: "utf8",
242+
timeout: 5000,
243+
env: { ...process.env, PATH: [...commonPaths, process.env.PATH ?? ""].join(PATH_SEP) },
244+
}).trim();
230245
if (claudePath) {
231-
const dir = path.dirname(claudePath);
232-
if (!process.env.PATH?.includes(dir)) process.env.PATH = `${dir}:${process.env.PATH}`;
233-
logger.dim(`claude found: ${claudePath}`);
246+
const resolved = claudePath.split(/[\r\n]/)[0];
247+
const dir = path.dirname(resolved);
248+
if (!process.env.PATH?.includes(dir)) process.env.PATH = `${dir}${PATH_SEP}${process.env.PATH}`;
249+
logger.dim(`claude found: ${resolved}`);
234250
return;
235251
}
236252
} catch {}
237253
for (const dir of commonPaths) {
238-
if (fs.existsSync(path.join(dir, "claude"))) {
239-
if (!process.env.PATH?.includes(dir)) process.env.PATH = `${dir}:${process.env.PATH}`;
240-
logger.dim(`claude found: ${dir}/claude`);
254+
const binName = IS_WIN ? "claude.cmd" : "claude";
255+
if (fs.existsSync(path.join(dir, binName)) || fs.existsSync(path.join(dir, "claude"))) {
256+
if (!process.env.PATH?.includes(dir)) process.env.PATH = `${dir}${PATH_SEP}${process.env.PATH}`;
257+
logger.dim(`claude found: ${dir}`);
241258
return;
242259
}
243260
}

src/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ export interface ProfileConfig {
7777
permission_mode?: "acceptEdits" | "auto" | "default";
7878
allowed_tools?: string[];
7979
thinking?: { type: "adaptive" } | { type: "enabled"; budgetTokens: number } | { type: "disabled" };
80+
path?: string;
8081
};
8182
overflow?: OverflowConfig;
8283
format_guide?: FormatGuideConfig; // 格式指导配置
@@ -256,6 +257,7 @@ export function loadConfig(cwd: string, profile?: string): LarkccConfig {
256257
permission_mode: claude.permission_mode ?? "acceptEdits",
257258
allowed_tools: claude.allowed_tools ?? DEFAULT_TOOLS,
258259
thinking: claude.thinking ?? { type: "adaptive" },
260+
path: claude.path,
259261
},
260262
overflow: {
261263
mode: overflow.mode ?? DEFAULT_OVERFLOW.mode,

0 commit comments

Comments
 (0)