| | |

Prettier 15 ๐ŸŽจ Pre-commit Hooks โ€” Husky & lint-staged integration

โฑ๏ธ Estimated read: 9 min | |

A pre-commit hook is the last line of defense before unformatted code enters the repository. The editor integration formats on save, and the CI check catches what the editor missed, but both depend on the developer’s environment and the pipeline’s timing. The pre-commit hook runs on the developer’s machine, against the files that are actually staged, before the commit object is created. If the hook fails, the commit does not happen. The combination of Husky (the Git hook manager) and lint-staged (the staged-file runner) is the standard implementation: Husky installs the hook, lint-staged selects the staged files, and Prettier formats them.

This chapter covers the installation and configuration of the Husky and lint-staged pair, the prettier --ignore-unknown flag that prevents the hook from failing on binary files, and the prepare script that makes the hook active for every contributor who clones the repository.

Key point: The pre-commit hook formats the staged files with prettier --write and re-stages them. The commit proceeds with the formatted content, and the developer does not need to remember to run Prettier manually. The --ignore-unknown flag is required because lint-staged passes every staged file to the command, including images and other binaries that Prettier cannot parse.


Why the pre-commit hook exists

The editor bypass problem. A developer can disable format-on-save, or use an editor that is not configured with Prettier, or edit a file through a tool that does not trigger the editor’s save hook. The pre-commit hook runs regardless of the editor. It is the one formatting gate that the developer’s local configuration cannot accidentally disable.

The CI latency problem. The CI check catches unformatted code, but only after the push and the pipeline run. The developer has to wait for the failure, fix the formatting, and push again. The pre-commit hook catches the same problem before the commit, which means the fix is immediate and the CI check passes on the first try.

The staged-file problem. Running prettier --write . before every commit would format the entire repository, including files that are not part of the commit. The formatting changes on the unrelated files would be staged or left in the working tree, and the developer would have to decide what to do with them. lint-staged solves this by passing only the staged files to the command, which keeps the formatting scoped to the commit.


a. The installation and the Husky initialization

The installation adds three packages as dev dependencies: husky, lint-staged, and prettier (if it is not already installed).

npm install --save-dev husky lint-staged prettier

The npx husky init command creates the .husky/ directory and adds a prepare script to package.json .

npx husky init

The prepare script is what makes the hook active for every contributor. When a new developer clones the repository and runs npm install, the prepare script runs automatically, and Husky installs the hooks into the .git/hooks/ directory. The developer does not need to run husky init manually .

The npx husky init command also creates a sample .husky/pre-commit file. The file is replaced with the lint-staged command.

# .husky/pre-commit
npx lint-staged

The hook file does not need a shebang in Husky v9 and later. The file is a POSIX shell script, and the npx lint-staged line is the entire content .


b. The lint-staged configuration

The lint-staged configuration is the mapping from file patterns to commands. The configuration can live in package.json or in a dedicated .lintstagedrc file. The simplest configuration passes every staged file to Prettier with the --write flag and the --ignore-unknown flag.

{
  "*": "prettier --ignore-unknown --write"
}

The * pattern matches every staged file. The prettier --ignore-unknown --write command formats the files that Prettier can parse and skips the files that it cannot. The --ignore-unknown flag is the mechanism that prevents the hook from failing when a .png or a .woff2 file is staged .

A more granular configuration uses file patterns to run different commands on different file types. A project that uses ESLint alongside Prettier runs eslint --fix first, then prettier --write, so that the ESLint fixes are also formatted .

{
  "*.{js,ts,jsx,tsx}": ["eslint --fix", "prettier --write"],
  "*.{css,scss,json,md}": "prettier --write"
}

The order matters. ESLint fixes the code first, and Prettier formats the result. If the order were reversed, the ESLint fixes would not be formatted .


c. The verification and the failure behavior

The hook is verified by staging a file with a formatting violation and running npx lint-staged directly. The command formats the file, re-stages it, and exits zero. The next git commit includes the formatted content.

# Stage a file with a formatting violation
git add src/app.js
npx lint-staged

If Prettier fails to parse a staged file, the hook exits non-zero, and the commit is blocked. The --ignore-unknown flag prevents this for the file types that Prettier does not support, but a syntax error in a file that Prettier does support still blocks the commit. The developer fixes the syntax error and retries the commit.

The hook can be bypassed with git commit --no-verify, which is the escape hatch for the cases where the developer needs to commit a work-in-progress that does not pass the hook. The HUSKY=0 environment variable disables all hooks temporarily .


Complete Example Session

# ============================================
# PART 1: INSTALL THE DEPENDENCIES
# ============================================
npm install --save-dev husky lint-staged prettier
# ============================================
# PART 2: INITIALIZE HUSKY
# ============================================
npx husky init

