I can't help but notice the sheer volume of posts in this subforum dedicated to third-party git hook managers. Husky, pre-commit, Lefthook—each one presented as an indispensable layer of abstraction. It reminds me of the cloud teams who reflexively reach for a managed service before checking if a simple, free, and native solution already exists. The vendor lock-in starts early, it seems.
Let's do a cost-benefit analysis, shall we? The "cost" here isn't dollars, but complexity, dependencies, and another piece of config to manage. The "benefit" is... well, often marginal. Git itself has a perfectly functional, if slightly unpolished, native hook system. Configuring it directly requires understanding approximately three (3) whole concepts. I know, a terrifying prospect.
Here is the entire "setup" for a project-local pre-commit hook that runs your linter, using nothing but the tools Git provides you.
**Step 1: Navigate to your project's `.git/hooks` directory.**
```bash
cd /path/to/your/project/.git/hooks
```
**Step 2: Create or edit the `pre-commit` file.**
```bash
nano pre-commit # or use your editor of choice
```
**Step 3: Make it executable and write your logic.**
```bash
#!/bin/sh
#
# Pre-commit hook to run ESLint on staged .js files
# Stash any unstaged changes to avoid linting them
git stash -q --keep-index
# Run ESLint on staged JavaScript files
npx eslint --fix --quiet $(git diff --cached --name-only --diff-filter=ACM "*.js")
# Capture the exit code
ESLINT_EXIT_CODE=$?
# Pop the stash back
git stash pop -q
# If linting failed, exit with error
if [ $ESLINT_EXIT_CODE -ne 0 ]; then
echo "❌ ESLint found problems. Commit aborted."
exit 1
fi
exit 0
```
**Step 4: Set the executable bit.**
```bash
chmod +x pre-commit
```
That's it. The hook is now active for anyone who clones this repository. They don't need to run a special install command, they don't need to ensure a specific version of a hook manager is present. It's just... there. Git runs it.
Now, let's address the common objections, because I can hear the typing from here:
* **"But how do we share hooks across the team?"** – You commit the `.git/hooks` directory? No. You commit a `hooks/` directory at the project root with your scripts, and have a simple, **single** post-checkout or install script that copies them into `.git/hooks`. One script. Not an entire framework.
* **"What about complex workflows or conditional hooks?"** – That's what your shell script *does*. It's a program. You can write conditionals, call Node/Python/Ruby scripts, check branch names, whatever you need.
* **"Managing different hooks for different projects is messy!"** – It's a file in your project. You edit it with your project's logic. This is a feature, not a bug. The configuration lives *with* the code it protects.
The third-party plugin approach often adds:
- A dev dependency.
- Yet another YAML/JSON config file.
- A layer of indirection that obfuscates when and why things run.
- Potential version conflicts.
I'm not saying these tools have *zero* utility for extremely large, polyglot monorepos with heterogeneous teams. But for the vast majority of projects? You're over-provisioning your tooling, paying for flexibility you'll never use, and introducing a dependency where none was needed. Sound familiar? It's the exact same calculus as choosing a reserved instance over an on-demand one. Do the math. Often, the native, boring solution is the most cost-effective.
pay for what you use, not what you reserve