Skip to content

fix(cli-claude): resolve npm global claude install via process.execPath#153

Merged
leeroybrun merged 2 commits intohappier-dev:devfrom
LightYear512:fix/claude-npm-global-resolution
Apr 16, 2026
Merged

fix(cli-claude): resolve npm global claude install via process.execPath#153
leeroybrun merged 2 commits intohappier-dev:devfrom
LightYear512:fix/claude-npm-global-resolution

Conversation

@LightYear512
Copy link
Copy Markdown
Contributor

@LightYear512 LightYear512 commented Apr 13, 2026

Summary

  • The Agent SDK's Claude Code path resolver currently depends on PATH to find claude when it is installed via npm global. Daemons / spawned CLI processes frequently inherit a minimal PATH that does not contain the directory where node itself lives, and the fallback paths (~/.local/bin, native installer locations) don't cover npm global installs either.
  • This shows up as a confusing Claude Code is not installed (or not detectable) error in the app when claude works fine from an interactive shell.
  • Two common setups that trip this:
    • Windows + nvm-windows: C:\Program Files\nodejs is a junction managed by nvm and can be missing from a detached daemon child's PATH.
    • Linux daemons with reset PATH: Same problem class previously addressed for systemd in fix(daemon): capture user PATH in systemd unit file on Linux #42, but that fix only captures PATH at happier daemon install time and doesn't help CLIs spawned outside that flow.
  • Adds a new resolution step that derives the npm global @anthropic-ai/claude-code/cli.js entrypoint from process.execPath, which always points to the active Node.js binary regardless of PATH state.

Details

  • New findClaudeInNpmGlobalModules() helper in apps/cli/src/backends/claude/sdk/utils.ts. It checks both the Windows layout (<execDir>/node_modules/...) and the Unix layout (<execDir>/../lib/node_modules/...).
  • Wired into getDefaultClaudeCodePathForAgentSdk() as a new fallback step between the existing resolveProviderCliCommand attempt and the versioned install dir / native installer location checks. Ordering ensures the existing happy paths (PATH-resolved claude, explicit HAPPIER_CLAUDE_PATH override) are preserved and unchanged.
  • No caching is introduced — matches the existing behavior of getDefaultClaudeCodePathForAgentSdk(), which is called once per remote Claude session spawn.

Test plan

  • Unit tests for both platform layouts added in apps/cli/src/backends/claude/sdk/utils.test.ts, stubbing process.execPath to a sandbox directory.
  • Existing test suite isolated from the host's real npm global install via a new stubExecPathToEmptySandbox() helper in both describe blocks, so the "no Claude Code installed" assertions no longer accidentally find a real cli.js on the test runner.
  • vitest run src/backends/claude/sdk/utils.test.ts — 15/15 passing.
  • Manual reproduction on Windows + nvm-windows: process.execPath points at C:\Program Files\nodejs\node.exe, which resolves to the real C:\Program Files\nodejs\node_modules\@anthropic-ai\claude-code\cli.js.

Related

Summary by CodeRabbit

  • New Features

    • The CLI can now detect Claude installations installed via npm global modules, improving installation detection across environments.
  • Tests

    • Added tests covering Claude path resolution for npm-global installations on both Windows and non-Windows platforms, including sandboxed execPath scenarios to ensure reliable discovery and restoration.

The Agent SDK path resolver could not find Claude Code when it was
installed via an npm global prefix whose `node` directory was not
reliably present on PATH at runtime. This happens in two common setups:

- Windows + nvm-windows, where `C:\Program Files\nodejs` is a junction
  to the active version and may be missing from a daemon-spawned child
  process's PATH.
