npm Package "bin" Not Executable - Fix EACCES / Permission Denied on CLI in CI
npm marks a package’s declared bin files executable on install and symlinks them into node_modules/.bin. If a bin script lacks the executable bit - lost in a tarball, a Windows checkout, or a copied tree - invoking it fails with permission denied even though the file is present.
What this error means
A CLI installed as a dependency fails with Permission denied when run from node_modules/.bin, or a local package’s own bin cannot execute. The script exists and is correct; it is just not marked executable.
$ ./node_modules/.bin/my-cli
sh: ./node_modules/.bin/my-cli: Permission denied
# or
npm error code 126
npm error command failed: my-cli (permission denied)Diagnose it: reproduce the CI install locally
Install failures are usually environment drift rather than a broken lockfile: a different package-manager major, a different Node version, or a cache that is being restored from a run with different inputs. Reproduce the CI conditions before changing the lockfile, because regenerating it hides the real cause.
# match the runner exactly, then install from a clean slate
node --version && npm --version
rm -rf node_modules
npm ci --foreground-scripts
# if that succeeds locally but fails in CI, the difference is the cache
# or the package-manager version, not your lockfileCommon causes
The bin file lost its executable bit
Archiving, a Windows-authored file, or copying a tree without preserving mode can strip the +x bit. Without it, the OS refuses to execute the script.
Running a local bin that was never chmod-ed
A package’s own bin script committed without the executable bit fails when run directly, since npm’s bin-linking relies on the file being executable.
How to fix it
Restore the executable bit
Mark the bin file executable, and run via npm/npx so the shebang is honored.
chmod +x node_modules/.bin/my-cli # or the package's bin source
# better: invoke through npm so .bin resolution applies
npx my-cli
# for your own package, commit the bit:
git update-index --chmod=+x bin/my-cli.jsReinstall so npm re-links bins
- Run a clean
npm ciso npm re-creates.binlinks with correct modes. - Ensure the bin file has a correct shebang (
#!/usr/bin/env node). - Avoid copying node_modules in a way that drops file modes.
Verify the fix survives a cold cache
A green run immediately after a fix often proves nothing, because it restored a cache written before the change. Force a cold install once to confirm the fix is real.
# temporarily bust the cache key to prove the fix on a cold runner
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
cache-dependency-path: package-lock.json
# then bump this suffix once, run, and remove it
# key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}-v2How to prevent it
- Commit the executable bit on your package’s bin files.
- Run CLIs via npx / npm scripts, not hard-coded .bin paths.
- Reinstall clean so npm re-links bins with correct modes.