Skip to content

fix: detect Claude CLI installed via npm on non-C drive Windows#2004

Open
octo-patch wants to merge 1 commit intoAndyMik90:developfrom
octo-patch:fix/issue-1922-windows-npm-custom-prefix
Open

fix: detect Claude CLI installed via npm on non-C drive Windows#2004
octo-patch wants to merge 1 commit intoAndyMik90:developfrom
octo-patch:fix/issue-1922-windows-npm-custom-prefix

Conversation

@octo-patch
Copy link
Copy Markdown

@octo-patch octo-patch commented Apr 9, 2026

Fixes #1922

Problem

When Node.js or npm is installed on a non-C drive on Windows (e.g., E:\nodejs), npm.cmd may not appear in the PATH that the Electron app inherits when launched from the GUI. In this scenario, getNpmGlobalPrefix() throws when trying to run npm config get prefix, and falls back only to the hardcoded default %APPDATA%\npm path — missing any custom npm global prefix the user may have configured (e.g., npm config set prefix E:\npm).

This causes Claude CLI (and other tools like GitHub CLI) installed via npm install -g on a non-standard drive to go undetected.

Solution

Add a readNpmrcPrefix() helper that parses the user's ~/.npmrc file for a prefix= line. This is used as a new fallback in both the sync and async variants of getNpmGlobalPrefix() — tried before the hardcoded %APPDATA%\npm default when the npm command itself is unavailable.

The .npmrc file is a simple key-value format that npm writes when users run npm config set prefix <path>. Reading it directly avoids the dependency on npm being in PATH, making detection reliable even when Node.js is installed on a non-standard drive.

Testing

  • Verified the readNpmrcPrefix() function correctly parses prefix=E:\npm from a .npmrc file
  • Verified existing fallback to %APPDATA%\npm is preserved when .npmrc has no prefix line
  • No behavior change when npm is already in PATH (the npm command path takes priority)

Summary by CodeRabbit

  • Bug Fixes
    • Updated npm global prefix discovery on Windows to prioritize user-defined settings from configuration files over default fallback paths.

@CLAassistant
Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.


octo-patch seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account.
You have signed the CLA already but the status is still pending? Let us recheck it.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 9, 2026

📝 Walkthrough

Walkthrough

Updated npm global prefix detection in env-utils.ts to first read a user-defined prefix from ~/.npmrc before falling back to the previous Windows path discovery logic. This allows the system to detect npm installations on non-standard drives.

Changes

Cohort / File(s) Summary
NPM Prefix Resolution
apps/desktop/src/main/env-utils.ts
Added readNpmrcPrefix() helper function to synchronously parse prefix= value from ~/.npmrc. Updated getNpmGlobalPrefix() and getNpmGlobalPrefixAsync() to prioritize .npmrc-derived prefix before applying Windows fallback path discovery.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 On every drive, from C to E and more,
The npmrc whispers what we seek before,
A humble rabbit reads each config line,
And finds the prefix where packages align!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: fixing detection of Claude CLI installed via npm on Windows non-C drives by improving npm prefix detection.
Linked Issues check ✅ Passed The PR directly addresses issue #1922 by adding .npmrc prefix parsing to detect custom npm installations on non-C drives, enabling detection of globally installed Claude CLI.
Out of Scope Changes check ✅ Passed All changes are focused on the npm prefix detection logic in env-utils.ts and directly support the objective of detecting npm installations on non-standard drives.
Docstring Coverage ✅ Passed Docstring coverage is 100.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 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.

Copy link
Copy Markdown
Contributor

@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: 2

🤖 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/desktop/src/main/env-utils.ts`:
- Around line 142-147: The prefix extracted from the .npmrc lines (via the regex
match in the loop that assigns const prefix = match[1].trim()) can include
surrounding quotes (e.g., "E:\npm"); update the extraction logic to strip any
surrounding single or double quotes from match[1] before trimming/returning so
callers like fs.existsSync receive a raw path; ensure you still return null for
empty values. Target the block that computes prefix from
line.match(/^prefix\s*=\s*(.+)$/) and apply a small sanitization step to remove
leading/trailing quotes.
- Around line 428-436: readNpmrcPrefix currently performs sync I/O which blocks
the event loop; add an async variant (e.g., readNpmrcPrefixAsync) that uses
non-blocking fs.promises APIs (or existsAsync + fs.promises.readFile) to read
and parse the .npmrc prefix, then replace the synchronous call in the async flow
with await readNpmrcPrefixAsync() in the same block that sets
npmGlobalPrefixCache and logs the prefix; ensure the new function preserves the
same parsing semantics and returns null/undefined when no prefix is found so the
existing existsAsync and binPath handling remains valid.
🪄 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: ASSERTIVE

Plan: Pro

Run ID: d9024af8-6355-4307-bad6-495ab4ca7658

📥 Commits

Reviewing files that changed from the base of the PR and between cba7a02 and 8dfc2d2.

📒 Files selected for processing (1)
  • apps/desktop/src/main/env-utils.ts

Comment on lines +142 to +147
for (const line of content.split(/\r?\n/)) {
const match = line.match(/^prefix\s*=\s*(.+)$/);
if (match) {
const prefix = match[1].trim();
return prefix || null;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Minor: Quoted prefix values not handled.

If a user's .npmrc contains a quoted value like prefix="E:\npm", the regex captures the quotes, causing fs.existsSync to fail. This is an uncommon edge case but worth considering.

🔧 Proposed fix to strip quotes
       const match = line.match(/^prefix\s*=\s*(.+)$/);
       if (match) {
-        const prefix = match[1].trim();
+        let prefix = match[1].trim();
+        // Strip surrounding quotes if present (e.g., prefix="E:\npm")
+        if ((prefix.startsWith('"') && prefix.endsWith('"')) ||
+            (prefix.startsWith("'") && prefix.endsWith("'"))) {
+          prefix = prefix.slice(1, -1);
+        }
         return prefix || null;
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/env-utils.ts` around lines 142 - 147, The prefix
extracted from the .npmrc lines (via the regex match in the loop that assigns
const prefix = match[1].trim()) can include surrounding quotes (e.g., "E:\npm");
update the extraction logic to strip any surrounding single or double quotes
from match[1] before trimming/returning so callers like fs.existsSync receive a
raw path; ensure you still return null for empty values. Target the block that
computes prefix from line.match(/^prefix\s*=\s*(.+)$/) and apply a small
sanitization step to remove leading/trailing quotes.