# Creates .husky/ and adds "prepare": "husky" to package.json.
# ============================================
# PART 3: CREATE THE PRE-COMMIT HOOK
# ============================================
# .husky/pre-commit
npx lint-staged
// ============================================
// PART 4: THE LINT-STAGED CONFIG
// ============================================
{
  "lint-staged": {
    "*": "prettier --ignore-unknown --write"
  }
}
# ============================================
# PART 5: THE VERIFICATION
# ============================================
git add src/app.js
npx lint-staged

# The file is formatted and re-staged.
# ============================================
# PART 6: THE COMMIT
# ============================================
git commit -m "feat: add feature"

# The hook runs lint-staged.
# The formatted file is committed.
// ============================================
// PART 7: THE GRANULAR CONFIG
// ============================================
{
  "lint-staged": {
    "*.{js,ts,jsx,tsx}": ["eslint --fix", "prettier --write"],
    "*.{css,scss,json,md}": "prettier --write"
  }
}
# ============================================
# PART 8: THE PREPARE SCRIPT
# ============================================
# package.json
"prepare": "husky"

# A new clone runs npm install, and the hooks are installed.
# ============================================
# PART 9: THE BYPASS
# ============================================
git commit -m "wip" --no-verify
# or
HUSKY=0 git commit -m "wip"
# ============================================
# PART 10: THE DECISION
# ============================================
# All files โ†’ "*": "prettier --ignore-unknown --write"
# With ESLint โ†’ eslint --fix first, then prettier --write

The ten parts covered the installation, the Husky initialization, the pre-commit hook, the lint-staged config, the verification, the commit, the granular config, the prepare script, the bypass, and the decision.


Quick Reference

The packages

PackagePurpose
huskyInstalls the Git hooks
lint-stagedRuns commands on staged files
prettierThe formatter

The setup commands

CommandEffect
npm install --save-dev husky lint-staged prettierInstall dependencies
npx husky initCreate .husky/ and the prepare script
npx lint-stagedRun the hook manually

The configuration

FileContent
.husky/pre-commitnpx lint-staged
package.json"prepare": "husky"
package.json or .lintstagedrcThe lint-staged mapping

The flags

FlagPurpose
--writeFormat the file in place
--ignore-unknownSkip files Prettier cannot parse
--no-verifyBypass the hook for one commit
HUSKY=0Disable all hooks temporarily

Best Practices

โœ… Do This:

// Use --ignore-unknown in the lint-staged command
"*": "prettier --ignore-unknown --write"                     // โœ…
# Verify the hook manually before committing
npx lint-staged                                               // โœ…
// Run ESLint before Prettier
"*.{js,ts}": ["eslint --fix", "prettier --write"]             // โœ…
# Keep the prepare script so new clones install the hook
"prepare": "husky"                                            // โœ…

โŒ Don’t Do This:

// Omit --ignore-unknown
"*": "prettier --write"                                      // โŒ (fails on images)
# Run prettier on the whole repo in the hook
"*": "prettier --write ."                                    // โŒ (formats unstaged files)
// Run Prettier before ESLint
"*.{js,ts}": ["prettier --write", "eslint --fix"]             // โŒ (ESLint fixes unformatted)

Common Pitfalls

PitfallWhy It HappensFix
Hook fails on a .png file--ignore-unknown not passedAdd the flag
Hook does not run for a new cloneThe prepare script is missingAdd "prepare": "husky"
Unstaged files are formattedThe command targets . instead of the staged filesUse the lint-staged * pattern
ESLint fixes are unformattedPrettier runs before ESLintReverse the order
Hook blocks a WIP commitFormatting violationFix it, or use --no-verify

Real-World Examples

1. The install

npm install --save-dev husky lint-staged prettier

2. The init

npx husky init

3. The hook

# .husky/pre-commit
npx lint-staged

4. The config

{ "lint-staged": { "*": "prettier --ignore-unknown --write" } }

5. The prepare script

{ "prepare": "husky" }

6. The ESLint + Prettier config

{ "lint-staged": { "*.{js,ts}": ["eslint --fix", "prettier --write"] } }

7. The manual verification

npx lint-staged

8. The bypass

git commit --no-verify

9. The disable

HUSKY=0 git commit -m "wip"

10. The decision

All files โ†’ "*": "prettier --ignore-unknown --write"

Visual

