ci: add diff-scoped gitleaks secret scanning - #116
Conversation
This repo had no secret scanning of any kind — no gitleaks/trufflehog workflow, no pre-commit config — while shipping 1Password and Bitwarden credential-bootstrap paths. supply-chain-audit.yml covers attack-pattern indicators only (.pth files, base64+exec) and is pull_request-scoped. The scan is diff-scoped: it never rescans history, so pre-existing placeholder credentials in test fixtures don't fire on unrelated changes. gitleaks is pinned by version and tarball SHA256; checkout is SHA-pinned per the dependency pinning policy. Two things worth flagging: gitleaks' stock ruleset has no Anthropic rule at all — a well-formed sk-ant-api03 canary passes straight through the default config. For an agent framework whose primary job is calling Claude, that is the most likely credential to leak, so anthropic-api-key, anthropic-admin-key and openrouter-api-key are added explicitly. OpenRouter otherwise falls to the entropy-based generic-api-key, which misses a key appearing without a nearby keyword. No committed credentials exist. A full-tree scan surfaced 647 findings in tracked files; all were triaged and all are benign — ~534 in two vendored upstream unsloth reference docs, the rest published OAuth client IDs, gemini-cli's public desktop client secret, Discord and Mattermost IDs in docs examples, and test fixtures. Verification: allowlist tuning takes the tracked tree 647 -> 82, all 82 residual being unmarked placeholders in tests/ that a diff scan never reaches. A canary with seven well-formed provider credentials is caught 7/7. End-to-end against the real CI command: clean commit exits 0, canary commit exits 1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012689txgT12g2hjRczcUZi8
🔎 Lint report:
|
All 13 RELEASE_*.md files landed in one commit (3beff00) — they are upstream NousResearch release notes vendored into this fork, not documents authored here. This fork's 0.15.0 is a local CVE bump, not an upstream release, so there is no missing artifact and authoring one would fabricate an upstream release document. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012689txgT12g2hjRczcUZi8
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ac65468e1d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
|
||
| concurrency: | ||
| group: secret-scan-${{ github.ref }} | ||
| cancel-in-progress: true |
There was a problem hiding this comment.
Preserve every push when cancelling concurrent scans
When two pushes target the same ref in quick succession, this cancels the first run, while the replacement run is bounded below by the second event's BEFORE_SHA..AFTER_SHA. Because gitleaks documents --log-opts as options passed to Git log in its v8.24.3 CLI documentation, the replacement excludes the first push's commits. If that first push introduced a secret and the second leaves it untouched, no completed workflow reports it; queue each push or widen the replacement scan to include the cancelled range.
AGENTS.md reference: AGENTS.md:L336-L339
Useful? React with 👍 / 👎.
| "${SCAN[@]}" --log-opts="${BEFORE_SHA}..${AFTER_SHA}" | ||
| elif git rev-parse HEAD~1 >/dev/null 2>&1; then | ||
| echo "Scanning latest commit (parent SHA unavailable after a rewrite)" | ||
| "${SCAN[@]}" --log-opts="HEAD~1..HEAD" |
There was a problem hiding this comment.
Scan all commits on a newly created branch
When BEFORE_SHA is the all-zero value for a newly created ref, a multi-commit initial push reaches this fallback and HEAD~1..HEAD scans only its final commit. Under gitleaks' documented --log-opts Git-log semantics, a credential introduced in an earlier pushed commit and left unchanged in the tip is therefore missed; use a merge-base or another baseline covering every newly reachable commit.
AGENTS.md reference: AGENTS.md:L336-L339
Useful? React with 👍 / 👎.
| && [ "$BEFORE_SHA" != "0000000000000000000000000000000000000000" ] \ | ||
| && git cat-file -e "${BEFORE_SHA}^{commit}" 2>/dev/null; then | ||
| echo "Scanning push diff: ${BEFORE_SHA}..${AFTER_SHA}" | ||
| "${SCAN[@]}" --log-opts="${BEFORE_SHA}..${AFTER_SHA}" |
There was a problem hiding this comment.
Include merge-commit patches in every pushed range
When a pushed range contains a merge commit, the supplied log options do not enable merge diffs; Git documents the default --diff-merges mode as off, so the merge commit has no patch in the stream gitleaks scans. A credential added only while resolving a merge conflict therefore appears in neither parent commit and is missed even though the merge is inside BEFORE_SHA..AFTER_SHA; include an appropriate --diff-merges mode or separately scan the resulting tree diff.
AGENTS.md reference: AGENTS.md:L336-L339
Useful? React with 👍 / 👎.
| # --- Marked placeholders in tests and docs --- | ||
| # Deliberately requires an explicit marker word on the line. A high-entropy | ||
| # string with no marker still fires, including inside tests/. | ||
| '''(?i)(fake|dummy|placeholder|example|changeme|change-me|not-?a-?real|your[-_](api[-_])?key)''', |
There was a problem hiding this comment.
Scope placeholder markers to the credential they exempt
Because the global allowlist targets the entire line, this unbounded marker regex suppresses every finding whenever any field or comment on that line contains fake, dummy, example, and similar substrings. For example, an actual provider key added to a one-line JSON fixture that also has an unrelated "description": "example response" field is silently ignored, so the exemption is not limited to marked placeholder credentials; couple the marker to the credential assignment or matched value instead.
AGENTS.md reference: AGENTS.md:L348-L350
Useful? React with 👍 / 👎.
Clears the two follow-ups named in #116. .pre-commit-config.yaml runs the same gitleaks against the staged diff, pinned to the upstream repo at a commit SHA per the Dependency Pinning Policy. Opt-in — nothing runs until a contributor runs `pre-commit install`. CI remains the enforcement boundary; this only moves the check to where the remedy is an amend rather than a history rewrite plus a credential rotation. ci-auto-healer.yml drops from */30 to hourly at :23. The workflow_run triggers already fire the moment a watched workflow completes, so the cron is only a backstop for missed completion events. At */30 it ran ~48x/day almost always to print "No failed runs found", which is what buried the real Tests/Lint history in the run list during the scan that prompted this work. Off-minute is deliberate: :00 is GitHub's most contended scheduler slot. Verified the hook rather than just linting it: staged canary of seven well-formed provider credentials exits 1 and reports all seven, including the custom Anthropic and OpenRouter rules — confirming it reads .gitleaks.toml and not the built-in default. Clean tree exits 0. Both edited workflows re-parsed; the pinned rev came from `git ls-remote --tags`, not from memory. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012689txgT12g2hjRczcUZi8
) Clears the two follow-ups named in #116. .pre-commit-config.yaml runs the same gitleaks against the staged diff, pinned to the upstream repo at a commit SHA per the Dependency Pinning Policy. Opt-in — nothing runs until a contributor runs `pre-commit install`. CI remains the enforcement boundary; this only moves the check to where the remedy is an amend rather than a history rewrite plus a credential rotation. ci-auto-healer.yml drops from */30 to hourly at :23. The workflow_run triggers already fire the moment a watched workflow completes, so the cron is only a backstop for missed completion events. At */30 it ran ~48x/day almost always to print "No failed runs found", which is what buried the real Tests/Lint history in the run list during the scan that prompted this work. Off-minute is deliberate: :00 is GitHub's most contended scheduler slot. Verified the hook rather than just linting it: staged canary of seven well-formed provider credentials exits 1 and reports all seven, including the custom Anthropic and OpenRouter rules — confirming it reads .gitleaks.toml and not the built-in default. Clean tree exits 0. Both edited workflows re-parsed; the pinned rev came from `git ls-remote --tags`, not from memory. Claude-Session: https://claude.ai/code/session_012689txgT12g2hjRczcUZi8 Co-authored-by: Claude <noreply@anthropic.com>
What does this PR do?
This repo had no secret scanning of any kind — no gitleaks/trufflehog
workflow, no
.pre-commit-config.yaml— while shipping 1Password and Bitwardencredential-bootstrap paths.
supply-chain-audit.ymlcovers attack-patternindicators only (
.pthfiles, base64+exec) and ispull_request-scoped, sodirect pushes to
mainhad no coverage at all. Surfaced by the 2026-07-25 dailyrepo scan.
The scan is diff-scoped — it never rescans history, so the placeholder
credentials already sitting in test fixtures don't fire on unrelated changes.
That's the design choice that keeps the check worth reading, and it mirrors the
warning already in
supply-chain-audit.yml: a scanner that fires on every PRtrains reviewers to ignore it.
Related Issue
Follow-up from the 2026-07-25 daily repo scan (no tracking issue filed).
Type of Change
Changes Made
.github/workflows/secret-scan.yml— gitleaks on every push and PR.gitleaks pinned by version and tarball SHA256;
actions/checkoutSHA-pinnedper the Dependency Pinning Policy. On failure, prints what to do next.
.gitleaks.toml— custom rules + a narrow, commented allowlist.AGENTS.md— a Secret Scanning section next to the Dependency PinningPolicy. Deliberately not added to
SECURITY.md§2: that section documentsthe runtime trust boundary and is careful about what counts as one. A CI
hygiene check is not a boundary.
docs/system-log/2026-07-27.md— session entry.Two findings worth your attention
1. gitleaks' stock ruleset has no Anthropic rule at all.
A well-formed
sk-ant-api03-…canary passes straight through the defaultconfig. For an agent framework whose primary job is calling Claude, that's the
single most likely credential to leak. Added explicit
anthropic-api-key,anthropic-admin-keyandopenrouter-api-keyrules — OpenRouter otherwisefalls to the entropy-based
generic-api-key, which misses a key appearingwithout a nearby keyword.
I confirmed this is a stock-ruleset gap and not something my allowlist caused:
the Anthropic miss reproduces identically with no config at all.
2. No committed credentials exist — and that's now a real claim.
The 2026-07-25 scan said this based on a narrow pattern grep. A full-tree
gitleaks scan surfaced 647 findings in tracked files the grep missed. All
647 were triaged; all are benign:
Bearer <placeholder>in docs examplesagent/google_oauth.py)-----BEGIN … PRIVATE KEY-----pattern insideagent/redact.py— the module whose job is recognising secretsHow to Test
into any tracked path and commit it, then run the workflow's command:
gitleaks detect --config .gitleaks.toml --redact --exit-code 1 --log-opts="HEAD~1..HEAD".Exits 1.
sensitive. Exits 0, despite 82 pre-existing findings in the tree.
python3 -c "import yaml; yaml.safe_load(open('.github/workflows/secret-scan.yml'))"Verification
residual are unmarked placeholder strings inside
tests/; none fire in CIbecause the scan is diff-scoped. I did not blanket-exempt
tests/— testfixtures are exactly where a real key gets pasted by accident.
OpenAI, GitHub PAT, AWS, Slack-bot and generic 48-char credentials is fully
detected with the config applied.
commit exits 1 with 7 leaks. Canary commit reset; nothing left in the tree.
actual release artifact, not transcribed.
ruff check .clean (no Python touched). The advisory lint bot reports 0 newruff and 0 new
tydiagnostics vs base.Retracting a sibling finding from the same scan
The second commit here corrects the system log. The 2026-07-25 scan reported
"
version = "0.15.0"since 2026-07-02 with noRELEASE_v0.15.0.md; everyrelease from v0.2.0 through v0.14.0 has one" as a gap. It isn't one.
All 13
RELEASE_*.mdfiles arrived in a single commit (3beff00,2026-07-01) — they are upstream NousResearch/hermes-agent release notes
vendored wholesale into this fork, not documents authored here; their contents
cite upstream PR numbers in the NousResearch#21000–NousResearch#26000 range and "215 community
contributors". This fork's
0.15.0is a local bump (5b95b76, "bump versionto 0.15.0 to resolve 3 Dependabot CVEs"), not an upstream release.
So there is no fork-local release process that skipped a step, and authoring a
RELEASE_v0.15.0.mdhere would fabricate an upstream release document. Theoriginal finding pattern-matched on filenames without checking provenance.
No action needed.
Follow-ups not in this PR
.pre-commit-config.yamlrunning the same config would catch leaks beforethey reach a remote, but it changes every contributor's local workflow — your
call.
ci-auto-healer.ymlfires on a*/30cron regardless of whether anything isfailing — ~48 mostly no-op runs/day, which is what made the relevant
Tests/Linthistory hard to read during the scan. Theworkflow_runtrigger already covers the immediate case, so the cron is a safety net that
could run hourly. Left alone: that's a judgement call about CI economics, not
a defect.