Skip to content

fix(providers/claude): reject directory paths and expand npm package dirs (#1723)#1737

Merged
Wirasm merged 3 commits into
devfrom
fix/issue-1723-claude-binary-dir
May 21, 2026
Merged

fix(providers/claude): reject directory paths and expand npm package dirs (#1723)#1737
Wirasm merged 3 commits into
devfrom
fix/issue-1723-claude-binary-dir

Conversation

@Wirasm
Copy link
Copy Markdown
Collaborator

@Wirasm Wirasm commented May 21, 2026

Summary

  • Problem: On Windows, configuring assistants.claude.claudeBinaryPath to the npm-distributed platform-package directory (e.g. ...\@anthropic-ai\claude-code-win32-x64) made the Claude Agent SDK crash with Claude Code native binary not found at <path>. The resolver validated with existsSync, which returns true for directories; the SDK then spawned the directory and got ENOENT.
  • Why it matters: Claude is unusable in compiled binaries on Windows under the natural npm install layout; the workaround (point at the inner claude.exe) was not discoverable from the misleading SDK-side error.
  • What changed: New pathKind() helper distinguishes file / directory / missing, plus expandDirectoryToExecutable() that transparently maps a configured directory to its claude / claude.exe child when present. Env and config branches now share a validateAndExpand() helper that throws a directory-specific error when the directory is empty (names the expected child filename).
  • What did NOT change (scope boundary): Autodetect branch (it already targets a file path directly). Codex resolver — same existsSync pattern exists in packages/providers/src/codex/binary-resolver.ts:18-25 but there is no current bug report; deliberately left for a follow-up to honor YAGNI. Public exports of resolveClaudeBinaryPath and fileExists are unchanged; pathKind is added.

UX Journey

Before

User                       Archon (resolver)              Claude Agent SDK
────                       ─────────────────              ────────────────
configures claudeBinary  ─▶ existsSync(dir) = true ─────▶ spawn(dir, [...]) ──┐
Path → C:\...\claude-      pass dir to SDK opaquely                            │
code-win32-x64                                                        ENOENT ◀─┘
                                                                              │
                              ◀─────────────────────────────────────────── reframes as
                                                                       "native binary not found"
sees confusing               throws to caller
SDK-side error      ◀───── (no actionable info about
                            directory-vs-file mismatch)

After

User                       Archon (resolver)                      Claude Agent SDK
────                       ─────────────────                      ────────────────
configures claudeBinary  ─▶ [pathKind(dir) = 'directory']
Path → C:\...\claude-       [expandDirectoryToExecutable] ─┐
code-win32-x64              ↓
                            join(dir, 'claude.exe')
                            [pathKind = 'file']            ─────▶ spawn(claude.exe, [...]) ✓
                            pass expanded file to SDK
sees workflow running ◀──── streams response                 ◀── streams normally

Failure case: directory configured but no claude/claude.exe inside
  ↓ resolver throws *directory-specific* error naming the expected child filename
  ↓ user knows exactly what to fix

Architecture Diagram

Before

config.yaml → parseClaudeConfig ──┐
                                  ▼
env CLAUDE_BIN_PATH ─────▶ resolveClaudeBinaryPath ──▶ Claude Agent SDK
                          (fileExists / existsSync)     pathToClaudeCodeExecutable
                                  │                        │
                                  ▼                        ▼
                            getLog().info              child_process.spawn

After

config.yaml → parseClaudeConfig ──┐
                                  ▼
env CLAUDE_BIN_PATH ─────▶ resolveClaudeBinaryPath ──▶ Claude Agent SDK
                                  │                        │
                                  ▼                        ▼
                          [~] validateAndExpand        child_process.spawn
                              ├─ [+] pathKind          (now always a file)
                              └─ [+] expandDirectoryToExecutable
                                  │
                                  ▼
                            getLog().info

Connection inventory:

From To Status Notes
resolveClaudeBinaryPath fileExists modified Still used by autodetect branch; no longer used by env/config branches
resolveClaudeBinaryPath validateAndExpand new Shared validator for env + config paths
validateAndExpand pathKind new File vs directory vs missing classification
validateAndExpand expandDirectoryToExecutable new Maps directory → child binary when present
expandDirectoryToExecutable pathKind new Confirms expanded child is a file
provider.ts:512 (pathToClaudeCodeExecutable) resolveClaudeBinaryPath unchanged Output contract identical (still string | undefined)

Label Snapshot

  • Risk: risk: low
  • Size: size: S
  • Scope: core (specifically providers)
  • Module: providers:claude

Change Metadata

  • Change type: bug
  • Primary scope: core (providers)

Linked Issue

Validation Evidence (required)

$ bun run type-check
# all packages exit 0

$ bun run lint
# exit 0

$ bun run format:check
# All matched files use Prettier code style!

$ bun --filter @archon/providers test packages/providers/src/claude/binary-resolver.test.ts packages/providers/src/claude/binary-resolver-dev.test.ts
# 14 binary-mode tests + 6 dev-mode tests + sibling provider/config tests: all pass
  • Evidence provided: Test output above (full run included new directory-expansion cases and directory-without-binary error paths for both CLAUDE_BIN_PATH and claudeBinaryPath).
  • Skipped: bun run validate reports 3 pre-existing failures in @archon/adapters > telegram-markdown > blockquotes that reproduce on dev (verified by checking out the file from dev and re-running the adapter tests — failures persist independent of this diff). Those tests were last touched by d1feab07 fix(adapters): bump telegramify-markdown to 1.3.3; not investigated as part of this PR.

Security Impact (required)

  • New permissions/capabilities? No
  • New external network calls? No
  • Secrets/tokens handling changed? No
  • File system access scope changed? NostatSync is added in the same code path that already called existsSync on the same configured paths. No new directories or files are stat'd that weren't being existence-checked before.

Compatibility / Migration

  • Backward compatible? Yes — every previously-accepted path (a file) still resolves to the same value. The only behavioral change is that a directory now either expands transparently (success path that previously crashed inside the SDK) or throws a clearer Archon-side error (previously a confusing SDK-side error).
  • Config/env changes? No
  • Database migration needed? No

Human Verification (required)

  • Verified scenarios:
    • macOS (this host): bun --filter @archon/providers test for both binary-mode and dev-mode resolver test suites — all pass. The directory-expansion tests dynamically pick claude (Unix) vs claude.exe (Windows) so they exercise the real branch on this host.
    • Logic walk-through: Confirmed that the autodetect branch (still using fileExists) is unchanged — its target path is built via join(homedir(), '.local', 'bin', 'claude{.exe}'), which is always a file by construction, so the directory-case bug cannot arise there.
  • Edge cases checked: Empty CLAUDE_BIN_PATH= (still falls through to undefined in dev mode — pinned by existing test). Directory with no inner binary (new directory-specific error). Symlink behavior: statSync follows symlinks by default, so a symlink-to-file resolves as 'file', and a broken symlink resolves as 'missing' — matches user expectations.
  • What was not verified: End-to-end run on an actual Windows host pointing at the user's original failing path (...\@anthropic-ai\claude-code-win32-x64). The author does not have a Windows host; verification depends on @wikimatt or another Windows user retesting after merge. The unit tests are platform-conditional but the directory→claude.exe expansion logic was specifically motivated by the Windows scenario.

Side Effects / Blast Radius (required)

  • Affected subsystems/workflows: Only resolveClaudeBinaryPath() callers — currently a single caller at packages/providers/src/claude/provider.ts:862. The output contract is unchanged (Promise<string \| undefined>). The expanded path is a child of the user-supplied directory, so it remains inside whatever filesystem region the user already granted.
  • Potential unintended effects: A user who previously had a file called claude in some unrelated directory and pointed at that directory expecting something else would now see Archon attempt to spawn that claude. This is so unlikely (the file would have to be named claude exactly) that it's a deliberately accepted edge case — and the previous behavior in that scenario was already broken (SDK ENOENT on the directory).
  • Guardrails/monitoring: The resolver's existing claude.binary_resolved log line now records the expanded path. Operators triaging spawn issues can grep for the actual path the SDK was given.

Rollback Plan (required)

  • Fast rollback: git revert <merge-commit> — single-commit revert is safe; no shared-state mutation, no migration.
  • Feature flags or config toggles: None. The fix is unconditional but strictly additive on the happy path.
  • Observable failure symptoms: If this regresses, users would see either (a) a directory path that previously resolved (impossible — directories never worked) or (b) a file path that previously resolved now throwing — would manifest as claude.binary_resolved log line missing followed by validateAndExpand error. Easy to spot.

Risks and Mitigations

  • Risk: statSync is added to the hot path; a hostile filesystem (e.g. dangling network mount) could now throw where existsSync would have returned false.
    • Mitigation: pathKind() wraps statSync in try/catch and returns 'missing' on any throw — behavior is conservatively a superset of the previous existsSync path.
  • Risk: User configures a directory that happens to contain a non-Claude claude binary (e.g. some unrelated executable named claude on Unix).
    • Mitigation: Same risk existed before the SDK was ever passed a path; the expansion only fires when the user explicitly configured a directory. The SDK will surface a clear runtime error from the spawned process if it isn't actually Claude Code. Accepted edge case.
  • Risk: Codex resolver has the same latent bug.
    • Mitigation: Documented in the investigation artifact (.claude/PRPs/issues/issue-1723.md) and intentionally deferred. No current bug report; YAGNI.

Summary by CodeRabbit

  • Bug Fixes

    • Properly expand and validate directory Claude binary paths so executables are found; clearer, directory-specific error messages when missing.
  • Improvements

    • More robust path classification (file / directory / missing) and logging of the resolved executable path; env/config precedence preserved.
  • Tests

    • Added/updated tests for directory expansion, broken symlinks, env-var behavior, and path classification.
  • Documentation

    • Clarified CLAUDE_BIN_PATH and config docs and .env example to note auto-expansion of platform-package directories to the Claude executable.

Review Change Stack

…dirs

The Claude binary resolver validated configured paths with existsSync, which
returns true for directories. Users on Windows who installed Claude Code via
npm and configured claudeBinaryPath to the npm platform-package directory
(e.g. ...\@Anthropic-AI\claude-code-win32-x64) hit a confusing SDK-side
ReferenceError ("Claude Code native binary not found at <path>") because the
SDK's child_process.spawn(directory) failed with ENOENT.

Replace the existence-only check with a pathKind() helper that distinguishes
file / directory / missing, and transparently expand a configured directory
to the platform-appropriate child executable (claude.exe on Windows, claude
on Unix) when present. A directory without the expected binary now produces
a directory-specific error that tells the user what to fix.

The autodetect branch already targets a file path directly and is unchanged.

Fixes #1723
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 21, 2026

📝 Walkthrough

Walkthrough

Stat-based path classification (pathKind) distinguishes files, directories, and missing paths. Directories are expanded to the platform-specific Claude executable; config/env overrides and autodetect are validated/expanded via this logic. Tests and docs updated to exercise directory expansion, error cases, and real filesystem pathKind behavior.

Changes

Path classification and directory expansion

Layer / File(s) Summary
Path classification utilities and validation
packages/providers/src/claude/binary-resolver.ts
Exports pathKind(path): 'file' | 'directory' | 'missing' using statSync; introduces CLAUDE_BINARY_NAME and internal validateAndExpand()/expansion logic and distinct directory-vs-missing errors.
Binary resolution with path validation
packages/providers/src/claude/binary-resolver.ts
resolveClaudeBinaryPath() routes CLAUDE_BIN_PATH and assistants.claude.claudeBinaryPath through validateAndExpand() and logs the resolved executable path; autodetect uses CLAUDE_BINARY_NAME + pathKind() to ensure returned path is an executable file.
Main test suite updates and pathKind unit tests
packages/providers/src/claude/binary-resolver.test.ts
Tests replace fileExists mocking with pathKind stubs; add directory-expansion assertions, broken-symlink unit tests for pathKind, and update env/config/autodetect precedence and error-message expectations.
Dev-mode tests for CLAUDE_BIN_PATH handling
packages/providers/src/claude/binary-resolver-dev.test.ts
Dev tests switched to pathKindSpy, ensure env-var cleanup, validate directory expansion to the nested executable using CLAUDE_BINARY_NAME and join, and assert directory-specific error messages when the expected executable is absent.
Docs and examples updates
.env.example, CLAUDE.md, packages/docs-web/src/content/docs/*
Documentation updated to state npm platform-package directory inputs are accepted and auto-expanded to claude/claude.exe for CLAUDE_BIN_PATH and claudeBinaryPath config.
Codex resolver note
packages/providers/src/codex/binary-resolver.ts
Add TODO noting existsSync can return true for directories and suggesting parity with Claude resolver's stat-based validation.

Sequence Diagram

sequenceDiagram
  participant Test
  participant resolveClaudeBinaryPath
  participant validateAndExpand
  participant pathKind

  Test->>resolveClaudeBinaryPath: call with env/config override
  resolveClaudeBinaryPath->>validateAndExpand: validate override path
  validateAndExpand->>pathKind: classify provided path
  pathKind-->>validateAndExpand: 'file' / 'directory' / 'missing'
  alt directory
    validateAndExpand->>validateAndExpand: expand to claude/claude.exe
    validateAndExpand->>pathKind: classify expanded path
    pathKind-->>validateAndExpand: 'file' or 'missing'
    alt executable found
      validateAndExpand-->>resolveClaudeBinaryPath: return executable path
    else executable missing
      validateAndExpand-->>resolveClaudeBinaryPath: throw directory error
    end
  else missing
    validateAndExpand-->>resolveClaudeBinaryPath: throw missing error
  else file
    validateAndExpand-->>resolveClaudeBinaryPath: return path as-is
  end
  resolveClaudeBinaryPath-->>Test: resolved path or error
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • coleam00/Archon#1361: Overlaps resolveClaudeBinaryPath() changes; that PR adds an autodetect fallback while this PR refactors validation to pathKind.
  • coleam00/Archon#1217: Earlier changes to Claude binary resolution and validation that this refactor builds upon.
  • coleam00/Archon#1481: Related updates to CLAUDE_BIN_PATH handling and dev-mode test expectations.

Poem

🐰 I sniffed the paths both dark and bright,
statSync showed me file, dir, or missing light.
I hop and expand each folder's claim,
to find the Claude executable and call its name.
Hooray—no more mysterious spawn-time fright!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the main changes: rejecting directory paths and expanding npm package directories for Claude binary resolution.
Description check ✅ Passed The description is comprehensive and follows the template structure with all major sections completed: Summary (4 bullets), UX Journey (before/after), Architecture Diagram with connection inventory, metadata, linked issues, validation evidence, security impact, compatibility, human verification, side effects, and rollback plan.
Linked Issues check ✅ Passed The PR directly addresses issue #1723 by implementing the required objective: directories are now either transparently expanded to the platform executable (claude/claude.exe) or throw a clear directory-specific error naming the expected child binary.
Out of Scope Changes check ✅ Passed All changes are in-scope for issue #1723. The PR modifies Claude binary resolver tests and implementation, adds documentation clarifications, and includes a TODO comment in Codex resolver referencing the related issue for deliberate future follow-up (not out-of-scope).
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-1723-claude-binary-dir

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

…dex TODO

- Add a regression test for pathKind() returning 'missing' on a broken
  symlink (uses a real tmp symlink so the statSync ENOENT path is actually
  exercised, not mocked).
- Add a TODO marker in the Codex resolver pointing at #1723. The Codex
  resolver has the identical existsSync-on-directory gap; left unfixed in
  this PR to avoid scope creep but now discoverable from the file itself
  when a Codex bug report lands or someone does a deliberate parity pass.
@Wirasm
Copy link
Copy Markdown
Collaborator Author

Wirasm commented May 21, 2026

Automated Self-Review

Ran code-reviewer against 3e8e4b70 (now superseded by 4e6727e0 which addresses both findings below).

Strengths

  • Root cause is precisely addressed: the new pathKind() + expandDirectoryToExecutable() + validateAndExpand() chain discriminates file / directory / missing before the SDK ever spawns.
  • Error messages are actionable and distinguish the two failure modes (missing vs. directory-without-binary).
  • Autodetect branch correctly left alone — it builds the path itself and only ever checks a known-good file.
  • Test-spy migration is clean and complete across both resolver test files.
  • No any, no AI attribution, no over-engineering. CLAUDE.md compliant.

Findings + follow-ups (addressed in 4e6727e0)

1. Codex resolver has the same existsSync-on-directory gap (confidence 85)

  • packages/providers/src/codex/binary-resolver.ts:23-26 uses the same pattern. Investigation artifact and PR body deferred this to avoid scope creep (YAGNI — no current Codex bug report), but the reviewer correctly noted that nothing in the Codex file itself made the gap discoverable to a future maintainer.
  • Fix in 4e6727e0: added an inline TODO(#1723) comment in the Codex resolver describing the gap and pointing at the Claude fix as the template. No behavior change in this PR.

2. Broken-symlink behavior claimed but not pinned by a test (confidence 80)

  • The PR body asserts that statSync following symlinks + the try/catch in pathKind means a broken symlink resolves as 'missing'. True, but no test exercised that path.
  • Fix in 4e6727e0: added a pathKind test that creates a real tmp symlink to a nonexistent target and asserts 'missing' — exercises the actual statSync ENOENT path, not a mock.

Security

No new network I/O or external surface. statSync is bounded to user-configured paths (env var or config file value) — no path constructed from untrusted input, no traversal vector.

Verdict

Ready for human review.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/providers/src/claude/binary-resolver.ts`:
- Around line 40-48: pathKind currently swallows all errors from _statSync and
returns 'missing', masking permission/access errors (EACCES/EPERM) so
validateAndExpand misreports them; update pathKind(path) to only treat ENOENT
(and maybe ENOTDIR) as 'missing' and rethrow any other errors from _statSync (or
return a distinct result that validateAndExpand handles) so callers like
validateAndExpand can surface permission failures—update the function around the
_statSync call and the catch block accordingly, referencing pathKind and its use
by validateAndExpand.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 344254ce-b2f9-467d-82af-8417f665836f

📥 Commits

Reviewing files that changed from the base of the PR and between 7ab4df8 and 3e8e4b7.

📒 Files selected for processing (3)
  • packages/providers/src/claude/binary-resolver-dev.test.ts
  • packages/providers/src/claude/binary-resolver.test.ts
  • packages/providers/src/claude/binary-resolver.ts

Comment on lines +40 to +48
export function pathKind(path: string): 'file' | 'directory' | 'missing' {
try {
const stat = _statSync(path);
if (stat.isFile()) return 'file';
if (stat.isDirectory()) return 'directory';
return 'missing';
} catch {
return 'missing';
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="packages/providers/src/claude/binary-resolver.ts"
echo "== Lines around pathKind() =="
nl -ba "$FILE" | sed -n '1,140p'

echo "== Search for validateAndExpand() usage and error messages =="
rg -n "validateAndExpand|pathKind\\(" "$FILE"

echo "== Lines around validateAndExpand() =="
nl -ba "$FILE" | sed -n '140,260p'

Repository: coleam00/Archon

Length of output: 150


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="packages/providers/src/claude/binary-resolver.ts"

echo "== Lines around pathKind() =="
sed -n '1,160p' "$FILE" | cat -n

echo "== Occurrences of validateAndExpand() and pathKind() in file =="
rg -n "validateAndExpand|pathKind\\(" "$FILE" || true

echo "== Lines around validateAndExpand() =="
# Print a wider range to ensure we capture function definition
sed -n '160,320p' "$FILE" | cat -n

Repository: coleam00/Archon

Length of output: 9362


Don’t collapse all statSync failures into "missing"

packages/providers/src/claude/binary-resolver.ts pathKind() catches all _statSync errors and returns 'missing', so validateAndExpand() can incorrectly report permission/access failures (e.g., EACCES/EPERM) as “file does not exist”.

Suggested fix
 export function pathKind(path: string): 'file' | 'directory' | 'missing' {
   try {
     const stat = _statSync(path);
     if (stat.isFile()) return 'file';
     if (stat.isDirectory()) return 'directory';
     return 'missing';
-  } catch {
-    return 'missing';
+  } catch (error: unknown) {
+    const code = (error as NodeJS.ErrnoException).code;
+    if (code === 'ENOENT' || code === 'ENOTDIR') return 'missing';
+    throw error;
   }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function pathKind(path: string): 'file' | 'directory' | 'missing' {
try {
const stat = _statSync(path);
if (stat.isFile()) return 'file';
if (stat.isDirectory()) return 'directory';
return 'missing';
} catch {
return 'missing';
}
export function pathKind(path: string): 'file' | 'directory' | 'missing' {
try {
const stat = _statSync(path);
if (stat.isFile()) return 'file';
if (stat.isDirectory()) return 'directory';
return 'missing';
} catch (error: unknown) {
const code = (error as NodeJS.ErrnoException).code;
if (code === 'ENOENT' || code === 'ENOTDIR') return 'missing';
throw error;
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/providers/src/claude/binary-resolver.ts` around lines 40 - 48,
pathKind currently swallows all errors from _statSync and returns 'missing',
masking permission/access errors (EACCES/EPERM) so validateAndExpand misreports
them; update pathKind(path) to only treat ENOENT (and maybe ENOTDIR) as
'missing' and rethrow any other errors from _statSync (or return a distinct
result that validateAndExpand handles) so callers like validateAndExpand can
surface permission failures—update the function around the _statSync call and
the catch block accordingly, referencing pathKind and its use by
validateAndExpand.

@Wirasm
Copy link
Copy Markdown
Collaborator Author

Wirasm commented May 21, 2026

PR Review Summary — Multi-Agent

Seven specialist agents reviewed the diff. The PR fixes the core bug correctly and CI is green on Ubuntu + Windows + Docker. The main finding is that the fix is incomplete on the autodetect branch — flagged independently by four agents.

Critical (must address)

Agent Issue Location
code-reviewer / silent-failure-hunter / pr-test-analyzer / type-design-analyzer Autodetect branch still uses fileExists (existsSync) — same class of bug the PR fixes for env/config. A directory at ~/.local/bin/claude (or \.local\bin\claude.exe) still passes silently and crashes inside the SDK as ENOENT. The pathKind tooling is right there. packages/providers/src/claude/binary-resolver.ts:162

Suggested fix: Replace if (fileExists(nativeInstallerPath)) with if (pathKind(nativeInstallerPath) === 'file'). The test at binary-resolver.test.ts:84 would need to mock pathKind instead of fileExists. Either fix the autodetect branch or add a one-line comment at line 162 documenting why the asymmetry is intentional (native installer always writes a plain file).

Important

Agent Issue Location
silent-failure-hunter pathKind's catch {} swallows EACCES, ELOOP, etc. and reports them as 'missing'. A user with a permission issue on the binary path sees "file does not exist" — factually wrong, with no operator breadcrumb. The existing comment only justifies the ENOENT/broken-symlink case. binary-resolver.ts:46
comment-analyzer pathKind JSDoc claims "Wrapped for spyOn parity with fileExists" — but pathKind is a new function and its design driver is the file/directory classification, not testability. The clause contradicts the accurate first sentence and will mislead readers. binary-resolver.ts:35
code-reviewer / code-simplifier fileExistsSpy and pathKindSpy declared as ReturnType<typeof spyOn> (non-optional) in binary-resolver.test.ts:24-25, but as | undefined in the dev-mode file. The ?.mockRestore() calls only typecheck because of the inconsistency. Align with the dev-mode pattern. binary-resolver.test.ts:24-25
pr-test-analyzer Dev mode has zero tests for CLAUDE_BIN_PATH as a directory. validateAndExpand is called before the BUNDLED_IS_BINARY guard at line 139, so dev-mode users hit this path — but a future refactor that reorders the checks would break it silently. Two tests mirroring binary-resolver.test.ts:172-213 would close this. binary-resolver-dev.test.ts
pr-test-analyzer describe('pathKind') only tests failure paths ('missing'). No real-filesystem test that pathKind(file) returns 'file' or pathKind(dir) returns 'directory'. Resolver tests all spy on pathKind, so a typo like isFile()isDirectory() would pass every existing test. binary-resolver.test.ts:216

Suggestions

Agent Suggestion Location
code-simplifier process.platform === 'win32' ? 'claude.exe' : 'claude' appears 7 times (3 source, 4 test). Extract as const CLAUDE_BINARY_NAME at module scope, export from source, import in tests. Rule of Three clearly cleared. binary-resolver.ts:59,75,159-161 + 4 test sites
code-simplifier expandDirectoryToExecutable is a 3-line single-caller private helper whose body is shorter than its JSDoc. Inline into the directory branch of validateAndExpand. binary-resolver.ts:58-61
type-design-analyzer Optional: extract export type PathKind = 'file' | 'directory' | 'missing' so test mockReturnValue('file') calls typecheck against the union (today they're unknown). Two-line change, zero downside. binary-resolver.ts:40
comment-analyzer Test section header has // ─── Directory expansion (issue #1723) ─── — the (issue #1723) will rot once the PR merges and the issue closes. The explanatory prose below already carries the context. CLAUDE.md explicitly calls out task-reference comments as rot risk. binary-resolver.test.ts:145
comment-analyzer expandDirectoryToExecutable JSDoc and the "Log must show the expanded executable path" inline comment in tests both restate what the code/assertion already says. Per CLAUDE.md guideline ("default to no comments; only WHY"), remove both. binary-resolver.ts:52-57; binary-resolver.test.ts:164
code-reviewer The Codex TODO(#1723) is a well-scoped, accurate YAGNI deferral. Keep — but consider replacing the #1723 back-reference with a fresh tracking issue number for Codex parity, since #1723 will close on merge. packages/providers/src/codex/binary-resolver.ts:28-34

Documentation Issues

docs-impact found 4–5 surfaces that still claim the binary path is "file only" or "either native binary or cli.js" — now incomplete:

  • CLAUDE.md:488-490claudeBinaryPath comment needs a Windows directory case
  • packages/docs-web/src/content/docs/getting-started/ai-assistants.md:62 — "The Claude Agent SDK accepts either the native compiled binary or a JS cli.js" is now stale
  • packages/docs-web/src/content/docs/reference/configuration.md:68-72 — same gap
  • .env.example:28 — could add a Windows npm directory example
  • packages/docs-web/src/content/docs/getting-started/configuration.mdCLAUDE_BIN_PATH row says "Absolute path to the Claude Code binary or SDK cli.js" — incomplete

Suggested wording: "…or the npm platform-package directory (e.g. @anthropic-ai/claude-code-win32-x64), which is auto-expanded to claude/claude.exe."

Strengths

  • The PR-description claim that statSync follows symlinks → broken symlinks resolve to 'missing' is verified by a real-filesystem test, not a mock. Correct call.
  • claude.binary_resolved log records the expanded path (post-expansion), giving operators the actual path the SDK will spawn. Right choice for triage.
  • Directory-expansion tests assert the source: 'env' vs source: 'config' field on the log line — catches copy-paste errors that would silently mislabel the resolution path.
  • The directory-vs-missing error messages are actionable and name the exact expected child filename.
  • validateAndExpand extraction with only two call sites is defensible despite being one shy of Rule of Three — the duplicated logic was 4-line throw new Error(...) blocks where divergence would have been a real risk.
  • Type design (3-way union) is the right abstraction for the three caller branches; no as casts; all returns provably typed (type-design-analyzer rated 8/10 overall).

Verdict

NEEDS FIXES — one critical (autodetect branch parity), four important (EACCES swallowing, misleading comment, test type hygiene, dev-mode test gap, pathKind happy-path tests).

Recommended Actions

  1. Fix the autodetect branch at binary-resolver.ts:162 — replace fileExists with pathKind === 'file', or document the deliberate asymmetry. This is the highest-confidence finding across reviewers.
  2. Log a WARN with the error code in pathKind's catch when the code is not ENOENT/ENOTDIR so EACCES leaves a triage breadcrumb.
  3. Remove the "Wrapped for spyOn parity" clause from the pathKind JSDoc — it contradicts the accurate first sentence.
  4. Align the two test spy declarations to ReturnType<typeof spyOn> | undefined to match the dev-mode file.
  5. Add two dev-mode tests for CLAUDE_BIN_PATH as a directory (mirror binary-resolver.test.ts:172-213).
  6. Add two pathKind happy-path tests (real file → 'file', real dir → 'directory') — the spy-based resolver tests never exercise the real statSync branch logic.
  7. Update docs (CLAUDE.md, ai-assistants.md:62, configuration.md) to mention directory paths are now accepted.
  8. Consider the CLAUDE_BINARY_NAME constant and inlining expandDirectoryToExecutable for polish.

…adcrumb, doc updates

Extends #1723 fix per multi-agent PR review:

- Autodetect branch now uses pathKind === 'file' instead of fileExists so
  a directory at ~/.local/bin/claude no longer slips past validation and
  crashes the SDK as ENOENT (matches the env/config branches).
- pathKind catches now distinguish ENOENT/ENOTDIR from other stat errors
  (EACCES, ELOOP, etc.) and emit a WARN log line with the error code so
  operators have a triage breadcrumb for permission issues that would
  otherwise surface as the misleading "file does not exist".
- Extract CLAUDE_BINARY_NAME constant (was duplicated 7 times across
  source + tests) and export PathKind type so test mockReturnValue calls
  are type-checked against the union rather than being unknown strings.
- Inline expandDirectoryToExecutable into validateAndExpand — single
  caller, body shorter than its JSDoc. Drop the WHAT-restating first
  sentence of validateAndExpand's docstring.
- Strip the "Wrapped for spyOn parity" clause from pathKind's JSDoc —
  contradicted the accurate first sentence and implied the design was
  testability-driven rather than classification-driven.
- Align spy declarations to `| undefined` in binary-resolver.test.ts to
  match the dev-mode file. Drop the now-unused fileExistsSpy.
- Add pathKind happy-path tests (real file → 'file', real dir →
  'directory'). Without these, a typo like isFile() → isDirectory()
  would pass every existing test because all resolver tests spy through
  pathKind and never exercise the real statSync logic.
- Add two dev-mode tests for CLAUDE_BIN_PATH-as-directory. The env
  branch runs validateAndExpand before the BUNDLED_IS_BINARY guard, so
  dev users get expansion too; pin the contract.
- Add a Windows autodetect-rejects-directory regression test.

Docs: surface the new directory-accepting behavior so Windows users who
install via npm can discover it without re-reading the source.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/providers/src/claude/binary-resolver.ts`:
- Around line 54-58: The catch block in binary-resolver.ts currently logs the
full error and absolute path (variables err and path) which may contain PII;
change the logging in the catch handler for stat failures (inside the try/catch
around fs.stat usage) to avoid emitting the raw err and path — instead extract
and log only non-sensitive fields such as the errno/code and syscall from (err
as NodeJS.ErrnoException) and, if helpful, a redacted hint (e.g., path basename
or a boolean indicating existence) rather than the full absolute path; update
the getLog().warn invocation accordingly so it only includes safe keys like {
code, syscall, redactedPathHint } and the existing message
'claude.path_stat_failed'.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bddd9572-b909-4c03-9e75-322d43fb6462

📥 Commits

Reviewing files that changed from the base of the PR and between 4e6727e and f762e48.

📒 Files selected for processing (8)
  • .env.example
  • CLAUDE.md
  • packages/docs-web/src/content/docs/getting-started/ai-assistants.md
  • packages/docs-web/src/content/docs/getting-started/configuration.md
  • packages/docs-web/src/content/docs/reference/configuration.md
  • packages/providers/src/claude/binary-resolver-dev.test.ts
  • packages/providers/src/claude/binary-resolver.test.ts
  • packages/providers/src/claude/binary-resolver.ts
✅ Files skipped from review due to trivial changes (5)
  • CLAUDE.md
  • .env.example
  • packages/docs-web/src/content/docs/getting-started/configuration.md
  • packages/docs-web/src/content/docs/getting-started/ai-assistants.md
  • packages/docs-web/src/content/docs/reference/configuration.md

Comment on lines +54 to +58
} catch (err) {
const code = (err as NodeJS.ErrnoException).code;
if (code !== 'ENOENT' && code !== 'ENOTDIR') {
getLog().warn({ err, path, code }, 'claude.path_stat_failed');
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid logging full filesystem paths in stat-failure warnings.

Line 57 logs path and raw err; absolute paths often embed usernames/home dirs and can leak PII. Log only non-sensitive diagnostics (e.g., code, syscall) or a redacted hint.

Proposed minimal redaction diff
   } catch (err) {
     const code = (err as NodeJS.ErrnoException).code;
     if (code !== 'ENOENT' && code !== 'ENOTDIR') {
-      getLog().warn({ err, path, code }, 'claude.path_stat_failed');
+      const fsErr = err as NodeJS.ErrnoException;
+      getLog().warn(
+        { code, syscall: fsErr.syscall },
+        'claude.path_stat_failed'
+      );
     }
     return 'missing';
   }

As per coding guidelines, "Never log API keys, tokens (mask as token.slice(0, 8) + '...'), user message content, or PII".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (err) {
const code = (err as NodeJS.ErrnoException).code;
if (code !== 'ENOENT' && code !== 'ENOTDIR') {
getLog().warn({ err, path, code }, 'claude.path_stat_failed');
}
} catch (err) {
const code = (err as NodeJS.ErrnoException).code;
if (code !== 'ENOENT' && code !== 'ENOTDIR') {
const fsErr = err as NodeJS.ErrnoException;
getLog().warn(
{ code, syscall: fsErr.syscall },
'claude.path_stat_failed'
);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/providers/src/claude/binary-resolver.ts` around lines 54 - 58, The
catch block in binary-resolver.ts currently logs the full error and absolute
path (variables err and path) which may contain PII; change the logging in the
catch handler for stat failures (inside the try/catch around fs.stat usage) to
avoid emitting the raw err and path — instead extract and log only non-sensitive
fields such as the errno/code and syscall from (err as NodeJS.ErrnoException)
and, if helpful, a redacted hint (e.g., path basename or a boolean indicating
existence) rather than the full absolute path; update the getLog().warn
invocation accordingly so it only includes safe keys like { code, syscall,
redactedPathHint } and the existing message 'claude.path_stat_failed'.

@Wirasm Wirasm merged commit 3974cba into dev May 21, 2026
4 checks passed
@Wirasm Wirasm deleted the fix/issue-1723-claude-binary-dir branch May 21, 2026 09:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Windows: claudeBinaryPath resolved from config but SDK fails with "native binary not found"

1 participant