Skip to content

Gitignore and Hooks

Tell Git what never belongs in the repo, and run automatic checks before commit or push. Commands: Git Command Set (e.g. git rm --cached).

See also: Git Command Set · Commit Conventions · Collaboration


How to read this page

  1. Concepts — what .gitignore is; build intuition first
  2. Overview — rules, commands, and hook type tables
  3. Scenarios — problem-driven walkthroughs with examples
  4. Symptom lookup — at the bottom for quick fixes

What .gitignore does

.gitignore is a plain-text rule file in the repository. Matching paths are not picked up by default git add and usually do not show as untracked in git status—unless they were already tracked.

In plain terms: “Git pretends these paths do not exist.”

Effect: Applies to untracked paths (or paths you untrack). It does not remove files already committed from history by itself.


Overview

.gitignore rule cheat sheet

Pattern In plain terms Scenario
foo/ Ignore a directory Rule syntax
*.log Match extension at any depth Rule syntax
/build Repo root only Rule syntax
**/tmp Any depth Rule syntax
!path Un-ignore a path Rule syntax, Keep one file
# Comment Rule syntax
Nested .gitignore Rules stack with parent Monorepo nested
core.excludesfile Machine-wide ignore file Global ignore

Git commands used in this doc

Command / config Job Scenario Command set
git check-ignore -v <path> Show matching rule Nested
git rm --cached Untrack, keep on disk Already tracked git rm
git rm -r --cached Untrack a tree Already tracked git rm
git restore --staged Unstage Secrets staged git restore
git config --global core.excludesfile Global ignore path Global ignore git config
git config core.hooksPath Versioned hooks dir Team hooks git config
git commit --no-verify Skip commit hooks Skip hooks git commit
git push --no-verify Skip push hooks Skip hooks git push
chmod +x .git/hooks/* Make hooks executable pre-commit, etc.

Hook types cheat sheet

Hook When Typical job Scenario
pre-commit Before git commit Lint, format, block large files pre-commit
commit-msg After message, before commit Message format, block trailers commit-msg
pre-push Before git push Tests, branch checks pre-push
post-checkout After branch switch Install deps, clear caches
post-merge After merge npm install, migration hints

Full list: git-scm githooks.

Team hook tooling

Approach In plain terms Scenario
core.hooksPath Version .githooks/ in the repo Team hooks
Husky / Lefthook Register on npm install Team hooks
CI / PR checks Catch --no-verify bypasses Team hooks

Scenario: first .gitignore on a new project

Typical uses:

  • Node / frontend: dependencies, dist, build caches
  • Python: virtualenvs, __pycache__
  • Editors: .idea, .vscode (sometimes keep shared recommendations)
  • OS junk: .DS_Store
# Dependencies and build output
node_modules/
dist/
.next/
out/

# Python
.venv/
**/__pycache__/

# Logs
*.log

# Editor (keep team VS Code extension list)
.vscode/*
!.vscode/extensions.json
.idea/

# macOS
.DS_Store

Note

Commit .gitignore itself so teammates get the same rules after clone.


Scenario: rule syntax that actually works

Pattern Meaning Example
foo/ Ignore directory foo node_modules/
*.log Any .log file in any folder npm-debug.log
/build Only at repo root Does not match src/build
**/tmp tmp at any depth a/b/tmp
!path Negation—un-ignore See below
# Comment
# Local env overrides; keep example in repo
.env
.env.local
.env.*.local

!.env.example

In plain terms: “* matches extensions, leading / anchors to root, ! pulls paths back out of ignore.”


Scenario: nested .gitignore in a monorepo

Common layout: root for global noise, packages/apps for local build artifacts.

repo/
├── .gitignore          # node_modules, .DS_Store
├── apps/web/.gitignore # apps/web/.next
└── packages/api/.gitignore

Effect: Rules stack; deeper files add/refine behavior. When confused, see which rule wins:

git check-ignore -v apps/web/.next/cache/foo

Scenario: global ignore on your machine

Skip .DS_Store in every repo without copying rules:

git config --global core.excludesfile ~/.config/git/ignore

# In ~/.config/git/ignore:
# .DS_Store
# *.swp

In plain terms: “Personal clutter I never want to commit anywhere.”

Effect: Local to your Git config; nothing is shared via the remote unless already tracked.


Scenario: rules exist but files stay tracked

.gitignore does not untrack committed files. Classic mistake: commit dist/, add ignore later—Git still tracks it.

In plain terms: “The file is already on the books; ignore alone is not enough.”

Effect: git rm --cached updates the index only; files stay on disk; next commit stops tracking them on the remote.

# Stop tracking one file, keep on disk
git rm --cached config.local.json

