Git Hooks for Code Quality: Pre-Commit and Pre-Push Workflows with Husky, lint-staged, and Python Alternatives
GitHuskylint-stagedPre-CommitJavaScriptPythonCI/CD

Git Hooks for Code Quality: Pre-Commit and Pre-Push Workflows with Husky, lint-staged, and Python Alternatives

CCodeNScripts Editorial Team
2026-11-14
7 min read

A practical checklist for Husky, lint-staged, Python pre-commit hooks, monorepos, CI, and fixing Git hooks that stop running.

Use this practical checklist to design Git pre-commit and pre-push hooks that catch code-quality problems without making everyday development unnecessarily slow. It covers Husky, lint-staged, Python pre-commit workflows, native Git hooks, monorepos, CI, and recovery steps when hooks stop running.

Overview

Git hooks are local scripts that run at defined points in a Git workflow. A pre-commit hook runs before Git creates a commit, while a pre-push hook runs before local commits are sent to a remote repository. Used well, they provide fast feedback close to the change that caused a problem.

The central design decision is what belongs in each hook. Put quick, focused checks in pre-commit: formatting, linting changed files, simple type checks, and validation of generated or structured files. Reserve pre-push for broader checks such as a unit-test suite, integration tests, or a build that is too expensive to repeat for every commit. Keep CI as the final shared authority because local hooks can be skipped, disabled, or unavailable in some environments.

There is no single required framework. JavaScript and TypeScript projects commonly combine Husky for Git hook installation with lint-staged for running commands only against staged files. Python projects often use the pre-commit framework, although native hooks and package-manager scripts are also valid. The best option is the one your team can understand, install, and repair.

ApproachBest fitStrengthTrade-off
HuskyNode.js and TypeScript repositoriesConnects Git hooks to package scripts and repository configurationRequires consistent dependency installation and setup
lint-stagedFast checks on changed filesReduces unnecessary lint and format workNot every test or check can safely run on only staged files
pre-commitPython, mixed-language, or multi-tool repositoriesProvides a declarative configuration for reusable hooksAdds another configuration layer to learn
Native Git hooksSmall repositories or tightly controlled environmentsMinimal dependencies and direct behaviorSharing and onboarding require more manual work

Checklist by scenario

Scenario 1: JavaScript or TypeScript with Husky and lint-staged

  1. Confirm the repository layout. Run the commands from the repository root, and verify that the project has a package manifest and a working lint or format command.
  2. Install development dependencies. Add Husky and lint-staged using the package manager already used by the project. Avoid mixing package managers unless the repository documents why.
  3. Enable hook setup for contributors. Use the Husky setup method supported by the version installed in the repository. The setup should create or enable a .husky directory and arrange for hooks to be installed after a normal dependency installation.
  4. Define staged-file commands. A typical package configuration is:
{
  "lint-staged": {
    "*.{js,jsx,ts,tsx}": ["eslint --fix"],
    "*.{json,md,yml,yaml}": ["prettier --write"]
  }
}

Adjust file patterns and commands to match the repository. Do not add --fix automatically if the team prefers hooks to report issues without changing files. If a formatter modifies a file, make sure contributors know whether they should stage the updated result and commit again.

  1. Keep the pre-commit hook short. The hook can call the project’s staged-file runner, for example:
npx lint-staged
  1. Add broader checks separately. A pre-push hook might run the project’s test command, type checker, or build. Choose commands that are reliable from a clean checkout and document any required services or environment variables.

Scenario 2: Python with the pre-commit framework

For Python projects, create a .pre-commit-config.yaml file at the repository root and list the formatters, linters, or validators the project has adopted. A deliberately small starting point might look like this:

repos:
  - repo: https://github.com/psf/black
    rev: <pin-a-reviewed-version>
    hooks:
      - id: black

  - repo: https://github.com/pycqa/flake8
    rev: <pin-a-reviewed-version>
    hooks:
      - id: flake8

Replace placeholder revisions with versions reviewed by the project. Install the framework in the project’s documented development environment, then register the Git hook and run it against the repository before asking contributors to rely on it. Pinning revisions makes behavior easier to reproduce, but those pins should be updated deliberately rather than copied blindly.

For Python repositories using a different formatter or linter, the same checklist applies: verify the tool’s scope, agree on configuration, test it on a clean branch, and make sure the command works in both local development and CI.

