Git Hooks

By Vishvesh Patel · DevOps EngineerUpdated September 16, 2026

Automate checks at commit and push time. Where hooks live, why they never reach your team, client vs server side, --no-verify, and setting up Husky properly.

What Are Git Hooks?

Git hooks are scripts that run automatically when Git reaches specific events. They help you automate repetitive checks so problems are caught before code is committed or pushed.

Hooks are usually grouped into two categories:

  • client-side hooks, which run on a developer's machine
  • server-side hooks, which run on the Git server when a push is received

Tip: Hooks are best for checks people frequently forget, such as formatting, test commands, or commit message rules.

Client-Side vs Server-Side Hooks

Client-Side Hooks

Client-side hooks run inside a local repository during actions like commit, merge, and push. Common examples include:

  • pre-commit
  • commit-msg
  • pre-push

Server-Side Hooks

Server-side hooks run on a remote repository and can reject pushes or enforce policy. On hosted platforms, CI and branch protection often serve a similar role.

Note: Local hooks improve developer experience, but CI is still the shared source of truth because local hooks can be skipped or may differ across machines.

Three Hooks You Will Use Often

pre-commit

pre-commit runs before the commit is created. If the script exits with a non-zero status, Git stops the commit.

Typical uses:

  • lint checks
  • formatting checks
  • secret scanning
  • basic file validation

Example:

#!/bin/sh
npm run lint
npm run format:check

commit-msg

commit-msg validates the commit message itself. It is the usual place to enforce Conventional Commits.

#!/bin/sh
message_file="$1"

grep -Eq '^(feat|fix|docs|chore): .+' "$message_file" || {
  echo "Commit message must start with feat:, fix:, docs:, or chore:"
  exit 1
}

pre-push

pre-push runs right before Git sends commits to the remote. It is useful for checks that are important but slightly heavier than commit-time checks.

#!/bin/sh
npm test

Where Hooks Live

A repository's built-in hooks live in:

.git/hooks/

Git often creates sample files there, such as pre-commit.sample. To install a hook manually, create a file with the exact hook name and make it executable.

Example:

nano .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit

If the file is not executable, Git ignores it.

Installing Hooks Manually

Here is a small pre-commit hook that runs linting:

cat > .git/hooks/pre-commit <<'HOOK'
#!/bin/sh
npm run lint
HOOK

chmod +x .git/hooks/pre-commit

And a commit-msg hook that blocks non-standard messages:

cat > .git/hooks/commit-msg <<'HOOK'
#!/bin/sh
message_file="$1"

grep -Eq '^(feat|fix|docs|chore): .+' "$message_file" || exit 1
HOOK

chmod +x .git/hooks/commit-msg

The Main Limitation of Raw Hooks

Plain .git/hooks/ scripts are local only. They are not shared automatically when another developer clones the repository. That leads to three common problems:

  • teammates do not get the same hooks
  • hook updates drift over time
  • onboarding becomes manual

Using Husky for Team-Wide Hooks

Husky is a popular solution in JavaScript projects because it stores hook definitions inside the repository.

Basic setup:

npm install --save-dev husky
npx husky init

That creates a .husky/ directory. A typical pre-commit hook might look like this:

#!/bin/sh
. "./_/husky.sh"

npm run lint
npm run format:check

A commit-msg hook can call commitlint:

#!/bin/sh
. "./_/husky.sh"

npx commitlint --edit "$1"

Best Practices

Keep Hooks Fast

Fast hooks get used. Slow hooks get bypassed. Put quick checks in pre-commit and slightly heavier ones in pre-push.

Pair Hooks with CI

Hooks provide local convenience, but CI should still verify important rules centrally.

Write Useful Errors

If a hook blocks work, explain what failed and which command fixes it.

What You Should Remember

Git hooks automate work around commits and pushes. pre-commit is great for lint or format checks, commit-msg enforces message rules, and pre-push can run tests before code is shared. Manual hooks live in .git/hooks/ and need chmod +x, while Husky makes hooks versioned and team-friendly.

Test Your Understanding

Exercise

Exercise 1: Choosing the right hook

Which hook should you use to reject commit messages that do not follow Conventional Commits?

Exercise

Exercise 2: Installing a local hook

What step is required after creating a script in `.git/hooks/pre-commit` so Git can run it?

Exercise

Exercise 3: Sharing hooks with a team

Why do many JavaScript teams use Husky instead of relying only on `.git/hooks/`?


Frequently Asked Questions

What are Git hooks and where do they live?

Hooks are executable scripts Git runs automatically at points in its workflow. Before a commit is created, after a message is written, before a push. They live in .git/hooks, which ships with .sample files you can rename to activate. The crucial consequence of that location is that .git is not version-controlled, so a hook you write is yours alone: it does not travel with a clone and nobody else on the team gets it.

Why don't my Git hooks work for the rest of the team?

Because .git/hooks is outside the repository's tracked content, so hooks are never committed, pushed or cloned. Every developer would have to create them by hand, and nothing keeps them in sync. The standard fix is a manager such as Husky or pre-commit, which stores hook definitions in tracked files and points Git's core.hooksPath at a committed directory during install, so a fresh clone plus npm install sets everything up.

What is the difference between client-side and server-side hooks?

Client-side hooks run on a developer's machine, pre-commit, commit-msg, pre-push, and are about fast feedback: catching a lint error in two seconds rather than two minutes of CI. Server-side hooks such as pre-receive and update run on the remote when a push arrives, and are about enforcement, because they cannot be skipped. Hosted platforms generally do not expose server-side hooks; branch protection rules and required status checks fill that role instead.

Can Git hooks be bypassed?

Yes, trivially, git commit --no-verify skips pre-commit and commit-msg, and git push --no-verify skips pre-push. That is a feature, since you sometimes need to commit through a broken hook, but it means client-side hooks are a convenience and not a control. Anything that genuinely must hold has to be checked where the developer cannot reach it: a required CI check on a pull request, or a server-side hook on self-hosted infrastructure.

What should I put in a pre-commit hook?

Only fast checks, and preferably only on the files being committed. Formatting and linting staged files is the classic use, lint-staged exists specifically for this, plus a scan for accidentally committed secrets. Keep it under a couple of seconds: a pre-commit hook that runs the full test suite gets disabled by the team within a week, and rightly so. Put the slow, comprehensive checks in CI, where they belong.

How do I set up Husky for a JavaScript project?

Install it as a dev dependency, run npx husky init, and it creates a .husky directory and a prepare script so the hooks install automatically after npm install. Add a hook by writing the command into .husky/pre-commit, commonly npx lint-staged, and commit that file. Because .husky is tracked, everyone gets the same hooks on their next install, which is the whole problem raw .git/hooks cannot solve.

Continue Learning

Explore Related Topics

When it breaks

Related Resources