Summary
prek install-hooks for node-language hooks (e.g. prettier, markdownlint-cli2) reports success but the hook env's bin/ and lib/node_modules/ stay empty. Running the hook then fails with:
error: Failed to run hook `prettier`
caused by: Run command `node hook` failed
caused by: No such file or directory (os error 2)
Root cause
prek's npm invocation sets the uppercase NPM_CONFIG_PREFIX env var but does not strip the lowercase npm_config_prefix / npm_config_global_prefix / npm_config_local_prefix / npm_config_globalconfig from the inherited environment. POSIX env vars are case-sensitive, so both reach npm as distinct keys; npm normalizes them to lowercase during config parsing, and the inherited lowercase entries win over prek's uppercase override.
The result: npm install -g <pkg> installs into whatever the parent shell's npm_config_prefix points at, not into prek's hook env. The npm process exits 0, prek writes .prek-hook.json based only on that exit code, and the next run fails because bin/ is empty. The marker file then makes the failure sticky — subsequent prek runs short-circuit on "already installed" and reproduce the same error even after the polluting env is unset.
npm exec / npx propagate these lowercase vars automatically to child processes, so anything that runs prek as a descendant of an npx/npm invocation reproduces the bug.
Minimal reproduction
mkdir /tmp/prek-repro && cd /tmp/prek-repro
git init -q
cat > prek.toml <<'EOF'
[[repos]]
repo = "https://github.com/rbubley/mirrors-prettier"
rev = "v3.8.3"
hooks = [{ id = "prettier" }]
EOF
echo '{"a":1}' > test.json
git add -A
# Simulate an inherited polluted env (this is what `npx` does to its children):
export npm_config_prefix=/tmp/fake-prefix
export npm_config_global_prefix=/tmp/fake-prefix
export npm_config_local_prefix=/tmp/fake-prefix
mkdir -p /tmp/fake-prefix
prek clean
prek install-hooks # reports success
ls ~/.cache/prek/hooks/node-*/bin/ # empty
ls /tmp/fake-prefix/lib/node_modules/ # prettier installed here
prek run prettier --files test.json
# error: Failed to run hook `prettier`
# caused by: Run command `node hook` failed
# caused by: No such file or directory (os error 2)
Workaround
env -u npm_config_prefix -u npm_config_globalconfig \
-u npm_config_local_prefix -u npm_config_global_prefix \
-u npm_config_cache -u npm_config_userconfig \
prek install-hooks
If already bitten, also prek clean (or rm -rf ~/.cache/prek/hooks/node-*) to drop stale .prek-hook.json markers.
Proposed fix
In both the install and run impls in crates/prek/src/languages/node/node.rs, additionally .env_remove() the lowercase npm config env vars before invoking npm / the hook entry. Minimum set:
npm_config_prefix
npm_config_global_prefix
npm_config_local_prefix
npm_config_globalconfig
npm_config_cache
npm_config_userconfig
A more robust version would scrub every npm_config_* (lowercase) key from the child env — they're an implementation detail of npm exec propagating its config to child scripts, and nothing inside the hook env needs them.
Defensive secondary fix: after npm install returns success, verify env_path/bin/ is non-empty before writing .prek-hook.json. That would surface this class of bug as an install-time error rather than a silent success + cryptic run-time failure.
Willing to submit a PR?
Platform
Darwin 25.4.0 arm64
Version
prek 0.3.13 (Homebrew 2026-05-05)
.pre-commit-config.yaml
(Using prek.toml, not .pre-commit-config.yaml. Minimal config that reproduces:)
[[repos]]
repo = "https://github.com/rbubley/mirrors-prettier"
rev = "v3.8.3"
hooks = [{ id = "prettier" }]
Log file
2026-05-11T17:43:58.909701Z TRACE Executing `/Users/me/.asdf/installs/nodejs/24.13.0/bin/npm install -g --no-progress --no-save --no-fund --no-audit --install-links /Users/me/.cache/prek/repos/80d0aae29b041cec [...]`
2026-05-11T17:43:58.948719Z TRACE Executing `/Users/me/.asdf/installs/nodejs/24.13.0/bin/npm install -g --no-progress --no-save --no-fund --no-audit --install-links /Users/me/.cache/prek/repos/965e662e79e8d127 [...]`
2026-05-11T17:43:59.475736Z DEBUG Installed hook `prettier` in `/Users/me/.cache/prek/hooks/node-XiROg2uNqnXGKGfMB2yX`
2026-05-11T17:44:00.742826Z DEBUG Installed hook `markdownlint-cli2` in `/Users/me/.cache/prek/hooks/node-jLuibkjB0k983KoDePA0`
# ...subsequent `prek run prettier`:
2026-05-11T17:44:00.804224Z DEBUG Found installed environment for hook `prettier` at `/Users/me/.cache/prek/hooks/node-XiROg2uNqnXGKGfMB2yX`
2026-05-11T17:44:00.816487Z TRACE run{hook_id=prettier language=node}: Resolved command: prettier
2026-05-11T17:44:00.816562Z TRACE run{hook_id=prettier language=node}: Executing `cd /Users/me/agent_harness && prettier --write --ignore-unknown <file>`
2026-05-11T17:44:00.818638Z TRACE run{hook_id=prettier language=node}: close time.busy=2.40ms time.idle=30.7µs
error: Failed to run hook `prettier`
caused by: Run command `node hook` failed
caused by: No such file or directory (os error 2)
The "Installed hook" messages above are emitted on npm-exit-0 regardless of whether env_path/bin/ was actually populated; inspecting that directory shows it is empty, while <value of $npm_config_prefix>/lib/node_modules/ contains the package that should have gone into the hook env.
Summary
prek install-hooksfor node-language hooks (e.g.prettier,markdownlint-cli2) reports success but the hook env'sbin/andlib/node_modules/stay empty. Running the hook then fails with:Root cause
prek's npm invocation sets the uppercase
NPM_CONFIG_PREFIXenv var but does not strip the lowercasenpm_config_prefix/npm_config_global_prefix/npm_config_local_prefix/npm_config_globalconfigfrom the inherited environment. POSIX env vars are case-sensitive, so both reach npm as distinct keys; npm normalizes them to lowercase during config parsing, and the inherited lowercase entries win over prek's uppercase override.The result:
npm install -g <pkg>installs into whatever the parent shell'snpm_config_prefixpoints at, not into prek's hook env. The npm process exits 0, prek writes.prek-hook.jsonbased only on that exit code, and the next run fails becausebin/is empty. The marker file then makes the failure sticky — subsequent prek runs short-circuit on "already installed" and reproduce the same error even after the polluting env is unset.npm exec/npxpropagate these lowercase vars automatically to child processes, so anything that runs prek as a descendant of an npx/npm invocation reproduces the bug.Minimal reproduction
Workaround
env -u npm_config_prefix -u npm_config_globalconfig \ -u npm_config_local_prefix -u npm_config_global_prefix \ -u npm_config_cache -u npm_config_userconfig \ prek install-hooksIf already bitten, also
prek clean(orrm -rf ~/.cache/prek/hooks/node-*) to drop stale.prek-hook.jsonmarkers.Proposed fix
In both the
installandrunimpls incrates/prek/src/languages/node/node.rs, additionally.env_remove()the lowercase npm config env vars before invoking npm / the hook entry. Minimum set:npm_config_prefixnpm_config_global_prefixnpm_config_local_prefixnpm_config_globalconfignpm_config_cachenpm_config_userconfigA more robust version would scrub every
npm_config_*(lowercase) key from the child env — they're an implementation detail ofnpm execpropagating its config to child scripts, and nothing inside the hook env needs them.Defensive secondary fix: after
npm installreturns success, verifyenv_path/bin/is non-empty before writing.prek-hook.json. That would surface this class of bug as an install-time error rather than a silent success + cryptic run-time failure.Willing to submit a PR?
Platform
Darwin 25.4.0 arm64
Version
prek 0.3.13 (Homebrew 2026-05-05)
.pre-commit-config.yaml
(Using
prek.toml, not.pre-commit-config.yaml. Minimal config that reproduces:)Log file
The "Installed hook" messages above are emitted on npm-exit-0 regardless of whether
env_path/bin/was actually populated; inspecting that directory shows it is empty, while<value of $npm_config_prefix>/lib/node_modules/contains the package that should have gone into the hook env.