← All articles

Secrets in Pull Requests: How Code Review Accidentally Publishes Your Credentials

June 21, 2026

The Leak Vector Nobody Talks About: Your Pull Request Tab

Most credential-leak conversations start and end with committed files — a .env checked in by accident, a hardcoded key buried in a config. That's a real problem, but it's only half the story. Every day, engineering teams expose secrets through a surface area that sits completely in the open: pull requests, code review comments, and the diff threads attached to them.

Pull requests on GitHub, GitLab, and Bitbucket are, by default, visible to anyone with repository access — and on public repos, they're visible to the entire internet. Even on private repos, the blast radius of a leaked secret in a PR is wider than most teams realize.

Exactly How Secrets End Up in Pull Requests

It helps to be specific. Here are the concrete paths that cause this:

1. Debugging Code Left in a Diff

A developer is chasing a bug in a third-party API integration. They temporarily paste a live API key directly into the source file to rule out auth issues, then push the branch so a teammate can reproduce the problem. The key appears in the diff. Even if the developer removes it in the next commit and the final merged code is clean, the key exists permanently in the PR diff history — queryable, linkable, and indexable.

2. Inline Review Comments Containing Credentials

A reviewer leaves a comment like: "Try replacing the token with ghp_xxxxxxxxxxxxxxxxxxxx — that's the staging key I used when I hit this same endpoint." That comment is now part of the PR thread. Deleting a GitHub comment removes it from the UI, but it does not guarantee removal from GitHub's API response history, notifications, or any third-party integrations (Slack, Jira, email) that already received a webhook payload containing the raw text.

3. Test Fixtures and Snapshot Files

Automated tests sometimes capture API responses as fixtures or snapshots. If a live response body contains a token or a signed URL with embedded credentials, that value lands verbatim in a snapshot file that gets committed to the branch and reviewed in a PR.

4. CI Log Output Linked from a PR

A failing CI run prints an environment variable to stdout during debugging. The logs are public (common on open-source repos), and the PR links directly to them. The secret is one click away from anyone reading the PR.

5. Draft PRs and "WIP" Branches

Draft pull requests feel informal — they're not "real" PRs yet, so the normal mental checklist slips. But a draft PR on a public or shared private repo has exactly the same visibility as a merged one.

Why Deleting the Commit or Comment Isn't Enough

This is the part that surprises most developers. Git history in a PR context is more persistent than it looks:

  • GitHub's REST API exposes review comments and commit diffs independently of what's shown in the UI. A deleted UI comment may still appear in API responses until the platform fully purges it — which is not instantaneous.
  • Webhook consumers (Slack bots, ticketing systems, notification services) receive a payload at event time. That payload is stored by the consumer, permanently, unless you specifically purge it there too.
  • Search engine indexing is a real risk for public repositories. Googlebot and others crawl PR pages. A secret exposed for even a few hours can be cached.
  • Forks created while the secret was visible retain a copy of the diff in their own fork network.

The only safe response to a secret exposed in a PR — however briefly — is to treat it as fully compromised and rotate it immediately.

Concrete Steps to Prevent Secrets in Pull Requests

Step 1: Enforce Pre-Push Secret Scanning Locally

Install a pre-commit hook that scans staged changes before they ever reach the remote. Tools like Gitleaks or TruffleHog can run locally in under a second per commit. Add them to your repo's .pre-commit-config.yaml and document the setup in your onboarding guide — not just your README.

Step 2: Add a PR Status Check for Secret Scanning

Local hooks are bypassed with --no-verify. A required CI status check is not. Configure your pipeline to run secret scanning on every PR diff as a blocking check. The scan should cover the incremental diff, not just the final state of the files — because secrets removed in a later commit still appear in the PR's commit history.

# Example: GitHub Actions step using Gitleaks
- name: Scan PR diff for secrets
  uses: gitleaks/gitleaks-action@v2
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Step 3: Establish a "No Live Credentials in Review Comments" Norm

This sounds obvious, but it needs to be written down. Add a one-liner to your contributing guide or code review checklist: Never paste a real credential into a PR comment, even for debugging. Use a dedicated short-lived test credential and share it through a secrets manager or an encrypted channel. Norms enforced only in people's heads don't survive team growth.

Step 4: Audit CI Log Visibility

Check whether your CI provider exposes logs publicly for your repositories. On GitHub Actions, for public repos, workflow logs are public by default. Review every job that touches secrets and ensure you're using add-mask to redact values:

- name: Mask secret before use
  run: echo "::add-mask::${{ secrets.MY_API_KEY }}"

This prevents the value from appearing in any subsequent log output, even if a downstream step accidentally prints it.

Step 5: Treat Snapshot and Fixture Files as a Credential Surface

Before committing any test fixture or API response snapshot, grep it for patterns that resemble secrets: long alphanumeric strings, Bearer tokens, signed URLs with X-Amz-Signature parameters, JWT-shaped values. Better yet, add these patterns to your secret scanner's configuration so the check runs automatically.

Step 6: Scan Your Entire Repo Periodically, Not Just New Commits

New controls don't retroactively clean up old PRs. A full repository scan — including closed PRs — is the only way to know your current exposure baseline. If you haven't done one recently, run a free GhostCred scan to get a mapped inventory of exposed secrets across your repos in around 60 seconds, with findings tied to SOC 2 and HIPAA control categories so you know what auditors will care about.

When a Secret Is Already Out: The Rotation Checklist

  1. Revoke immediately. Don't investigate first. Revoke the credential at the provider (AWS, Stripe, GitHub, etc.) the moment you confirm exposure. The window between discovery and revocation is your real risk window.
  2. Issue a replacement. Generate a new credential with the minimum required permissions for its actual use case.
  3. Audit usage logs. Check the provider's access logs for the exposed credential. Look for requests from unexpected IPs, at unusual times, or to unusual endpoints.
  4. Document the incident. For SOC 2 or HIPAA environments, you need a record of what was exposed, when it was detected, what access it could have granted, and what you did. A PR comment containing a database password is a potential HIPAA reportable event depending on what that database holds.
  5. Update your controls. Identify which prevention step above was missing and close the gap.

The Mindset Shift: PRs Are Public Documents

The most durable fix isn't a tool — it's treating pull requests the way you treat any public-facing document. You wouldn't paste a production API key into a Confluence page or a Slack channel shared with external users. A pull request on a shared private repo, and especially a public repo, deserves the same discipline. The diff is the document. Review it accordingly.

See what's exposed in your own code.

Run a free scan