Scenario 3: A monorepo or mixed-language repository

  • Identify which files each package owns and avoid running every package’s full suite for every small change.
  • Use staged-file filtering for formatting and linting where supported.
  • Route checks based on changed paths, or call a workspace-aware task runner that already understands dependency boundaries.
  • Define the working directory explicitly when a command assumes a package root.
  • Run cross-package tests in pre-push or CI when they are too slow for pre-commit.

Test the workflow from the repository root and from a package directory if both are common contributor entry points. A hook that works only from one directory creates confusing failures during onboarding.

Scenario 4: A small project that needs no framework

Native Git hooks can be enough for a small repository. Store executable scripts in a documented location, configure Git to use that location if necessary, and explain the setup in the README. This is simple, but it does not automatically solve distribution, permissions, shell portability, or onboarding. Use it when those costs are acceptable and the team can maintain the scripts.

What to double-check

  • Hook installation: Clone the repository into a fresh directory, install dependencies, and confirm that the hook is actually present and executable.
  • Staged-file behavior: Stage a valid file, an invalid file, and a file type that should be ignored. Confirm that only intended files are checked.
  • Partial staging: Test a file where only some changes are staged. The workflow should not silently validate or rewrite unrelated working-tree changes.
  • Exit codes: Introduce a controlled lint or test failure and verify that the commit or push is blocked. Then fix it and confirm that the operation succeeds.
  • Generated files: Decide whether generated output is checked, regenerated, or excluded. Ambiguity here often causes hooks to modify files unexpectedly.
  • Environment assumptions: Check Node, Python, virtual-environment, shell, and operating-system requirements. Prefer repository-local binaries over commands that depend on global installations.
  • Secrets and network access: Hooks should not require production credentials. If a tool downloads resources, document that requirement and provide a useful failure message.
  • CI parity: Run the same core lint, test, and type-check commands in CI. Hooks improve local feedback; they should not be the only enforcement layer.

For related release checks, connect this workflow with focused validation tasks such as JSON Schema validation, OpenAPI contract testing, or the web accessibility testing workflow.

Common mistakes

Putting the entire test suite in pre-commit

A slow hook encourages frustration and bypasses. Start with fast checks, measure the experience on ordinary changes, and move broader tests to pre-push or CI.

Relying on hooks as the only quality gate

Developers can use Git’s bypass options, and automated environments may not install local hooks. Treat hooks as feedback, not as a replacement for protected branches or CI checks appropriate to the project.

Running linters against the whole repository unintentionally

Whole-repository checks can be valid, but they do not belong in every pre-commit workflow. Use lint-staged or an equivalent filter when the objective is to validate the current change.

Ignoring permissions and platform differences

A script may work on one operating system but fail because of executable permissions, shell syntax, path handling, or an unavailable command. Prefer cross-platform package scripts where practical, and test setup on the operating systems your team supports.

Updating tools without testing the hook contract

A formatter, package manager, hook manager, or runtime update can change command names, configuration formats, or installation behavior. Review the complete hook path after upgrades rather than assuming the old setup remains valid.

Hiding the recovery path

Every hook should fail with a readable command and a next step. Contributors should know how to reinstall hooks, run the underlying check directly, inspect Git’s hook configuration, and temporarily bypass a broken local hook only when the team’s policy allows it.

When to revisit

Revisit the workflow before a planning or release cycle, after changing the language runtime or package manager, and whenever the repository adds a formatter, linter, test framework, workspace layout, or new contributor group. Also review it when commits become noticeably slower, hooks start modifying unexpected files, or CI reports failures that local hooks never catch.

Use this short maintenance checklist:

  1. Clone the repository into a clean directory.
  2. Install dependencies using the documented command.
  3. Create one intentional lint or test failure and confirm the correct hook blocks the operation.
  4. Test a partial stage, an ignored file, and a change in each supported package.
  5. Compare local commands with CI commands and remove obsolete configuration.
  6. Update tool versions only after checking their installation and configuration guidance.
  7. Record the setup and recovery steps in the contributor documentation.

A dependable Git hook workflow is small, visible, and proportionate to the project. Keep pre-commit checks fast, use pre-push for meaningful broader validation, and let CI provide the shared final check. That balance helps teams catch errors early without turning ordinary commits into a recurring obstacle.

Related Topics

#Git#Husky#lint-staged#Pre-Commit#JavaScript#Python#CI/CD
C

CodeNScripts Editorial Team

Developer Tools Editor

Senior editor and content strategist. Writing about technology, design, and the future of digital media. Follow along for deep dives into the industry's moving parts.