Using Husky with pnpm and Yarn Jump to heading
Husky’s documentation leads with npm, and most examples assume npx. Teams on pnpm or Yarn hit small but confusing differences. Yarn Berry (v2 and later) does not run a package’s prepare script on install, so hooks are never set up. pnpm workspaces run lifecycle scripts per package, and the hook should be installed once at the root. Yarn’s Plug’n’Play mode has no node_modules/.bin, so hooks that call tools by path fail. And calling npx in a pnpm project can download a different version of a tool than the lockfile pins. This page sets up Husky correctly for each package manager and workspace layout, within local hook configuration with Husky.
When to use this approach Jump to heading
- Your repository uses pnpm, Yarn classic (v1) or Yarn Berry instead of npm.
- Hooks are not installed after a fresh install, or tools in hooks are “not found”.
- You use workspaces and want one set of hooks for the whole repository.
- You are following setting up Husky in a new project on a non-npm project.
Step 1 — Know which install hook each package manager runs Jump to heading
Husky relies on a script that runs after install. Package managers differ in which scripts they run for the root project.
Step 2 — pnpm: install at the workspace root Jump to heading
With pnpm, add Husky as a dev dependency of the workspace root, not of a package, and keep the prepare script there.
pnpm add -D -w husky lint-staged
pnpm exec husky init
cat package.json | grep '"prepare"' # "prepare": "husky" In hooks, run tools with pnpm exec, which uses the version from the lockfile. npx in a pnpm project may resolve a different copy or download one.
# .husky/pre-commit
pnpm exec lint-staged # Verification: after a clean install, hooks are configured
rm -rf node_modules && pnpm install && git config --get core.hooksPath Step 3 — Yarn classic: same as npm, with yarn run Jump to heading
Yarn v1 runs the root prepare script like npm. Use yarn to run tools in hooks so the workspace’s resolution is used.
yarn add -D -W husky lint-staged
npx husky init
printf 'yarn lint-staged\n' > .husky/pre-commit Step 4 — Yarn Berry: wire Husky to postinstall Jump to heading
Yarn Berry does not run prepare for the root project. Use postinstall instead — but only in a private root package, because in a published package postinstall would run on consumers’ machines.
{
"private": true,
"scripts": {
"postinstall": "husky"
}
} If the root package is published, keep it out of postinstall and have contributors run yarn husky once, documented in the contributing guide, or use the pinst approach of disabling postinstall during publishing.
With Plug’n’Play there is no node_modules/.bin; run tools with yarn so Yarn’s resolver finds them.
# .husky/pre-commit
yarn lint-staged Step 5 — Handle workspaces consistently Jump to heading
In every package manager, a monorepo should have exactly one Husky installation at the root and one .husky/ directory, because Git has one hooks path per repository. Packages contribute their own lint-staged configuration rather than their own hooks.
repo/
.husky/pre-commit # pnpm exec lint-staged (one hook)
package.json # husky + lint-staged at the root
packages/web/.lintstagedrc.json
packages/api/.lintstagedrc.json If a package’s own package.json also declares "prepare": "husky" — often left over from before it joined the monorepo — remove it. Multiple installs fight over core.hooksPath. The per-package configuration pattern is in configuring lint-staged in a monorepo.
# Find stray husky installs in workspace packages
grep -l '"prepare": *"husky' packages/*/package.json services/*/package.json 2>/dev/null Step 6 — Pin the package manager for hooks too Jump to heading
Hooks run whatever pnpm or yarn binary is on PATH. If contributors have different major versions, hooks can behave differently. Pin the package manager in package.json and enable Corepack so everyone uses the same one.
{ "packageManager": "[email protected]" } corepack enable
pnpm --version # matches packageManager Step 7 — Migrate an existing project between package managers Jump to heading
Switching a repository from npm to pnpm, or from Yarn classic to Berry, is when hooks most often break unnoticed: the lockfile changes, the install script that ran Husky stops running, and hooks keep working on existing clones because core.hooksPath is already set — until someone clones fresh. Make the hook check part of the migration.
# After switching package managers, test from a truly fresh clone
cd "$(mktemp -d)" && git clone "$REPO_URL" app && cd app
corepack enable && pnpm install # or yarn install
git config --get core.hooksPath || echo "HOOKS NOT INSTALLED — check prepare/postinstall"
grep -rn 'npx ' .husky/ && echo "replace npx with the new package manager's exec command" Update every hook that calls npx to the new package manager’s equivalent in the same pull request as the lockfile change, so the switch is atomic. A clone made before the migration should run git config --unset core.hooksPath and reinstall, which resets it cleanly.
Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
Does npx husky work in a pnpm project? Jump to heading
Usually, but it may resolve Husky outside the lockfile or download it. Use pnpm exec husky so the pinned version is used.
Why do hooks run twice in my Yarn workspace? Jump to heading
Typically because two packages ran husky and one wrote hooks that call the other’s tools, or because a package-level hook script calls the root one. Keep a single .husky/ at the root and remove package-level installs.
What about Bun? Jump to heading
Bun runs the root prepare script on install, so the npm-style setup works. Call tools with bunx or bun run in hooks for consistent resolution.
Related Jump to heading
- Local Hook Configuration with Husky — the parent topic.
- Sharing Husky Hooks Across a Monorepo — the monorepo layout in more depth.
- Skipping Husky in CI and Production Installs — the same scripts outside developer machines.
- Husky Hooks in GUI Clients and IDEs — when the package manager itself is not found.