{
  "$type": "site.standard.document",
  "bskyPostRef": {
    "cid": "bafyreig4pumwfgiziwvtwnzje5oxoeepjwi5rtlrc2dekxckzye4rsbwom",
    "uri": "at://did:plc:25rdn5elo5izoxrmtis34zuk/app.bsky.feed.post/3moial2b2j6d2"
  },
  "coverImage": {
    "$type": "blob",
    "ref": {
      "$link": "bafkreicgh52qsqqtjcu2ldf4m6xhpx3rdt5yr5y7b2svrgrnlb2fquz62m"
    },
    "mimeType": "image/webp",
    "size": 187612
  },
  "path": "/rufilboss/secret-scanning-in-ci-what-pre-commit-pull-request-and-main-branch-each-actually-catch-3c55",
  "publishedAt": "2026-06-17T11:32:18.000Z",
  "site": "https://dev.to",
  "tags": [
    "devsecops",
    "githubactions",
    "gitleaks",
    "secrets",
    "Gitleaks configuration",
    "GitHub secret scanning",
    "Removing sensitive data from a repository"
  ],
  "textContent": "A teammate pastes an AWS access key into a PR comment to \"debug quickly.\" Another commits `.env.production` because `.gitignore` was wrong on a new microservice. A third rotates nothing after a contractor laptop compromise because \"we never committed secrets, probably fine.\"\n\nSecret scanners are not interchangeable at every stage of the software development lifecycle (SDLC). Running only on `main` means the secret already lived in git history. Running only locally means it never ran on the machine that mattered. **Layers** matter, and each layer should have a different job.\n\nIn this article, you will implement a practical three-layer secret-scanning model with Gitleaks and GitHub Actions, then verify each layer and handle real incidents without creating scanner fatigue.\n\n**Who this is for:** Engineers owning GitHub Actions security checks for application repos.\n\n**What you'll build:** Pre-commit hook, PR scan, and post-merge history scan with shared allowlist discipline.\n\n**Prerequisites:** Git, GitHub repo, optional Gitleaks binary locally.\n\n##  TL;DR\n\n  * Use three layers with different goals: pre-commit for fast local feedback, pull request as a merge gate, and scheduled default-branch scans for hygiene.\n  * Block merges on PR findings; treat default-branch findings as incidents plus historical cleanup.\n  * Share one `.gitleaks.toml` across local and CI runs, and allowlist only specific safe paths.\n  * Rotation and revocation matter more than history rewrite; deleting git history without credential rotation does not contain compromise.\n\n\n\n##  Why one scanner in one place fails\n\nLayer | Catches | Misses\n---|---|---\nPre-commit | Keys before they enter any remote | Skipped hooks (`--no-verify`), new clones without hooks installed\nPull request | Keys introduced in the diff; blocks merge | Secrets only in comments, wiki, or release assets\nDefault branch/history | Deep scan, scheduled; finds old leaks | Still too late for keys already exfiltrated from an open PR\n\nTreat findings like production incidents at the PR layer; treat main-branch scans as **hygiene and audit evidence**.\n\n##  Layer 1 — Pre-commit (fast feedback)\n\nStart with local feedback so engineers catch leaks before pushing.\nInstall Gitleaks and wire a hook:\n\n\n\n    # .pre-commit-config.yaml\n    repos:\n      - repo: https://github.com/gitleaks/gitleaks\n        rev: v8.21.2\n        hooks:\n          - id: gitleaks\n\n\n\n    pre-commit install\n\n\nDevelopers see failures in under two seconds on staged files. **Allowlist** test fixtures explicitly, never disable rules globally:\n\n\n\n    # .gitleaks.toml (repo root)\n    [allowlist]\n    paths = [\n      '''^tests/fixtures/''',\n      '''^docs/examples/fake-credentials\\.json$'''\n    ]\n\n\n**Rule:** If a path needs a real secret for tests, use a vault-injected env var in CI—not a committed file with \"fake\" in the name and a real-looking key format.\n\n##  Layer 2 — Pull request scan (merge gate)\n\nThe PR layer is your enforcement point. Scan **only the PR diff** so developers are not punished for historical debt on day one:\n\n\n\n    name: Secret scan (PR)\n\n    on:\n      pull_request:\n\n    permissions:\n      contents: read\n      pull-requests: write\n\n    jobs:\n      gitleaks:\n        runs-on: ubuntu-latest\n        steps:\n          - uses: actions/checkout@v4\n            with:\n              fetch-depth: 0\n\n          - name: Run Gitleaks on PR diff\n            uses: gitleaks/gitleaks-action@v2\n            env:\n              GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n            with:\n              config-path: .gitleaks.toml\n\n\nConfigure branch protection: this check is required before the merge.\n\n**Fork PRs:** `GITHUB_TOKEN` from forks is read-only; gitleaks-action still scans the diff but cannot comment with elevated permissions, acceptable for public repos; for private repos, consider org-level secret scanning (GitHub Advanced Security) as a parallel signal.\n\n##  Layer 3 — Main branch and history (hygiene)\n\nThe default-branch layer catches historical debt and supply-chain surprises.\nRun a weekly full-history scan:\n\n\n\n    name: Secret scan (history)\n\n    on:\n      schedule:\n        - cron: \"0 6 * * 1\"\n      workflow_dispatch:\n\n    jobs:\n      gitleaks-history:\n        runs-on: ubuntu-latest\n        steps:\n          - uses: actions/checkout@v4\n            with:\n              fetch-depth: 0\n\n          - uses: gitleaks/gitleaks-action@v2\n            with:\n              config-path: .gitleaks.toml\n              # no PR context: full repo scan\n\n\nWhen this fires on `main`, assume rotation: revoke the credential, purge from history (`git filter-repo` or BFG) only **after** revocation—scrubbing git without rotating the secret fixes nothing.\n\n##  How to verify this works\n\nUse these checks to confirm each layer is doing the right job:\n\n  1. **Pre-commit check:** add a fake test secret string in a staged file and confirm `pre-commit` blocks the commit.\n  2. **PR gate check:** Open a test pull request with the same string and confirm the workflow fails before merge.\n  3. **History scan check:** run `workflow_dispatch` for the history workflow and confirm it scans the full repository.\n  4. **Allowlist check:** Place the same test token in an allowlisted fixture path and confirm scanner behavior matches your policy.\n\n\n\nUse clearly fake values in tests and examples. Never use production-like secrets for scanner testing.\n\n##  Reducing false positives without going blind\n\n  1. **Allowlist paths, not regexes for \"AWS\"** — Broad entropy exceptions hide real keys.\n  2. **Separate example keys:** Use obviously invalid formats (`AKIAFAKE00000000000`) in docs; scanners and humans both win.\n  3. **Custom rules sparingly:** Add org-specific patterns (internal API key prefix) via Gitleaks `[[rules]]`; do not fork the entire ruleset unless you can maintain it.\n  4. **Teach the escape hatch:** If pre-commit blocks a docs change, fix the example, not `SKIP=gitleaks`.\n\n\n\n##  When a secret is found anyway\n\n  1. **Revoke** in the provider (AWS, Stripe, Slack) immediately.\n  2. **Rotate** dependent systems; assume compromise if the repo is public or the PR was open.\n  3. **Purge history** if the secret touched git; coordinate with legal/comms if customer data access was possible.\n  4. **Post-incident:** add a rule or allowlist fix so the same mistake is a one-liner next time.\n\n\n\nPair this scanning pipeline with short-lived cloud credentials (for example, OIDC-based CI federation) so leaked CI identity material has reduced blast radius.\n\n##  When this breaks down\n\n  1. Teams can bypass local hooks, so pre-commit cannot be your enforcement layer by itself.\n  2. Diff-only PR scans do not catch existing secrets already in history, release assets, or external systems.\n  3. Aggressive allowlists can silently reduce coverage if they are not reviewed during security change management.\n  4. Scanner success can create false confidence if incident response, revocation, and rotation are weak.\n\n\n\n##  Summary\n\n  * Use three layers: pre-commit (speed), PR (gate), scheduled main (history).\n  * Share one `.gitleaks.toml`; allowlist paths, not categories of secrets.\n  * Block merge on PR findings; treat main-branch hits as incident + hygiene work.\n  * Rotation beats history rewriting; scanning is detection, not prevention alone.\n\n\n\n##  Further reading\n\n  * Gitleaks configuration\n  * GitHub secret scanning\n  * Removing sensitive data from a repository\n\n",
  "title": "Secret Scanning in CI: What Pre-Commit, Pull Request, and Main Branch Each Actually Catch"
}