The pre-commit flow

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  git commit                                                  โ”‚
โ”‚       โ”‚                                                      โ”‚
โ”‚       โ–ผ                                                      โ”‚
โ”‚  .husky/pre-commit runs                                      โ”‚
โ”‚       โ”‚                                                      โ”‚
โ”‚       โ–ผ                                                      โ”‚
โ”‚  npx lint-staged                                             โ”‚
โ”‚       โ”‚                                                      โ”‚
โ”‚       โ–ผ                                                      โ”‚
โ”‚  Selects staged files                                        โ”‚
โ”‚       โ”‚                                                      โ”‚
โ”‚       โ–ผ                                                      โ”‚
โ”‚  prettier --ignore-unknown --write                           โ”‚
โ”‚       โ”‚                                                      โ”‚
โ”‚       โ–ผ                                                      โ”‚
โ”‚  Re-stages the formatted files                               โ”‚
โ”‚       โ”‚                                                      โ”‚
โ”‚       โ–ผ                                                      โ”‚
โ”‚  Commit proceeds with formatted content                      โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The two flags

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  --write                                                     โ”‚
โ”‚    Formats the file in place.                                โ”‚
โ”‚                                                              โ”‚
โ”‚  --ignore-unknown                                            โ”‚
โ”‚    Skips files Prettier cannot parse (images, etc.).         โ”‚
โ”‚                                                              โ”‚
โ”‚  Both are required in the lint-staged command.               โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The prepare script

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  New clone                                                   โ”‚
โ”‚       โ”‚                                                      โ”‚
โ”‚       โ–ผ                                                      โ”‚
โ”‚  npm install                                                 โ”‚
โ”‚       โ”‚                                                      โ”‚
โ”‚       โ–ผ                                                      โ”‚
โ”‚  "prepare": "husky" runs                                     โ”‚
โ”‚       โ”‚                                                      โ”‚
โ”‚       โ–ผ                                                      โ”‚
โ”‚  Hooks installed in .git/hooks/                              โ”‚
โ”‚       โ”‚                                                      โ”‚
โ”‚       โ–ผ                                                      โ”‚
โ”‚  The pre-commit hook is active                               โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The ESLint and Prettier order

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  eslint --fix     โ†’ fixes the code                           โ”‚
โ”‚       โ”‚                                                      โ”‚
โ”‚       โ–ผ                                                      โ”‚
โ”‚  prettier --write โ†’ formats the fixed code                   โ”‚
โ”‚                                                              โ”‚
โ”‚  The order matters.                                          โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Summary

ItemValue
Hook managerhusky
Staged-file runnerlint-staged
Init commandnpx husky init
Hook file.husky/pre-commit
Hook contentnpx lint-staged
Config locationpackage.json or .lintstagedrc
The commandprettier --ignore-unknown --write
The prepare script"prepare": "husky"
Bypass--no-verify or HUSKY=0

Key takeaways:

  • The pre-commit hook is the formatting gate that the developer’s editor cannot bypass. It runs on the staged files, formats them with Prettier, and re-stages the result before the commit object is created. If the formatting fails, the commit is blocked.
  • Husky installs the hook, and lint-staged selects the files. The npx husky init command creates the .husky/ directory and the prepare script. The .husky/pre-commit file runs npx lint-staged, and the lint-staged configuration maps the file patterns to the Prettier command .
  • The --ignore-unknown flag is required. lint-staged passes every staged file to the command, including images and other binaries. Without the flag, Prettier fails on the first file it cannot parse, and the commit is blocked for a reason that has nothing to do with formatting .
  • The prepare script makes the hook active for every contributor. When a new developer clones the repository and runs npm install, the prepare script runs husky, and the hooks are installed into .git/hooks/. The developer does not need to run husky init manually .
  • The ESLint and Prettier commands run in order. If the project uses both, eslint --fix runs first, and prettier --write formats the result. The reverse order would leave the ESLint fixes unformatted .
  • The hook can be bypassed for a work-in-progress commit. git commit --no-verify skips the hook for one commit, and HUSKY=0 disables all hooks temporarily. The bypass is an escape hatch, not a default .

Remember: The pre-commit hook is the bridge between the editor and CI. The editor formats on save, the CI check catches the misses, and the pre-commit hook catches the files that were staged without a save. Husky installs the hook, lint-staged scopes it to the staged files, and Prettier formats them. The --ignore-unknown flag keeps the hook from failing on binary files, and the prepare script keeps the hook active for every contributor. The result is a repository where the formatted code is the committed code, and the CI check is a confirmation rather than a discovery.

Was this helpful?


Stop using slow, ad-bloated tool sites! ๐Ÿคฎ

๐Ÿ”Ž Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
โœ… Finance (Mortgage, Interest, Inflation)
โœ… Tech (Base64, JSON, Dev Suite, IP)
โœ… Health (BMI, BMR, TDEE)
โœ… Productivity (Timer, Workspace, QR)

โšก๏ธ Fast & Private
๐Ÿ”’ No data leaves your device
๐Ÿ’Ž 100% Free

๐Ÿ”— Use it now: https://tools.kandz.me
๐Ÿ”– Bookmark itโ€”youโ€™ll need it later!