# Whole tree
git rm -r --cached dist/

git status
git commit -m "chore: stop tracking dist and local config"

Warning

After you stop tracking, teammates’ pulls may delete their tracked copies depending on merge outcome. Coordinate in the PR description.


Scenario: secrets or huge files staged by mistake

Typical uses:

  • .env with API keys staged
  • git add . grabbed node_modules or gigabytes of data
# Unstage; working tree unchanged
git restore --staged .env

echo ".env" >> .gitignore

# Not pushed yet: amend or reset (see git-commands reset / amend)
# Already pushed: rotate secrets + history rewrite (BFG / filter-repo)—deleting the file is not enough

Warning

Secrets pushed to a remote are compromised. Rotate credentials first; history cleanup is secondary.


Scenario: ignore a folder except one file

logs/

!logs/
!logs/.gitkeep

In plain terms: “Ignore the whole directory, then ! the one file you need in Git.”


Scenario: monorepo .gitignore patterns

Typical Node + Turbo monorepo (many open templates follow this shape):

node_modules/
dist/
.next/
.turbo/
*.tsbuildinfo

**/src/**/*.js
**/src/**/*.js.map
!**/src/**/*.config.js

.venv/
releases/

Typical uses:

  • Ignore .pnpm-store after local installs
  • Use !packages/foo/generated/ in a subfolder when one package must track generated output

What Git hooks are

Hooks are scripts Git runs before or after actions like commit or push. They live under .git/hooks/ (names like pre-commit, commit-msgno extension, must be executable).

In plain terms: “Automated bouncers at the door of commit and push.”

Effect: Non-zero exit code aborts the Git operation. See Hook types cheat sheet above.


Scenario: lint / format before commit (pre-commit)

Typical uses:

  • ESLint / Prettier on staged files
  • Block debug console.log from landing on main
#!/bin/sh
# .git/hooks/pre-commit
set -e

npm run lint --if-present
# or: pnpm exec tsc --noEmit
chmod +x .git/hooks/pre-commit

When it runs: During git commit, before the commit object is created.

Teams often use Husky or Lefthook to version hook scripts and wire them on npm install instead of hand-copying into .git/hooks.


Scenario: validate commit messages (commit-msg)

Typical uses:

  • Enforce commit conventions (feat:, fix:, etc.)
  • Reject auto-injected Co-authored-by: Cursor ... trailers
#!/bin/sh
# .git/hooks/commit-msg
MSG_FILE="$1"
MSG=$(cat "$MSG_FILE")

echo "$MSG" | grep -q 'Co-authored-by: Cursor' && {
  echo "error: Cursor Co-authored-by trailer is not allowed"
  exit 1
}

head -1 "$MSG_FILE" | grep -q . || {
  echo "error: empty commit subject"
  exit 1
}

exit 0
chmod +x .git/hooks/commit-msg

When it runs: After the message is written, before the commit is finalized.

Note

If the project ships scripts/git-hooks/install.sh (or .ps1), run it once after clone to install shared hooks.


Scenario: tests before push (pre-push)

Typical uses:

  • Block pushes while unit tests fail
  • Quick smoke test before updating a shared branch
#!/bin/sh
# .git/hooks/pre-push
set -e
pnpm test --if-present

When it runs: During git push, before objects are sent to the remote.


Scenario: hook blocked me—skip or fix?

git commit --no-verify -m "hotfix: emergency patch"
git push --no-verify

In plain terms: “The bouncer said no—you used --no-verify to walk past anyway.”

Typical uses:

  • Temporary local WIP (still avoid on shared branches)
  • Hook false positive during an emergency hotfix

Warning

Do not make --no-verify a habit on shared branches. Fix the hook or the code; CI should still catch bypasses on PR.


Scenario: share hooks with the team

.git/hooks is not cloned by default. Common approaches:

  1. core.hooksPath → versioned folder in the repo
    git config core.hooksPath .githooks
    chmod +x .githooks/*
    
  2. Husky / Lefthook → declared in package.json, registered on install
  3. CI as backstop → hooks are local first line; PR checks catch --no-verify
repo/
├── .githooks/
│   ├── pre-commit
│   └── commit-msg
├── .gitignore          # do not ignore .githooks
└── package.json

Quick lookup: symptom → action

Symptom Try first
New file should not be in repo Add to .gitignore → check git status
Ignored but still shows modified Already tracked → git rm --cached
Which rule matched? git check-ignore -v <path>
Commit blocked by hook Read error, fix code; --no-verify only if justified
Teammate has no hooks Document install; enforce in CI
Secret was pushed Rotate credentials + history cleanup

More commands: Git Command Set.