How-To Guide

Setting Up Pre-Commit Security Hooks

Catch secrets, vulnerable patterns, and misconfigurations before they reach your repository with pre-commit hooks that developers will actually keep enabled.

James
Senior DevOps Engineer
6 min read

Pre-commit hooks are the fastest feedback loop in your security toolchain. A developer writes code, attempts to commit, and gets immediate feedback about security issues before the code ever reaches the repository. No waiting for CI. No context-switching back to fix something flagged 20 minutes later. Immediate, local, and fast.

The catch is that developers will disable hooks that are slow, noisy, or annoying. So the goal is building a hook suite that catches real issues quickly and quietly.

Why Pre-Commit Hooks Matter for Security

The earlier you catch a security issue, the cheaper it is to fix. A hardcoded AWS key caught at commit time takes 30 seconds to fix. The same key caught by a scanner in CI takes a few minutes. That key discovered in a public repository by an automated scraper takes hours of incident response, credential rotation, and audit.

Pre-commit hooks are particularly effective for:

  • Secret detection - The highest-value pre-commit check. Catches credentials before they enter git history.
  • Dockerfile linting - Catches insecure patterns like running as root or missing version pins.
  • IaC scanning - Flags misconfigured Terraform, CloudFormation, or Kubernetes manifests.
  • Dangerous function detection - Catches use of eval(), shell execution with user input, and similar patterns.

Setting Up the Framework

The pre-commit framework (pre-commit.com) is the standard tool. It manages hooks from multiple sources, handles installation, and updates cleanly.

Install it:

pip install pre-commit

Create .pre-commit-config.yaml in your repository root:

repos:
  # Secret detection
  - repo: https://github.com/Yelp/detect-secrets
    rev: v1.4.0
    hooks:
      - id: detect-secrets
        args: ['--baseline', '.secrets.baseline']

  # Private key detection
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.5.0
    hooks:
      - id: detect-private-key
      - id: check-added-large-files
        args: ['--maxkb=1000']
      - id: check-merge-conflict

  # Dockerfile linting
  - repo: https://github.com/hadolint/hadolint
    rev: v2.12.0
    hooks:
      - id: hadolint-docker

  # Terraform security
  - repo: https://github.com/antonbabenko/pre-commit-terraform
    rev: v1.83.5
    hooks:
      - id: terraform_tfsec

  # YAML/JSON validation
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.5.0
    hooks:
      - id: check-yaml
      - id: check-json
      - id: pretty-format-json
        args: ['--autofix', '--no-sort-keys']

Install the hooks:

pre-commit install

Now every git commit runs these checks automatically.

Secret Detection in Detail

Secret detection is the single most impactful pre-commit hook. Here is how to configure detect-secrets properly.

Initial Baseline

Generate a baseline of existing secrets (you will need to address these separately):

detect-secrets scan > .secrets.baseline

Review the baseline and mark known false positives:

detect-secrets audit .secrets.baseline

The baseline file ensures that existing findings do not block every commit. Only new secrets trigger the hook.

Custom Patterns

Add patterns specific to your organization:

# .detect-secrets.yaml
plugins_used:
  - name: ArtifactoryDetector
  - name: AWSKeyDetector
  - name: AzureStorageKeyDetector
  - name: BasicAuthDetector
  - name: CloudantDetector
  - name: GitHubTokenDetector
  - name: HexHighEntropyString
    hex_limit: 3
  - name: IbmCloudIamDetector
  - name: JwtTokenDetector
  - name: KeywordDetector
    keyword_exclude: ''
  - name: MailchimpDetector
  - name: NpmDetector
  - name: PrivateKeyDetector
  - name: SendGridDetector
  - name: SlackDetector
  - name: SoftlayerDetector
  - name: StripeDetector
  - name: TwilioKeyDetector

Handling False Positives

When a detection is a false positive, add an inline comment:

SECRET_PATTERN = "sk_test_.*"  # pragma: allowlist secret

Or add the file path to the baseline exclusion:

detect-secrets scan --exclude-files 'tests/fixtures/.*' > .secrets.baseline

Dockerfile Security Hooks

Hadolint catches common Dockerfile anti-patterns that have security implications:

# Hadolint will flag these:
FROM ubuntu          # DL3006: Always tag the version
RUN apt-get update   # DL3009: Delete apt-get lists after install
USER root            # DL3002: Last user should not be root

Configure a .hadolint.yaml for your standards:

ignored:
  - DL3008  # Pin versions in apt get install (can be noisy)
trustedRegistries:
  - docker.io
  - gcr.io
  - your-registry.com

Infrastructure as Code Scanning

For Terraform, tfsec catches security misconfigurations before they are applied:

# tfsec will flag this:
resource "aws_s3_bucket" "data" {
  bucket = "my-data-bucket"
  # Missing: encryption, logging, public access block
}

For Kubernetes manifests, use kubesec:

- repo: https://github.com/controlplaneio/kubesec
  rev: v2.13.0
  hooks:
    - id: kubesec

Performance Optimization

The biggest risk to hook adoption is speed. Here is how to keep things fast.

Only scan changed files. Most hooks automatically only check staged files. Verify this is the case for each hook.

Skip hooks on non-relevant files. Use the types and files filters:

- id: hadolint-docker
  files: Dockerfile
  types: [file]

- id: terraform_tfsec
  files: \.tf$
  types: [file]

Use local caching. Pre-commit caches hook environments. The first run is slow; subsequent runs are fast.

Set a time budget. Total pre-commit execution should stay under 10 seconds. If it exceeds that consistently, developers will start using --no-verify.

Measure your hook performance:

time pre-commit run --all-files

If a specific hook is too slow, move it to CI instead.

Team Adoption Strategy

Make installation automatic. Add a setup script to your repository:

#!/bin/bash
# setup.sh
pip install pre-commit
pre-commit install
echo "Pre-commit hooks installed successfully."

Reference it in your contributing guide and onboarding documentation.

Do not block on informational findings. Configure hooks with appropriate exit codes. Hooks that block commits should have near-zero false positives. Hooks that are informational should warn but not prevent the commit.

Provide escape hatches. Sometimes developers need to commit despite a finding (a test fixture that looks like a secret, for example). Document the --no-verify flag but also provide the proper way to handle false positives (allowlists, baseline updates).

Track adoption. You cannot force pre-commit hook installation on developer machines, but you can verify in CI:

- name: Verify pre-commit hooks would pass
  run: pre-commit run --all-files

This catches issues from developers who skipped hooks locally.

Maintenance

Update hook versions regularly. Run monthly:

pre-commit autoupdate

Review the secrets baseline quarterly. Old secrets in the baseline should be rotated and removed.

Collect feedback from developers. If a hook consistently produces false positives, fix the configuration or remove the hook. A hook that developers disable is worse than no hook at all because it creates a false sense of security.

How Safeguard.sh Helps

Safeguard.sh complements pre-commit hooks by providing the server-side verification layer. While hooks catch issues locally before commits, Safeguard.sh validates the same checks in your CI/CD pipeline to ensure nothing slips through when hooks are bypassed. It provides the organizational visibility to know which projects have dependency vulnerabilities, regardless of whether individual developers have their local hooks configured. Together, pre-commit hooks and Safeguard.sh create a defense-in-depth approach where local checks catch issues early and platform checks ensure nothing reaches production unchecked.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.