fix(auth-status): route claude auth status/login through resolver, not PATH#492
Merged
Conversation
…t PATH (#478) @stefanpartheym diagnosed that two call sites in v1.42.0 still spawn `claude` via shell PATH instead of routing through the resolver we shipped in #485. He's correct — and there's a third site he didn't see plus two related `claude auth login` invocations that share the same flaw. Five total. Repro: bunx-installed meridian under systemd with no global `claude` binary. The resolver correctly picks the bundled or platform-package path for the SDK subprocess (visible in /health.claudeExecutable), but the auth-status / auth-login spawns don't go through it — they hit shell PATH. Without `claude` on PATH, those spawns fail and the user sees "Could not verify Claude auth status" on startup, plus `meridian profile list` and `meridian profile add` silently fall back to "loggedIn: false" reporting. Fixes: 1. New `resolveClaudeExecutableSync` in src/proxy/models.ts. Sync subset of the resolver — env override, bundled, platform-package. Skips path-lookup (sync exec is platform-fragile) and legacy-cli-js (only matters for stale Bun installs of SDK < 0.2.98). Used by sync CLI commands that can't await before spawning claude. 2. `getClaudeAuthStatusAsync` (models.ts:234) — now resolves the executable path via `resolveClaudeExecutableAsync` and runs `<resolved> auth status` via `execFile`. No PATH dependency. 3. `getAuthStatus` (profileCli.ts:47) — sync version using `resolveClaudeExecutableSync` + `execFileSync`. Powers `meridian profile list` reporting. 4. `runCli`'s pre-flight auth check (bin/cli.ts:157) — same fix. Refactored the test injection point: was `runExec` (typeof exec), now `runAuthCheck` (() => Promise<{stdout}>). The existing test that simulates spawn ENOENT keeps working with the new contract. 5. `profileAdd` and `profileLogin` — both spawn `claude auth login` for the browser handshake. Same fix: resolve the path first, then spawn via the resolved binary. Tests: 5 new in claude-executable-resolver.test.ts covering the sync resolver (env / bundled / platform-package / null-on-miss / does-not- consult-exec). 1 existing test in proxy-async-ops.test.ts updated for the runCli signature change. Full suite 1732/0. End-to-end verification: booted the patched build with PATH stripped of any `claude` shim (PATH=/usr/local/bin:/usr/bin:/bin — no ~/.local/bin). Result: - No "Could not verify Claude auth status" warning at startup ✓ - /health reports loggedIn:true with email/subscriptionType ✓ - claudeExecutable.source resolves correctly via bundled ✓ Stefan should be unblocked once 1.42.1 (or whatever version this lands in) reaches him.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #478.
What @stefanpartheym revealed
His update on the issue (after I shipped #485) cut through the noise:
The original `error: Script not found "stream-json"` from his first report was a downstream symptom of this same class of issue — without auth-status visibility, the proxy's behavior under the missing-`claude` condition cascades in surprising ways.
What I found
Five call sites, not two:
Fix
New `resolveClaudeExecutableSync` in `src/proxy/models.ts` — sync subset of the resolver. Steps: env → bundled → platform-package. Skips path-lookup (sync exec is platform-fragile) and legacy-cli-js (only matters on stale Bun installs of SDK < 0.2.98). Returns `{ path, source } | null`.
All five call sites refactored to resolve the path first, then spawn via that explicit path. `execFile` / `execFileSync` (not `exec` / `execSync`) so we bypass the shell entirely — no PATH lookup, no quoting issues with spaces in resolved paths.
`runCli` test injection point updated from `runExec` (typeof exec) to `runAuthCheck` (`() => Promise<{stdout}>`). The existing "keeps CLI missing-binary warning behavior" test still validates the same scenario (auth check fails → warning fires) under the new contract.
Tests
5 new in `src/tests/claude-executable-resolver.test.ts`:
1 existing test in `proxy-async-ops.test.ts` updated for the new `runCli` signature.
Full suite: 1732 / 0. Typecheck clean. Build clean.
End-to-end verification
Reproduced Stefan's environment in miniature: booted the patched build with `PATH=/usr/local/bin:/usr/bin:/bin` (no `~/.local/bin` where my real `claude` shim lives — confirmed missing via `PATH=... which claude` → not found).
Before this PR (current main):
```
⚠ Could not verify Claude auth status. If requests fail, run: claude login
```
After this PR (this branch):
```
[PROXY] Plugins loaded: 2 active, 0 disabled, 0 errors
Meridian running at http://127.0.0.1:3500
Telemetry dashboard: http://127.0.0.1:3500/telemetry
Model pins: opus=claude-opus-4-7 sonnet=claude-sonnet-4-6 haiku=claude-haiku-4-5
Claude executable: /Users/.../node_modules/@anthropic-ai/claude-code/bin/claude.exe (resolved via bundled)
```
```json
{
"status": "healthy",
"auth": {
"loggedIn": true,
"email": "...",
"subscriptionType": "max"
},
"claudeExecutable": {
"path": "/Users/.../node_modules/@anthropic-ai/claude-code/bin/claude.exe",
"source": "bundled"
}
}
```
No warning. Auth verified. `/health` reports the right state. Same image-binary resolution path that was working for the SDK subprocess all along — now applied consistently across every call site.
Why a sync resolver
`profileCli.ts:getAuthStatus` is sync, called from `profileList` / `profileAdd` / `profileLogin` — making it async ripples through the entire CLI. A small sync resolver covering the FS-only steps (env / bundled / platform-package) is the surgical fix. Path-lookup is intentionally skipped — sync `execSync("which claude")` is platform-fragile and the audit showed bundled + platform-package covers every supported install layout (npm-global, npx/bunx, Docker, NixOS).
Risk
Strictly improves on existing behavior:
Merge strategy
Squash merge. Hold on merging — Stefan should verify with the next release, but this one's small and well-tested enough that you can ship it whenever.
Credit
Diagnosis, the two-call-site pointer, and the workaround that confirmed the hypothesis: @stefanpartheym in #478. The other three call sites surfaced during the audit while implementing his pointer.