Skipping Husky in CI and production installs Jump to heading
Husky installs itself through npmβs prepare lifecycle script, which runs after every npm install and npm ci. On a developer machine that is exactly right. Elsewhere it causes three distinct problems. In CI, installing hooks is pointless, since nobody commits there, and occasionally harmful when a pipeline does commit and hooks fire unexpectedly. In Docker builds and production installs with --omit=dev, Husky is not installed, so prepare fails with husky: not found and the build breaks. And in published packages, consumersβ installs may run your prepare script. This page covers each environment with the smallest reliable fix, within local hook configuration with Husky.
When to use this approach Jump to heading
- Production or Docker builds fail with
sh: husky: not foundduringnpm ci --omit=dev. - CI pipelines that create commits run your local hooks unexpectedly.
- You publish a package to a registry and its
preparescript runs Husky. - You set up Husky recently, as in setting up Husky in a new project, and are preparing it for deployment.
Step 1 β Understand when prepare runs Jump to heading
prepare runs after local npm install and npm ci, before npm publish, and when a package is installed from a Git URL. It does not run when a published package is installed from the registry as a dependency.
Step 2 β Guard the prepare script Jump to heading
The simplest robust fix makes prepare tolerate Husky being absent. This stops production installs failing without changing developer behaviour.
{
"scripts": {
"prepare": "husky || true"
}
} A slightly stricter variant only skips when Husky is genuinely not installed, so a real Husky error on a developer machine is still visible:
{
"scripts": {
"prepare": "node -e \"try { require.resolve('husky') } catch { process.exit(0) }\" && husky || true"
}
} # Verification: a production-style install no longer fails
rm -rf node_modules && npm ci --omit=dev && echo "install ok" Step 3 β Disable hooks in CI with HUSKY=0 Jump to heading
Husky v9 skips installation when the HUSKY environment variable is 0. Set it globally in CI so pipelines never install or run hooks.
# GitHub Actions
env:
HUSKY: 0 # GitLab CI
variables:
HUSKY: "0" HUSKY=0 also disables already-installed hooks at run time, which matters for pipelines that commit β a release job bumping a version, a bot updating a lockfile. Those commits should be checked by CIβs own steps, not by developer hooks that assume a developer environment.
Step 4 β Fix Docker builds Jump to heading
Multi-stage Docker builds often run npm ci twice: once with dev dependencies to build, once without for the runtime image. The second fails without a guard. Besides the guarded script, --ignore-scripts is an option for the runtime stage, as long as no other dependency needs its install scripts.
FROM node:20 AS build
WORKDIR /app
COPY package*.json ./
ENV HUSKY=0
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-slim AS runtime
WORKDIR /app
COPY package*.json ./
ENV HUSKY=0
RUN npm ci --omit=dev # guarded prepare: no failure
COPY /app/dist ./dist
CMD ["node", "dist/server.js"] .git is usually excluded from the build context, so Husky would skip anyway β but only if it is installed. The guard and HUSKY=0 handle both stages explicitly.
Step 5 β Keep Husky out of published packages Jump to heading
For libraries published to a registry, the prepare script runs before npm publish and when someone installs your package from a Git URL. Husky has no place in either. Move it to a script that only runs in development.
{
"scripts": {
"prepare": "husky || true",
"prepublishOnly": "HUSKY=0 npm run build"
},
"files": ["dist/"]
} Because files limits the published contents to dist/, the .husky/ directory never ships. Check what would be published before releasing:
npm pack --dry-run 2>&1 | grep -E '\.husky|prepare' || echo "no hook files in the package" Step 6 β Verify every environment once Jump to heading
After changing the setup, run each install path once so you know it behaves.
HUSKY=0 npm ci && git config --get core.hooksPath || echo "CI: hooks not configured (expected)"
rm -rf node_modules && npm ci --omit=dev && echo "production install: ok"
docker build -t app-test . && echo "docker: ok" Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
Is --ignore-scripts a better fix than guarding prepare? Jump to heading
It skips every lifecycle script, including those that dependencies need to compile native modules. Use it only where you know no dependency relies on install scripts; the guarded prepare is safer as a default.
Will HUSKY=0 in CI skip checks we rely on? Jump to heading
No β it skips local hooks, which CI should not rely on. Every check a hook runs should also be a CI step, which is where enforcement belongs.
What about pnpm and Yarn? Jump to heading
They run prepare similarly, with some differences in workspaces and Yarnβs PlugβnβPlay mode. The specifics are in using Husky with pnpm and Yarn.
Related Jump to heading
- Local Hook Configuration with Husky β the parent topic.
- Disabling Git Hooks Temporarily Without Breaking the Team β the developer-side counterpart.
- Caching Docker Layers in CI β keeping the Docker builds above fast.
- Signing Bot Commits Made by CI Workflows β pipelines that commit, where HUSKY=0 matters.