git hooks
pre-commit
file changes
added files
version control

Git pre-commit hook changed/added files

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

A pre-commit hook runs before Git creates the commit, which makes it the right place to inspect exactly what is staged. The important detail is that hooks should usually work against the index, not the whole working tree. If you only want added or modified files, Git already exposes that information directly.

Read the Staged File List

Inside a pre-commit hook, the safest starting point is git diff --cached. The --cached flag tells Git to compare the staged index to the current HEAD, which means you are checking what will actually be committed.

bash
git diff --cached --name-only --diff-filter=AM

This command means:

  • '--cached reads staged content.'
  • '--name-only prints only file paths.'
  • '--diff-filter=AM limits the result to added and modified files.'

That is usually the correct file set for linters, format checks, and repository policy rules.

Minimal Hook Example

Place a script in .git/hooks/pre-commit and make it executable. This example prints the staged added and modified files, then exits successfully.

bash
1#!/usr/bin/env bash
2set -euo pipefail
3
4files="$(git diff --cached --name-only --diff-filter=AM)"
5
6if [ -z "$files" ]; then
7  exit 0
8fi
9
10echo "Checking staged files:"
11printf '%s\n' "$files"

Then mark it executable:

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

This skeleton is intentionally small. A good hook should be quick, predictable, and easy to debug.

Filter for Relevant File Types

Most teams do not want to run every tool on every file. A better pattern is to filter the staged set and run only the checks that matter for that commit.

bash
1#!/usr/bin/env bash
2set -euo pipefail
3
4python_files="$(git diff --cached --name-only --diff-filter=AM | rg '\.py$' || true)"
5
6if [ -z "$python_files" ]; then
7  exit 0
8fi
9
10echo "Validating staged Python files"
11printf '%s\n' "$python_files" | xargs python -m py_compile

That keeps the hook focused. If no Python files are staged, the hook exits immediately instead of wasting time.

Understand Index Versus Working Tree

One of the most common mistakes is checking on-disk files while the commit actually contains a different staged version. This happens when a developer stages part of a file and keeps more edits unstaged.

If your tool must inspect the exact staged content, read it from the index:

bash
git show :src/example.py

That command prints the version stored in the index, not whatever is currently sitting in the working tree. This distinction matters for strict validation or content-based policies.

Example Policy: Block Large Newly Added Files

Hooks are useful for more than linting. They are also a good place to stop common repository mistakes before they reach history.

bash
1#!/usr/bin/env bash
2set -euo pipefail
3
4while IFS= read -r file; do
5  [ -f "$file" ] || continue
6  size="$(wc -c < "$file")"
7  if [ "$size" -gt 5000000 ]; then
8    echo "Refusing commit: $file is larger than 5 MB"
9    exit 1
10  fi
11done < <(git diff --cached --name-only --diff-filter=A)

This version checks only newly added files, which is often the right policy for blocking accidental binaries or large generated artifacts.

Share Hooks Deliberately

Files inside .git/hooks are local to one clone and are not versioned by default. If the team needs shared hook behavior, keep hook scripts in the repository and point Git at that directory.

bash
git config core.hooksPath .githooks

That makes the setup reproducible and easier to review. It also avoids the common problem where one developer has a hook and another does not.

Common Pitfalls

  • Reading all working tree changes instead of only staged commit content.
  • Forgetting --diff-filter and accidentally processing deleted files.
  • Running heavy checks on every commit and training developers to bypass the hook.
  • Breaking on filenames with spaces because variables are not quoted properly.
  • Assuming a hook in .git/hooks is automatically shared with the rest of the team.

Summary

  • Use git diff --cached --name-only --diff-filter=AM to target staged added and modified files.
  • Build hook logic around the index rather than the whole working tree.
  • Filter by file type to keep the hook fast and relevant.
  • Read staged blobs from the index when exact commit content matters.
  • Version shared hooks explicitly with core.hooksPath if the whole team should use them.

Course illustration
Course illustration

All Rights Reserved.