Skip Husky – A Tiny Git Pre‑Commit Hook with PNPM
Today I realised that pulling in Husky just to run lint
and format
felt like overkill on a small side‑project. I wanted the same guardrails without the extra install time or nested node_modules
. Here's the path I took.
1. Create a lightweight pre-commit
hook
# githooks/pre-commit
#!/bin/sh
# Run the precommit command defined in your package.json
pnpm run precommit
RESULT=$? # capture exit status
[ $RESULT -ne 0 ] && exit 1 # abort commit on failure
exit 0
chmod +x githooks/pre-commit # make it executable
2. Wire it up in package.json
{
"scripts": {
...
"precommit": "pnpm run lint && pnpm run format",
"postinstall": "git config core.hooksPath githooks"
}
}
postinstall
tells Git to use the custom githooks/
folder instead of .git/hooks/
, so the hook comes along when the repo is cloned.
Why PNPM?
Any package manager works, but I’m already on pnpm and its workspace‑level caching keeps CI snappy.
3. CI gotcha 💥 ... and the one‑liner fix
My first pipeline exploded because CI images sometimes omit Git entirely (e.g., alpine‑based containers). git config
naturally fails, which bubbles up as a non‑zero exit code and marks the whole build red.
The quick fix: guard the call with command -v git
and swallow the error.
"postinstall": "command -v git >/dev/null 2>&1 && git config core.hooksPath githooks || true"
Now postinstall
succeeds whether or not Git is present, and local devs still get the hook automatically.
Results
- Zero extra dependencies – no Husky, no lint‑staged.
- Instant feedback – commits abort locally if either linter or formatter fails.
- CI friendly – no Git? no problem.
If your project only needs a single hook, this 6‑line script plus a one‑liner in package.json
may be all you need.
Helpful links