Skip to content

gitleaks scans every branch, so one false positive on any branch turns hygiene red on every PR #3599

Description

@kyle-sexton

Summary

The hygiene job's secret scan is failing on open PRs for a finding that belongs to a
branch those PRs do not contain. As of this writing that is every PR CI runs, plus main's
next push. The finding itself is a false positive.

Reproduction

.github/workflows/ci.yml:137-144 runs the scan with scan-mode: git. In that mode
gitleaks walks the commits reachable from all refs in the clone, not the checked-out
ref. actions/checkout with fetch-depth: 0 fetches every branch, so each PR's scan
covers unrelated in-flight branches.

Reproduced locally against gitleaks 8.30.1, the version CI installs:

# only origin/main plus the PR branch present
$ gitleaks git --config .gitleaks.toml --redact
INF 1977 commits scanned.
INF no leaks found

# after git fetch origin '+refs/heads/*:refs/remotes/origin/*'
$ gitleaks git --config .gitleaks.toml --redact
INF 2018 commits scanned.
WRN leaks found: 1

Same config, same binary, same checked-out commit. The only variable is which other
branches happen to be in the clone.

The finding

RuleID : generic-api-key
File   : plugins/claude-config/lib/resolve-convention-home.sh
Line   : 248
Commit : f7aec1549dec60407a24a7d8ea720696b7b099af
Branch : chore/retired-conventions-mechanism
Match  : CLAUDE_TOKEN=""

The matched text is an empty shell variable initialisation, next to its sibling on the
line above:

AGENTS_STATE="none"; AGENTS_TOKEN=""
CLAUDE_STATE="none"; CLAUDE_TOKEN=""

These are parser state variables for a file scan. There is no credential here, and the
assigned value is the empty string, so nothing is disclosed. generic-api-key fired on
the identifier alone.

Neither the file nor the line exists on main, which is why main's last push run
(33562372229, f43bd07, 21:40Z) was green and PR runs started failing at 22:12Z: the
commit landed on its branch at 21:47Z, between the two.

Two separate defects

  1. The false positive. CLAUDE_TOKEN="" is a state variable, not a secret.
  2. The blast radius. A finding on any branch fails hygiene on every other branch. A
    contributor sees a red check citing a file their PR never touched, which is the part
    that actually costs time. Defect 1 is a one-line fix on one branch; defect 2 will do
    this again on the next false positive.

Options

For the finding, per .gitleaks.toml's own policy ("Intentional findings use a
repository-owned .gitleaksignore or inline gitleaks:allow comment"), an inline
# gitleaks:allow on that line in the owning branch is the narrower of the two and does
not need a fingerprint that goes stale when the commit is rebased.

For the blast radius, scan-mode: git could be scoped to the range under review
(--log-opts over the PR's own commits) so a PR is judged on its own diff, with
full-history scanning kept on the main-branch push run where it still catches
history rewrites. That is a change to the shared ci-workflows action's contract or its
inputs, so it wants its own decision rather than a drive-by edit.

Not fixed here

Neither fix belongs in an unrelated PR: the false positive is on a branch whose author
should decide the annotation, and the scoping change touches shared CI. Filed so the
red check has an owner instead of being re-diagnosed by whoever hits it next.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions