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.

Install-time scripts by package managernpm, pnpm and Yarn classic all run the root package's prepare script after install. Yarn Berry does not run prepare for the root project, but runs postinstall, so Husky must be wired to postinstall there — while avoiding it for published packages.Runs root prepare?Husky wiringnpmyesprepare: huskypnpmyesprepare: huskyYarn classic (v1)yesprepare: huskyYarn Berry (v2+)nopostinstall (private pkgs)Yarn Berry is the one that silently never installs hooks

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
Yarn Berry with Plug'n'PlayAfter yarn install, the private root package's postinstall runs husky to set core.hooksPath. At commit time the hook calls yarn lint-staged; Yarn resolves lint-staged and its tools through the Plug'n'Play map rather than a node_modules directory.yarn installno preparepostinstallhusky (private root)git commit.husky/pre-commityarn lint-stagedPnP resolutioncalling tools by node_modules path fails under PnP — always go through yarn

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
Hooks are not installed — which package manager?With Yarn Berry, prepare never runs, so move husky to postinstall in a private root. With pnpm or Yarn workspaces, check Husky is installed at the root and not only in a package. With npm, check the prepare script exists and that HUSKY is not set to zero.Which package manager is in use?Yarn BerryUse postinstallprepare never runspnpm / Yarn workspacesInstall at root-w / -W flagnpmCheck prepareand HUSKY not 0git config --get core.hooksPath is the one-line check after any install

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.