Add a Local Gitleaks Pre-Commit Hook (No Frameworks)

Add a Local Gitleaks Pre-Commit Hook (No Frameworks)

~ 5 min read


Why a local .git hook?

Secrets are usually committed by accident: a quick debug key, a copied .env line, or a local token that never should have been pasted. A local .git/hooks/pre-commit hook is the last stop before Git records a commit, so it prevents secrets from ever entering history.

This post shows a minimal setup that uses only files inside .git/, no extra frameworks or repo config. It is fast, easy to remove, and perfect for personal repos or a quick safety net.

What it does not do: .git/hooks/ is not tracked in Git, so every teammate must install it themselves. For team-wide enforcement, pair this with CI (see the last section).

If you want team-shared hooks that live in the repo, use Lefthook instead and commit a lefthook.yml file. A sample config is included below.


Step 1: Install Gitleaks

Pick the method that fits your OS and environment. The official README covers Homebrew, Docker, and building from source, and the releases page has prebuilt binaries.

Releases page:

https://github.com/gitleaks/gitleaks/releases

macOS

Homebrew (officially documented):

brew install gitleaks

Or download the macOS release archive from the releases page and place the gitleaks binary on your PATH.

Windows

Download the Windows .zip from the releases page, extract gitleaks.exe, and add its folder to your PATH.

Linux

Download the Linux release archive, extract gitleaks, and move it to a directory like /usr/local/bin.

If you prefer containers, the official Docker image is documented in the README.

Verify the install:

gitleaks version

Step 2: Create the hook

Gitleaks ships an example pre-commit.py you can copy into .git/hooks/ for a local hook.

Example location in the repo:

https://github.com/gitleaks/gitleaks/blob/master/scripts/pre-commit.py

Copy it into place and make it executable:

cp /path/to/gitleaks/scripts/pre-commit.py .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit

If you want a more robust path that works even when .git/ is a file (like in submodules or worktrees), resolve the hook directory first:

HOOK_DIR="$(git rev-parse --git-dir)/hooks"
cp /path/to/gitleaks/scripts/pre-commit.py "$HOOK_DIR/pre-commit"
chmod +x "$HOOK_DIR/pre-commit"

This python script can disable gitleaks checks with git config set hooks.gitleaks false and re-enable them with either git config set hooks.gitleaks true Or git config unset hooks.gitleaks.

Already have a pre-commit hook? Open it and append the Gitleaks call from the sample script instead of overwriting.

Alternatively, just want something simple? You can go with running the gitleaks command directly in your .git/hooks/pre-commit hook, thus avoiding the python wrapper and ensure the check always runs:

#!/usr/bin/env bash
set -euo pipefail

if ! command -v gitleaks >/dev/null 2>&1; then
  echo "gitleaks is required for pre-commit checks. Install: https://github.com/gitleaks/gitleaks" >&2
  exit 1
fi

gitleaks protect --staged --redact

Option: Team-shared hook with Lefthook

If you want the hook tracked in Git and shared across the team, use Lefthook. Install it:

pnpm add -D lefthook
pnpm exec lefthook install

Add lefthook.yml to the repo:

pre-commit:
    commands:
        gitleaks:
            run: gitleaks protect --staged --redact

If you have a .gitleaks.toml, add --config .gitleaks.toml to the command.


Step 3: Verify it works

Stage a file with a test secret (like a dummy API key), then try to commit to see the hook in action. If the hook is working, the commit is blocked and Gitleaks prints a finding. Remove the test secret afterward.

API_KEY=12345
  • Add a .gitleaks.toml to customize rules for your project.
  • Mirror the scan in CI so server-side checks back up local hooks.
  • If a secret does slip through, rotate it immediately and remove it from history.

This approach is lightweight and fast, but remember it’s local to your machine. If you want team-wide enforcement, pair it with CI.


Troubleshooting

If the hook does not fire, start with these quick checks:

  • Make sure the hook is executable: ls -l .git/hooks/pre-commit should show -rwx.
  • Confirm the hook file is actually being used: git rev-parse --git-dir tells you where Git is reading hooks from.
  • If you are using Lefthook, confirm the hooks are installed: pnpm exec lefthook install should create a .lefthook/ directory and hook scripts.
  • Verify gitleaks is on your PATH inside Git hooks. Hooks often run with a minimal PATH, so use an absolute path in the script if needed (for example, /usr/local/bin/gitleaks or the output of which gitleaks).
  • If you see Python errors, you may be using a system without python on PATH. Run the script with python3 or change the shebang to #!/usr/bin/env python3.

More on gitleaks.toml

The official Gitleaks docs include the config format, examples, and rule/allowlist options.

https://github.com/gitleaks/gitleaks#configuration

Here is a recommended starter config that keeps the default rules, scopes the scan to the Git history, and adds an allowlist for common non-secrets:

title = "gitleaks config"

[extend]
useDefault = true

[allowlist]
description = "Allowlist common non-secrets"
paths = [
    '''^node_modules/''',
    '''^dist/''',
    '''^public/''',
    '''^\.git/''',
]
regexes = [
    '''^-----BEGIN (PUBLIC|PRIVATE) KEY-----$''',
    '''^AKIAIOSFODNN7EXAMPLE$''',
]

If you store test fixtures with fake secrets, add a paths entry for that folder or add a regexes entry that matches the fake value.


Add a GitHub Actions check (team-wide enforcement)

This catches secrets in commits and pull requests, blocking them if any secrets are found. However, it does mean you’ll need to rotate that secret as it made it into the history, which is why it’s better to use a pre-commit hook team wide and then have this as an additional check just in case.

Add a GitHub Actions workflow to run the scan on pushes and pull requests. The official Gitleaks Action docs include a workflow example and document GITLEAKS_CONFIG for custom config paths.

Create .github/workflows/gitleaks.yml:

Minimum viable workflow (no custom config):

name: gitleaks

on:
    push:
        branches: ["**"]
    pull_request:
        branches: ["**"]

permissions:
    contents: read

jobs:
    scan:
        runs-on: ubuntu-latest
        steps:
            - uses: actions/checkout@v4
              with:
                  fetch-depth: 0
            - uses: gitleaks/gitleaks-action@v2
              env:
                  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

If you have a .gitleaks.toml, add GITLEAKS_CONFIG:

name: gitleaks

on:
    push:
        branches: ["**"]
    pull_request:
        branches: ["**"]

permissions:
    contents: read

jobs:
    scan:
        runs-on: ubuntu-latest
        steps:
            - uses: actions/checkout@v4
              with:
                  fetch-depth: 0
            - uses: gitleaks/gitleaks-action@v2
              env:
                  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
                  GITLEAKS_CONFIG: .gitleaks.toml

all posts →