Skip to content

fix(resolve): resolve project-local node_modules/.bin tools (fixes exit 127 for hook-rewritten npx on Windows)#2321

Open
danscMax wants to merge 1 commit into
rtk-ai:developfrom
danscMax:fix/resolve-local-node-modules-bin
Open

fix(resolve): resolve project-local node_modules/.bin tools (fixes exit 127 for hook-rewritten npx on Windows)#2321
danscMax wants to merge 1 commit into
rtk-ai:developfrom
danscMax:fix/resolve-local-node-modules-bin

Conversation

@danscMax

@danscMax danscMax commented Jun 8, 2026

Copy link
Copy Markdown

Problem

The agent Bash hook (rtk hook claude / cursor / gemini / copilot) rewrites npx <tool> to rtk <tool>, dropping the npx launcher. RTK then resolves the tool through resolve_binarywhich::which, which only searches the system PATH.

Project-local JS tools — eslint, vite, vitest, tsc, prettier, playwright, … — are installed in ./node_modules/.bin, which is not on the system PATH. So on Windows:

$ rtk eslint --version
rtk: Failed to resolve 'eslint' via PATH, falling back to direct exec: Binary 'eslint' not found on PATH
[rtk: program not found]      # exit 127

…even though npx eslint --version runs fine. npx/npm add node_modules/.bin to PATH at runtime; RTK strips the launcher during the rewrite but never replicated that lookup. The result: every hook-rewritten npx <local-tool> invocation fails with exit 127 for locally-installed tooling — the common case, since running not-globally-installed tools is exactly what npx is for.

Fix

resolve_binary now mirrors npx's resolution order:

  1. nearest node_modules/.bin (cwd walking up to the filesystem root — handles monorepos / nested packages)
  2. then the inherited PATH

…via which::which_in (PATHEXT-aware on Windows; already used in this module's tests). tool_exists uses the same resolution so handlers that branch on it (tsc_cmd, next_cmd, prisma_cmd, …) also detect local tools.

  • Native tools unaffected: git, cargo, npm, etc. aren't in node_modules/.bin, so they fall through to PATH exactly as before — this is an additive change to the search path.
  • Bonus correctness: a project-local tool now takes precedence over a globally-installed shadow of the same name (e.g. a playwright on PATH from another ecosystem), matching npx semantics.

The logic is split into a testable resolve_binary_in(name, cwd, path_var) so resolution can be unit-tested without mutating global process state. Adds tests for local-node_modules/.bin resolution and the not-found path.

Verification (Windows 11)

command before after
rtk eslint --version exit 127 v9.39.4, exit 0
rtk vite --version exit 127 vite/6.4.3, exit 0
npx eslint --version (hook-rewritten → rtk eslint) exit 127 exit 0
rtk git status / rtk npm run / rtk cargo --version ok ok (no regression)

cargo check is clean on develop; the resolution unit tests pass (cargo test resolve_binary).

🤖 Generated with Claude Code

The agent Bash hook rewrites 'npx <tool>' to 'rtk <tool>', dropping the npx launcher. RTK then resolves the tool via which::which, which only searches the system PATH. Project-local JS tools (eslint, vite, vitest, tsc, prettier, playwright) are installed in ./node_modules/.bin and are NOT on PATH, so 'rtk eslint' fails with 'program not found' (exit 127) on Windows even though 'npx eslint' would run fine. npx/npm add node_modules/.bin to PATH at runtime; RTK strips the launcher but never replicated that lookup.

Fix resolve_binary to mirror npx resolution: search the nearest node_modules/.bin (cwd walking up to the filesystem root) first, then the inherited PATH, via which::which_in (PATHEXT-aware). tool_exists uses the same resolution so handlers that branch on it detect local tools. Native tools (git, cargo) are unaffected and fall through to PATH; project-local tools now take precedence over a globally-installed shadow. Adds unit tests for local-bin resolution and the not-found path.

Verified on Windows against v0.42.3: rtk eslint/vite/vitest and the hook-rewritten 'npx eslint' now exit 0; native git/cargo/npm unaffected; resolution unit tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.

1 participant