Skip to content

Node hook install ignores NPM_CONFIG_PREFIX when shell env has lowercase variant #2073

@duanyutong

Description

@duanyutong

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?

  • Yes — I'm willing to open a PR to fix this.

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions