EACCES permission denied на macOS при npm — как починить правильно
Самый невинный сценарий: ставишь глобальный пакет через npm install -g и получаешь:
npm ERR! Error: EACCES: permission denied, mkdir '/usr/local/lib/node_modules/...'Привычный рефлекс — приписать sudo. Этот путь ведёт ровно в ад, который потом неделю выгребаешь. Расскажу, почему так делать не надо, и что делать вместо.
Почему sudo npm install — плохо
Файлы, поставленные с правами root, остаются с правами root. Когда тот же npm потом попытается обновиться, проверить лок-файл, или бинари ускоренно записать — он падает на permission denied уже при чтении. Дальше начинается каскад: ты ставишь под root всё больше, пока в кэшах не появляется куча файлов, до которых ты не можешь дотянуться без sudo.
На моей предыдущей машине я года три держала смесь root- и user-файлов в ~/.npm, и каждое обновление было приключением.
Правильный путь — сделать так, чтобы npm никогда не лез туда, где у пользователя нет прав.
Способ 1: использовать менеджер версий Node
Самый простой и универсальный путь. nvm, fnm, volta ставят Node в твою домашнюю директорию. Глобальные пакеты приземляются туда же, под твоего пользователя — никаких EACCES никогда.
Установка fnm через Homebrew:
brew install fnm
fnm install --lts
fnm default $(fnm current)В ~/.zshrc добавь активацию:
eval "$(fnm env --use-on-cd)"После этого npm install -g typescript ставится в ~/.local/share/fnm/aliases/..., никакого root не требуется.
Способ 2: настроить prefix npm в свой каталог
Если не хочется ставить менеджер версий и текущий node устраивает, можно сказать npm: «ставь всё глобальное в мою домашнюю директорию».
mkdir -p ~/.npm-global
npm config set prefix ~/.npm-globalЗатем добавь ~/.npm-global/bin в PATH через ~/.zshrc:
export PATH="$HOME/.npm-global/bin:$PATH"Перезапусти shell. Теперь npm install -g typescript кладёт бинарник в ~/.npm-global/bin/tsc, и shell его находит без всяких прав.
Способ 3: вообще не ставить пакеты глобально
Я уже несколько лет почти не использую -g. Все нужные тулы — TypeScript, Astro CLI, Drizzle Kit — у меня в devDependencies проекта, и я зову их через npx, pnpm exec или скрипты в package.json:
{
"scripts": {
"build": "tsc -p .",
"db:push": "drizzle-kit push"
},
"devDependencies": {
"typescript": "5.6.0",
"drizzle-kit": "0.27.0"
}
}Плюсы: версия инструмента совпадает с проектом, не нужно глобально что-то ставить, на новой машине достаточно pnpm install — и всё уже доступно.
Если очень нужен глобальный CLI (gh, vercel, expo), я ставлю его через Homebrew, не через npm:
brew install gh
brew install vercel-cliBrew управляет правами и путями за меня.
Восстановление прав, если уже накосячила с sudo
Если в ~/.npm часть файлов с root-владельцем, верни всё себе:
sudo chown -R $(whoami) ~/.npm ~/.config
sudo chown -R $(whoami) /usr/local/lib/node_modules(второй пример — только если ты сама ставила туда глобально и хочешь там всё забрать обратно).
Дальше можно либо переехать в ~/.npm-global (см. способ 2), либо снести Node и поставить через fnm, и стартовать с чистого листа.
Особый случай: брю-Node против системного
На моей сборке какое-то время одновременно были Node от brew (/opt/homebrew/bin/node) и от установщика с nodejs.org (/usr/local/bin/node). PATH перепутывался, npm то ставил пакеты в один префикс, то в другой, ловила EACCES в самых разных местах.
Лекарство — выбрать что-то одно. Я перешла на fnm и снесла оба:
brew uninstall node
# и вручную удалила то, что от установщика nodejs.orgПосле этого жизнь стала проще.
Что делать, если EACCES прилетел не на установку, а на запуск
Иногда EACCES появляется при работе с файлами проекта — например, npm пытается записать в node_modules/.package-lock.json, а у файла стоят чужие права.
Это происходит, если кто-то делал sudo npm install в этом каталоге раньше. Чинится так:
sudo chown -R $(whoami) node_modules
rm -rf node_modules
pnpm install # или npm installЧек-лист
- Не запускать
sudo npm. Никогда. Совсем. - Если есть выбор — ставить Node через
fnm/nvm/voltaили через Homebrew. - Если нужен npm prefix —
~/.npm-globalи PATH к нему. - Глобальные тулы держать минимально, остальное — в
devDependencies. - Если уже накосячила —
chown -R $(whoami)для проблемных директорий.
EACCES — это не баг npm, это сигнал «ты пытаешься писать туда, где у тебя нет прав». Решение — не получать новые права через sudo, а класть npm-пакеты туда, где права уже есть. После правильной начальной настройки EACCES не появляется месяцами.