Back to Blog

An Understated Script for Cleaner Commits

An Understated Script for Cleaner Commits

I once spent twenty minutes waiting for a Jenkins pipeline to finish, only to realize I’d left a stray console.log in a production file. It’s that specific brand of developer shame—the "Red X" on a PR for a trivial formatting error—that makes you realize how much time we waste on things that should be automated locally.

We often treat CI (Continuous Integration) as our primary safety net. But CI is slow, expensive, and public. If you’re waiting for a remote server to tell you that you forgot a semicolon, your feedback loop is broken.

The Power of the .git/hooks Directory

Hidden inside every .git folder in every project you own is a hooks directory. It’s full of .sample files that most people never open. These are scripts that Git executes when certain actions happen—like committing, pushing, or merging.

The most valuable one for your daily sanity is the pre-commit hook.

By using a simple shell script, you can force Git to check your work before it ever lets you finish the git commit command. If the script fails, the commit is aborted. No mess, no broken CI, no spinach in your teeth.

A Script That Actually Works

Many developers reach for heavy tools like Husky or specialized npm packages to manage hooks. Those are fine, but sometimes you just want a raw, zero-dependency script that gets the job done.

Here is a bash script I use to lint and format only the files that are currently staged. Checking the whole project is too slow; we only care about what’s changing.

Save this as .git/hooks/pre-commit and make it executable with chmod +x .git/hooks/pre-commit.

#!/bin/sh

# Get the list of files staged for commit
# We filter for added (A), copied (C), modified (M), or renamed (R) files
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACMR | grep -E '\.(js|ts|jsx|tsx)$')

# If no relevant files are staged, just exit quietly
if [ -z "$STAGED_FILES" ]; then
  exit 0
fi

echo "🔍 Running linter on staged files..."

# Run your linter/formatter here. 
# In this example, we use ESLint.
# The --fix flag handles the easy stuff automatically.
npx eslint $STAGED_FILES --fix

# If the linter found errors it couldn't fix, it exits with a non-zero code
if [ $? -ne 0 ]; then
  echo "❌ Linting failed. Fix the errors and try again."
  exit 1
fi

# Crucial step: Re-add the files in case the linter modified them
git add $STAGED_FILES

echo "✅ Clean as a whistle. Committing..."
exit 0

Why this specific approach?

1. `git diff --cached --name-only`: This is the magic phrase. It looks only at the files in the "Staging Area." It doesn't care about the 50 other files you have modified in your working directory that you aren't ready to commit yet.
2. `--diff-filter=ACMR`: This prevents the script from breaking when you delete a file. Trying to lint a file that no longer exists is a quick way to make your hook crash.
3. The "Re-add" step: If your linter (like ESLint or Prettier) automatically fixes a trailing comma, that change is made to the file on disk. You need to git add it again within the script so that the fix actually makes it into the commit.

Sharing is the Hard Part

The biggest "gotcha" with Git hooks is that the .git folder isn't versioned. If you write a great hook and push your code, your teammates won't have it.

I usually handle this by keeping my hooks in a directory called .githooks in the project root. Then, I add a simple setup script or a line in the README telling contributors to run:

git config core.hooksPath .githooks

This tells Git to look in your versioned folder instead of the default .git/hooks.

When You Need to Break the Rules

Sometimes you’re in a hurry. Maybe you’re committing a "Work in Progress" and you know the code is a mess, but you need to switch branches. You don't want the linter yelling at you.

You can bypass the hook by adding the --no-verify (or -n) flag:

git commit -m "wip: switching laptops" --no-verify

It’s a trapdoor. Use it sparingly, but it’s there when the automation becomes a hindrance rather than a help.

Focus on the Feedback Loop

The goal isn't to add more chores to your workflow. The goal is to move the "failure point" as close to the "typing point" as possible. When the feedback loop is measured in milliseconds on your local machine rather than minutes on a CI server, you stay in the flow longer.

That's the real value of a boring shell script. It keeps you focused on the code that matters, rather than the formatting that doesn't.