- Detached daemons on any platform whose PATH has been stripped down
  to a service-default set that does not include the npm global prefix
  (the same class of issue addressed by happier-dev#42 for systemd on Linux).

In those environments every existing fallback (`resolveProviderCliCommand`,
the versioned installer dir, the native installer locations) fails, and
users see the confusing "Claude Code is not installed (or not detectable)"
error even though `claude` works fine from their interactive shell.

This change adds a new fallback step that derives the npm global
`@anthropic-ai/claude-code/cli.js` entrypoint directly from
`process.execPath`, which always points to the active `node` binary
regardless of PATH state. Both the Windows layout
(`<execDir>/node_modules/...`) and the Unix layout
(`<execDir>/../lib/node_modules/...`) are checked.

Includes unit tests that stub `process.execPath` to a sandbox directory
and verify the new resolution path on both platform shapes. The existing
tests are also updated to isolate `process.execPath` so the host's real
npm global install cannot bleed into the assertions.
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 13, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 47f73021-b479-461f-9446-2fb6d31e01e2

📥 Commits

Reviewing files that changed from the base of the PR and between 7337a38 and ef55627.

📒 Files selected for processing (1)
  • apps/cli/src/backends/claude/sdk/utils.ts

Walkthrough

Adds npm-global npm module discovery for Claude CLI entrypoints by deriving locations from dirname(process.execPath) and checking typical npm global layouts; integrates this probe into getDefaultClaudeCodePathForAgentSdk() between versioned-install detection and native-installer probing.

Changes

Cohort / File(s) Summary
Implementation
apps/cli/src/backends/claude/sdk/utils.ts
Adds findClaudeInNpmGlobalModules() which derives likely npm-global @anthropic-ai/claude-code/cli.js locations relative to dirname(process.execPath) (Windows and Unix layouts) and checks existsSync. Integrates this probe into getDefaultClaudeCodePathForAgentSdk() after the Unix versioned-install probe and before native-installer probing; if compatible, returns the discovered path directly (no canonicalization).
Test Coverage
apps/cli/src/backends/claude/sdk/utils.test.ts
Saves/restores process.execPath descriptor; adds stubExecPathToEmptySandbox(workDir) to create sandboxed node executable and stub process.execPath; adds test cases for npm-global discovery on non-Windows and Windows by stubbing PATH, process.platform, and process.execPath to validate resolution to the global cli.js.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: adding npm global Claude resolution via process.execPath.
Description check ✅ Passed The description includes all key template sections: detailed Summary, clear Why/problem statement, comprehensive How to test with specific test results, and Notes with related work.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Apr 13, 2026

Greptile Summary

This PR adds findClaudeInNpmGlobalModules() to derive the @anthropic-ai/claude-code cli.js entrypoint from process.execPath rather than PATH, fixing a Claude Code is not installed error for daemons/spawned processes that inherit a minimal PATH (Windows + nvm-windows, Linux daemons). The new fallback is correctly wired between the PATH-based resolver and the versioned/native-install checks, and both tests pass with proper process.execPath isolation via stubExecPathToEmptySandbox.

Confidence Score: 5/5

Safe to merge — the change is a narrowly scoped fallback resolution step with no effect on the existing happy paths.

No P0 or P1 findings. The two remaining comments are P2 style suggestions (candidate ordering comment, fallback precedence comment). The core logic is correct for all documented npm layouts, tests cover both platforms, and the isolation strategy properly prevents host-environment bleed-through.

No files require special attention.

Important Files Changed

Filename Overview
apps/cli/src/backends/claude/sdk/utils.ts Adds findClaudeInNpmGlobalModules() helper and wires it into getDefaultClaudeCodePathForAgentSdk() as a fallback between PATH resolution and versioned install checks; logic is correct for both Unix and Windows npm layouts.
apps/cli/src/backends/claude/sdk/utils.test.ts Adds stubExecPathToEmptySandbox() isolation helper to both describe blocks and two new tests covering Unix and Windows npm global resolution; restore/isolation lifecycle is correct.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[getDefaultClaudeCodePathForAgentSdk] --> B{HAPPIER_CLAUDE_PATH set?}
    B -- yes --> C[Return override path]
    B -- no --> D[resolveProviderCliCommand via PATH]
    D -- found --> E[Return PATH-resolved command]
    D -- not found --> F["findClaudeInNpmGlobalModules() NEW"]
    F --> G{"Check execDir/node_modules/…\n(Windows layout)"}
    G -- exists --> H[Return npm global cli.js]
    G -- missing --> I{"Check execDir/../lib/node_modules/…\n(Unix layout)"}
    I -- exists --> H
    I -- missing --> J[findLatestVersionedClaudeEntrypointForAgentSdk]
    J -- found --> K[Return versioned install]
    J -- not found --> L[findClaudeInNativeInstallerLocations]
    L -- found --> M[Return native installer path]
    L -- not found --> N[Throw helpful error]
Loading

Reviews (1): Last reviewed commit: "fix(cli-claude): resolve npm global clau..." | Re-trigger Greptile

Comment thread apps/cli/src/backends/claude/sdk/utils.ts
Comment thread apps/cli/src/backends/claude/sdk/utils.ts Outdated
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 the current code and only fix it if needed.

Inline comments:
In `@apps/cli/src/backends/claude/sdk/utils.ts`:
- Around line 331-334: The npm-global fallback currently calls
canonicalizeClaudeEntrypointPath on npmGlobalPath which resolves
symlinks/junctions and pins to a versioned target; instead, when
findClaudeInNpmGlobalModules() returns a path and
isAgentSdkCompatibleClaudeEntrypoint(npmGlobalPath) passes, return npmGlobalPath
directly (preserving symlinks) rather than calling
canonicalizeClaudeEntrypointPath; update the branch that uses
findClaudeInNpmGlobalModules, isAgentSdkCompatibleClaudeEntrypoint, and
canonicalizeClaudeEntrypointPath to remove the canonicalization step so
PATH-style symlink updates remain effective.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9324e062-a6d7-42bd-b215-b9f712a6404a

📥 Commits

Reviewing files that changed from the base of the PR and between a2c9f8b and 7337a38.

📒 Files selected for processing (2)
  • apps/cli/src/backends/claude/sdk/utils.test.ts
  • apps/cli/src/backends/claude/sdk/utils.ts

Comment thread apps/cli/src/backends/claude/sdk/utils.ts Outdated
Addresses three review findings on happier-dev#153:

- CodeRabbit (Major): drop canonicalizeClaudeEntrypointPath() on the
  npm-global fallback path. Canonicalization resolves symlinks and
  junctions (including nvm-windows C:\Program Files\nodejs) to their
  version-pinned targets, which breaks the "preserve symlinks so Claude
  auto-updates can retarget the same path" pattern that the PATH
  resolution branch already follows.

- Greptile (P2): move the npm-global step to run AFTER the Unix
  versioned-install probe, so a user with both a newer
  ~/.local/share/claude/versions/ install and an older npm-global
  install still gets the versioned one.

- Greptile (P2): strengthen the inline comment on the candidate array
  in findClaudeInNpmGlobalModules() to explicitly explain why checking
  the Windows candidate first is safe on Unix (that path layout never
  exists there).

Tests: 15/15 passing. No behavior change in the PATH happy path or in
the HAPPIER_CLAUDE_PATH override branch.
@leeroybrun leeroybrun merged commit 39305c7 into happier-dev:dev Apr 16, 2026
1 check passed
@leeroybrun
Copy link
Copy Markdown
Collaborator

Thank you very much! :) Merged and will be pushed in next dev + preview release

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.

2 participants