fix(cli): fall back to settings.json when Claude Code binary is unresolvable (#6701) (#6734)

getCliRuntimeStatus() only ever answered `installed` from binary resolution
(known install paths + where/which PATH search), so a stale PATH, moved
binary, or uncatalogued install method reported "not found" even when
~/.claude/settings.json proved the CLI was installed and used before —
regressing behind upstream 9router's checkClaudeInstalled(), which already
falls back to the settings file when where/which fails.

withSettingsFallback() (new src/shared/services/cliInstallFallback.ts, kept
out of the frozen cliRuntime.ts to respect its file-size ceiling) restores
that parity: only when the binary lookup's own reason is "not_found" (never
for deliberate security rejections like unsafe/relative env overrides or
symlink escapes) and the tool's settings file exists on disk.
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-10 15:06:15 -03:00
committed by GitHub
parent 23c4086a81
commit de193f8b24
4 changed files with 147 additions and 3 deletions

View File

@@ -0,0 +1,69 @@
import fsSync from "fs";
/**
* #6701 — 9router-parity fallback for CLI install detection.
*
* `getCliRuntimeStatus()` in `cliRuntime.ts` determines `installed` from
* binary resolution alone (known install paths + a `where`/`which` PATH
* search). If the binary is not currently resolvable — stale PATH inherited
* by a long-running/background OmniRoute process, the binary having moved,
* or an install method we don't enumerate yet — it used to unconditionally
* report `installed:false`, even when the tool's own settings/config file on
* disk proves it was installed and used before.
*
* Upstream 9router's equivalent route
* (`src/app/api/cli-tools/claude-settings/route.js::checkClaudeInstalled()`)
* has a second-chance fallback: when `where`/`which` fails, it still reports
* `installed:true` if the settings file exists. This restores that fallback
* for any CLI tool that declares a `settings` config path (currently
* `claude` and `droid` — see `CLI_TOOLS` in `cliRuntime.ts`).
*
* Only applies when the lookup's own reason is "not_found" — i.e. the binary
* genuinely couldn't be located on PATH/known install paths. Deliberate
* security rejections (unsafe/relative env override paths, symlink escapes,
* suspicious file sizes, etc.) must stay `installed:false` regardless of
* whether a settings file happens to exist.
*/
export interface NotInstalledResult {
installed: false;
runnable: boolean;
command: string | null;
commandPath: string | null;
reason: string;
runtimeMode: string;
requiresBinary: boolean;
}
export interface SettingsFallbackResult {
installed: true;
runnable: false;
command: string | null;
commandPath: null;
reason: "settings_found_binary_unresolved";
runtimeMode: string;
requiresBinary: boolean;
}
/**
* Given the resolved settings-file path for a tool (or undefined if the tool
* has none) and the "not installed" result the binary lookup already
* produced, return a settings-fallback result when the settings file exists
* on disk, or the original "not installed" result unchanged otherwise.
*/
export const withSettingsFallback = (
settingsPath: string | undefined,
notInstalledResult: NotInstalledResult
): NotInstalledResult | SettingsFallbackResult => {
if (notInstalledResult.reason !== "not_found") return notInstalledResult;
if (!settingsPath || !fsSync.existsSync(settingsPath)) return notInstalledResult;
return {
installed: true,
runnable: false,
command: notInstalledResult.command,
commandPath: null,
reason: "settings_found_binary_unresolved",
runtimeMode: notInstalledResult.runtimeMode,
requiresBinary: notInstalledResult.requiresBinary,
};
};