Comment on lines +428 to +436
const npmrcPrefix = readNpmrcPrefix(os.homedir());
if (npmrcPrefix) {
const binPath = path.normalize(npmrcPrefix);
if (await existsAsync(binPath)) {
console.warn('[env-utils] Using npm prefix from .npmrc:', binPath);
npmGlobalPrefixCache = binPath;
return binPath;
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Sync I/O in async function contradicts non-blocking design.

readNpmrcPrefix() uses fs.existsSync and fs.readFileSync, blocking the event loop. This contradicts the docstring stating this is "non-blocking" and "Safe to call from Electron main process without blocking the event loop."

While .npmrc files are typically small, consider adding an async variant for consistency.

♻️ Proposed async variant
+/**
+ * Read npm global prefix from .npmrc file asynchronously
+ */
+async function readNpmrcPrefixAsync(homedir: string): Promise<string | null> {
+  const npmrcPath = path.join(homedir, '.npmrc');
+  if (!await existsAsync(npmrcPath)) {
+    return null;
+  }
+
+  try {
+    const content = await fsPromises.readFile(npmrcPath, 'utf-8');
+    for (const line of content.split(/\r?\n/)) {
+      const match = line.match(/^prefix\s*=\s*(.+)$/);
+      if (match) {
+        const prefix = match[1].trim();
+        return prefix || null;
+      }
+    }
+  } catch {
+    // Ignore read errors (permission issues, etc.)
+  }
+
+  return null;
+}

Then update the async fallback:

-        const npmrcPrefix = readNpmrcPrefix(os.homedir());
+        const npmrcPrefix = await readNpmrcPrefixAsync(os.homedir());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/env-utils.ts` around lines 428 - 436, readNpmrcPrefix
currently performs sync I/O which blocks the event loop; add an async variant
(e.g., readNpmrcPrefixAsync) that uses non-blocking fs.promises APIs (or
existsAsync + fs.promises.readFile) to read and parse the .npmrc prefix, then
replace the synchronous call in the async flow with await readNpmrcPrefixAsync()
in the same block that sets npmGlobalPrefixCache and logs the prefix; ensure the
new function preserves the same parsing semantics and returns null/undefined
when no prefix is found so the existing existsAsync and binPath handling remains
valid.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a fallback mechanism for Windows to determine the npm global prefix by reading the .npmrc file, specifically for cases where npm.cmd is not available in the PATH. The review feedback highlights two main areas for improvement: the use of synchronous file reading within an asynchronous function which could block the event loop, and the need for more robust parsing of the .npmrc file to correctly handle quotes, comments, and multiple prefix entries.

}

try {
const content = fs.readFileSync(npmrcPath, 'utf-8');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The readNpmrcPrefix function uses fs.readFileSync, which is a blocking operation. While this is acceptable for the synchronous getNpmGlobalPrefix, it is also called by getNpmGlobalPrefixAsync (line 428). Since getNpmGlobalPrefixAsync is intended to be a non-blocking alternative for the Electron main process, consider using fs.promises.readFile or providing an asynchronous version of this helper to avoid blocking the event loop during app startup.

Comment on lines +142 to +148
for (const line of content.split(/\r?\n/)) {
const match = line.match(/^prefix\s*=\s*(.+)$/);
if (match) {
const prefix = match[1].trim();
return prefix || null;
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The current .npmrc parsing logic is fragile and doesn't follow standard npm configuration rules:

  1. Quotes: npm allows values to be quoted (e.g., prefix="E:\npm"). fs.existsSync will fail if these quotes are not stripped.
  2. Comments: It doesn't ignore lines starting with # or ;, nor does it handle trailing comments on the same line (e.g., prefix=E:\npm # comment).
  3. Precedence: If multiple prefix entries exist in the same file, npm uses the last one. The current code returns the first one found.
    let lastPrefix: string | null = null;
    for (const line of content.split(/\r?\n/)) {
      const trimmedLine = line.trim();
      if (!trimmedLine || trimmedLine.startsWith('#') || trimmedLine.startsWith(';')) {
        continue;
      }
      const match = trimmedLine.match(/^prefix\s*=\s*(.+)$/);
      if (match) {
        let prefix = match[1].split(/[#;]/)[0].trim();
        if ((prefix.startsWith('"') && prefix.endsWith('"')) || (prefix.startsWith("'") && prefix.endsWith("'"))) {
          prefix = prefix.slice(1, -1);
        }
        if (prefix) {
          lastPrefix = prefix;
        }
      }
    }
    return lastPrefix;

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.

claude has installed on windows, not in c drive, in e drive,but can't detect claude cli.

2 participants