fix(worker): resolve claude CLI shims (.cmd/.bat) not just .exe on PATH

UseShellExecute=false only appends .exe when searching PATH, so an
npm-installed claude.cmd was never found even though it works from a shell.
Adds a shared ExecutableResolver in ClaudeDo.Data (PATH/PATHEXT aware, with
known npm/claude install-dir fallbacks) and wires it into ClaudeCliPreflight
and ClaudeProcess; shims are launched via cmd.exe /c.
This commit is contained in:
mika kuns
2026-08-05 19:08:20 +02:00
parent bdee731376
commit e88f9d01e6
5 changed files with 290 additions and 13 deletions
@@ -1,4 +1,5 @@
using System.Diagnostics;
using ClaudeDo.Data.Environment;
namespace ClaudeDo.Worker.Lifecycle;
@@ -8,17 +9,23 @@ public static class ClaudeCliPreflight
public static async Task<Result> CheckAsync(string claudeBin, CancellationToken ct = default)
{
var resolved = ExecutableResolver.Resolve(claudeBin);
if (resolved is null)
return new Result(false, "", $"'{claudeBin}' not found on PATH.", -1);
try
{
var psi = new ProcessStartInfo
{
FileName = claudeBin,
Arguments = "--version",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
};
var psi = resolved.IsShim
? BuildShimPsi(resolved.Path)
: new ProcessStartInfo
{
FileName = resolved.Path,
Arguments = "--version",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
};
using var proc = Process.Start(psi);
if (proc is null) return new Result(false, "", "Process.Start returned null", -1);
@@ -35,4 +42,18 @@ public static class ClaudeCliPreflight
return new Result(false, "", ex.Message, -1);
}
}
private static ProcessStartInfo BuildShimPsi(string shimPath)
{
var shim = ExecutableResolver.BuildShimStartInfo(shimPath, new[] { "--version" });
return new ProcessStartInfo
{
FileName = shim.FileName,
Arguments = shim.Arguments,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
};
}
}
+18 -1
View File
@@ -1,5 +1,6 @@
using System.Diagnostics;
using System.Text;
using ClaudeDo.Data.Environment;
using ClaudeDo.Worker.Config;
namespace ClaudeDo.Worker.Runner;
@@ -24,9 +25,11 @@ public sealed class ClaudeProcess : IClaudeProcess
Func<string, Task> onStdoutLine,
CancellationToken ct)
{
var resolved = ExecutableResolver.Resolve(_cfg.ClaudeBin)
?? throw new InvalidOperationException($"'{_cfg.ClaudeBin}' not found on PATH.");
var psi = new ProcessStartInfo
{
FileName = _cfg.ClaudeBin,
WorkingDirectory = workingDirectory,
RedirectStandardInput = true,
RedirectStandardOutput = true,
@@ -36,6 +39,20 @@ public sealed class ClaudeProcess : IClaudeProcess
StandardOutputEncoding = Encoding.UTF8,
StandardErrorEncoding = Encoding.UTF8,
};
// A shim (.cmd/.bat) can't be launched directly with UseShellExecute=false;
// route it through cmd.exe /c so the real interpreter starts the target.
if (resolved.IsShim)
{
psi.FileName = "cmd.exe";
psi.ArgumentList.Add("/c");
psi.ArgumentList.Add(resolved.Path);
}
else
{
psi.FileName = resolved.Path;
}
foreach (var arg in arguments)
psi.ArgumentList.Add(arg);