Suggest an editImprove this articleRefine the answer for “What are Git hooks?”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Git hooks** are scripts that Git runs automatically at specific events (pre-commit, pre-push, etc.) to enforce rules or automate tasks. They live in `.git/hooks/`. Exit code 0 lets the operation continue; anything else aborts it. **Key:** hooks are local-only by default, use Husky to share them across a team.Shown above the full answer for quick recall.Answer (EN)Image**Git hooks** are scripts that Git runs automatically at fixed points in its workflow, like before a commit or before a push, to enforce rules or trigger automation. ## Theory ### TL;DR - Think of hooks as checkpoints at an airport: Git pauses at specific moments, runs your script, and a non-zero exit code blocks the operation. - Hooks live in `.git/hooks/` and only run on your machine. Teammates don't get them when they clone the project. - For local checks under 10 seconds (lint, tests) use hooks. For team-wide enforcement, use CI/CD. - `git commit --no-verify` bypasses pre-commit hooks, so they are not a security layer. - Husky lets you version hooks alongside your code and share them across a team. ### Quick example ```bash # .git/hooks/pre-commit #!/bin/sh # Runs before every commit on this machine if ! npm run lint; then echo "Lint failed - fix before committing" exit 1 # Non-zero exit aborts the commit fi ``` Make it executable first: `chmod +x .git/hooks/pre-commit`. Without that step, Git skips the file with no warning and the commit goes through as if the hook doesn't exist. ### How Git runs hooks Git checks `.git/hooks/` for an executable file named after the event, for example `pre-commit`. When you run `git commit`, Git spawns a shell subprocess to execute that script. Exit code 0 means continue. Anything else aborts the operation. Hooks inherit Git's environment variables like `$GIT_DIR`. The four main phases: - **pre**: runs before the action (`pre-commit`, `pre-push`) and can block it - **during**: modifies the operation in progress (`prepare-commit-msg`, `commit-msg`) - **post**: runs after the action is complete (`post-commit`, `post-merge`) and cannot block anything - **server-side**: runs on the remote (`pre-receive`, `post-receive`) and requires server access ### Client vs server hooks Client hooks sit in `.git/hooks/` on your local machine. Server hooks live on the remote server, for example in a self-hosted GitLab or Gitea instance. GitHub-managed repos don't give you access to server hooks. The practical implication: anyone can bypass client hooks by deleting the file or running `git commit --no-verify`. For actual enforcement, configure checks in GitHub Actions or a CI pipeline instead. ### When to use - Pre-commit linting: catch style issues before they reach the repo - Pre-push tests: run the test suite before the push goes out - Commit message validation: enforce [conventional commits](https://www.conventionalcommits.org/) format via `commit-msg` - Post-merge dependency sync: run `npm install` automatically after pulling changes that modify `package.json` Skip hooks for anything that takes more than 10 seconds locally or that the team must not be able to bypass. ### Sharing hooks with Husky The `.git/` directory is not committed to the repo. That means your hooks don't transfer when a colleague clones the project. Husky fixes this by pointing Git to a versioned `.husky/` directory. ```bash # Install and initialize Husky npm install husky --save-dev npx husky install # Creates .husky/ directory # Add a pre-commit hook npx husky add .husky/pre-commit "npx lint-staged" ``` ```json // .lintstagedrc.json { "*.{js,ts,jsx,tsx}": ["eslint --fix", "git add"] } ``` Husky v9 simplified this further by using `git config core.hooksPath .husky/` directly, dropping the npm lifecycle magic that v4 relied on. ### Common mistakes **Forgetting `chmod +x`:** ```bash # File exists but is not executable .git/hooks/pre-commit ``` Git skips non-executable files with no error message. The commit goes through, nothing runs, and you spend time wondering why the hook isn't working. Fix: `chmod +x .git/hooks/pre-commit`. This gets almost every developer at least once. **Hardcoded paths:** ```bash #!/bin/sh node_modules/.bin/eslint . # Breaks on a fresh clone ``` Use `npx eslint .` or `npm run lint` instead. Hardcoded `node_modules` paths fail right after `git clone` before dependencies are installed. **Trying to abort from a post-hook:** ```bash # post-commit npm run notify-slack || exit 1 # exit 1 has no effect here ``` Post-hooks run after the action is already done. A non-zero exit code there does nothing. To block an operation, use the corresponding pre-hook. **Expecting hooks to run in CI:** GitHub Actions clones the repo fresh and gets no local hooks. Hooks are a local developer tool. Configure the same checks separately in your workflow file if you need them in CI. ### Real-world usage - **Husky + lint-staged** (60k+ GitHub stars): runs ESLint only on staged files, standard in React and Next.js repos - **Lefthook**: YAML-configured hooks, common in Ruby/Rails projects at GitLab - **pre-commit.com framework**: multi-language hook management, used at Airbnb and in the dask project - **commit-msg hook**: validates conventional commit format before the message is saved ### Follow-up questions **Q:** How do you bypass a hook in an emergency? **A:** Run `git commit --no-verify`. This skips `pre-commit` and `commit-msg` hooks. Useful for WIP commits or when a broken hook is blocking the whole team. **Q:** What is the difference between `pre-commit` and `commit-msg` hooks? **A:** `pre-commit` runs before Git opens the editor for the commit message. `commit-msg` runs after you write the message but before the commit is saved. Use `pre-commit` to validate code, `commit-msg` to validate message format. **Q:** Why don't hooks run in CI pipelines? **A:** CI runners clone the repo fresh. The `.git/hooks/` directory is local-only and not in version control. Hooks only exist on machines where someone explicitly set them up. **Q:** How does Husky v9 differ from v4? **A:** Husky v4 stored config in `package.json` and installed hooks via npm lifecycle scripts. v9 uses `git config core.hooksPath .husky/` directly and requires an explicit `husky install` call. The setup is more transparent and easier to debug. **Q:** If both `pre-commit` and `prepare-commit-msg` hooks exist, which runs first? **A:** Git runs them in a fixed order: `pre-commit`, then `prepare-commit-msg`, then `commit-msg`, then `post-commit`. You cannot change this sequence. If both hooks modify the same state, you need to account for the order manually or use a tool like Lefthook to coordinate them. ## Examples ### Basic: lint only staged files ```bash #!/bin/sh # .git/hooks/pre-commit (chmod +x required) # Checks only staged JS files, not the entire project changed_files=$(git diff --cached --name-only --diff-filter=ACM | grep '\.js$') for file in $changed_files; do npx eslint "$file" || exit 1 done ``` This is faster than `eslint .` on large repos because it only checks files staged for this commit. If any file fails, the loop exits with code 1 and Git aborts. On a clean project with no staged JS files, the loop runs zero times and exits 0. ### Intermediate: Husky + lint-staged in a Next.js project ```bash # .husky/pre-commit #!/bin/sh . "$(dirname "$0")/_/husky.sh" npx lint-staged ``` ```json // .lintstagedrc.json { "*.{js,ts,jsx,tsx}": ["eslint --fix", "git add"], "*.{css,scss}": ["stylelint --fix", "git add"] } ``` `lint-staged` auto-fixes issues and re-stages the corrected files. If a file can't be fixed automatically, the commit aborts. The `.husky/pre-commit` file is tracked by Git, so every developer who clones the repo and runs `npm install` gets the same checks automatically, because the `prepare` script triggers `husky install`.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.