How to Run Secrets Detection Automation with GitGuardian's Native Tools
Six lines in a pre-commit config stop the next credential from ever reaching your history:
repos:
- repo: https://github.com/gitguardian/ggshield
rev: v1.34.0
hooks:
- id: ggshield
stages: [pre-commit]
That block is cheap insurance for everything committed from today forward. It does nothing for the backlog: the hundreds of incidents that landed before anyone wired it up. In part one of this series we covered why that backlog exists. Env files, debug commits, notebooks and CI logs keep leaking credentials, deletion doesn't remediate anything once a secret is pushed, and validity checking is what separates a live key from dead noise.
This part is the hands-on half: secrets detection automation built entirely on GitGuardian's own tooling. You'll wire ggshield into pre-commit and CI, drive the incidents workflow, prioritize by validity and severity, query the GitGuardian API with curl, and plant honeytokens as tripwires. Nothing beyond a GitGuardian account and an API key. We'll go tool by tool, work one live incident end to end by hand, then name the ceiling every DIY setup hits.
01Laptop
Pre-commit hook
ggshield blocks the staged diff before the secret enters history.
02Pipeline
CI scan
The net nobody can skip with --no-verify. Scans the full push range.
03Queue
Incidents and API
Every hit becomes an owned incident, filtered by validity and severity.
04Perimeter
Honeytokens
Decoy keys that fire only when someone acts on stolen access.
Tool 1: ggshield, shift detection left
ggshield is GitGuardian's CLI. Install it, authenticate once, and it scans locally against the same secrets engine the platform uses (GitGuardian docs).
pip install ggshield
ggshield auth login
Pre-commit: block the leak before it's a commit
Add the hook block from the top of this article to .pre-commit-config.yaml, then activate it:
pip install pre-commit
pre-commit install
Now every git commit runs the staged diff through GitGuardian first. A matched secret fails the commit with the file, line and detector name. This is the cheapest remediation there is, because there's nothing to remediate: the secret never enters history.
If you'd rather not depend on the pre-commit framework, install the hook directly:
ggshield install --mode local -t pre-commit
How to read it. Pre-commit is developer-side and skippable (git commit --no-verify), so treat it as the first net, not the only one. It also can't see what's already committed, only the diff in front of it. That's why you also scan in CI, where nobody can bypass it.
CI: the net nobody can skip
In CI, scan the full push range so a secret buried in an intermediate commit still surfaces. A GitHub Actions job:
name: gitguardian
on: [push, pull_request]
jobs:
scanning:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: GitGuardian/ggshield-action@v1
env:
GITHUB_PUSH_BEFORE_SHA: ${{ github.event.before }}
GITHUB_PUSH_BASE_SHA: ${{ github.event.base }}
GITHUB_PULL_BASE_SHA: ${{ github.event.pull_request.base.sha }}
GITHUB_DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
GITGUARDIAN_API_KEY: ${{ secrets.GITGUARDIAN_API_KEY }}
The equivalent bare command, for GitLab or any other runner:
ggshield secret scan ci
To scan a repository's whole history in one pass (the right move when you first onboard a repo):
ggshield secret scan repo .
How to read it. fetch-depth: 0 matters: a shallow clone hides the commits where secrets usually hide. The full-history scan is your backlog discovery pass. Run it once per repo, and every hit becomes a tracked incident you work in the next tool.
Tool 2: The incidents workflow, turning detections into owned work
Every secret GitGuardian finds becomes an incident on the dashboard (Internal Monitoring → Incidents → Secrets). An unmanaged incident list is just anxiety. The workflow is what turns it into progress.
Each incident carries a status you drive explicitly:
- Assign it to the engineer who owns the service the secret belongs to. Unassigned incidents are the ones that rot.
- Resolve it once the credential is revoked and rotated, not merely deleted from the file.
- Ignore it only with a reason:
test_credential,false_positiveorlow_risk. The reason is the audit trail. A silent ignore looks exactly like neglect.
Prioritize by validity, then severity. GitGuardian tests detected credentials against the provider and tags each incident's validity as valid, invalid or unknown. A valid secret is a live key an attacker can use right now, so it jumps the queue. Within the valid set, sort by severity, which GitGuardian derives from the secret type and the permissions it likely grants (a cloud provider root key outranks a read-only third-party token). Work valid-and-critical first. Everything else is backlog you schedule, not a fire you fight.
For recurring, low-judgment patterns, set playbooks (Settings → Playbooks) to act automatically. For example, auto-resolve an incident when GitGuardian confirms the credential has become invalid, or notify a Slack channel the moment a valid incident of critical severity appears. Auto-resolve on invalidity is safe because an invalid key is no longer a live risk. Auto-resolving valid incidents is something you never want to configure.
Tool 3: The GitGuardian API, querying incidents at scale
The dashboard is fine for a handful of incidents. The API is how you triage hundreds. Everything the UI shows is available over REST (API reference). Create a personal access token (API → Personal access tokens) with incidents:read, then:
export GG_TOKEN="your_token_here"
List the open incidents, newest first:
curl -s -H "Authorization: Token $GG_TOKEN" \
"https://api.gitguardian.com/v1/incidents/secrets?status=TRIGGERED&ordering=-date&per_page=50"
The triage query that matters: valid credentials at critical severity, the ones you work today.
curl -s -H "Authorization: Token $GG_TOKEN" \
"https://api.gitguardian.com/v1/incidents/secrets?validity=valid&severity=critical&status=TRIGGERED"
Pipe it through jq to get a scannable worklist instead of a wall of JSON:
curl -s -H "Authorization: Token $GG_TOKEN" \
"https://api.gitguardian.com/v1/incidents/secrets?validity=valid&status=TRIGGERED" \
| jq -r '.[] | [.id, .severity, .detector.name, .gitguardian_url] | @tsv'
Drive workflow actions the same way (these need incidents:write). Assign an incident, then resolve it once you've rotated the key:
# Assign incident 12345 to an owner by email
curl -s -X POST -H "Authorization: Token $GG_TOKEN" \
-H "Content-Type: application/json" \
-d '{"email":"owner@yourco.com"}' \
"https://api.gitguardian.com/v1/incidents/secrets/12345/assign"
# Resolve it after revoke and rotate
curl -s -X POST -H "Authorization: Token $GG_TOKEN" \
-H "Content-Type: application/json" \
-d '{"secret_revoked":true}' \
"https://api.gitguardian.com/v1/incidents/secrets/12345/resolve"
How to read it. The validity and severity filters do the real work: they collapse a 400-incident list into the five that can hurt you before lunch. Wire the valid-and-critical query into a scheduled job and page on a non-empty result, and you have a live-key alarm built from two curl calls. The write endpoints let you script bulk cleanup (mass-ignore a batch of confirmed test credentials with a reason), but the part of remediation that changes anything lives outside GitGuardian. More on that below.
Tool 4: Honeytokens, tripwires for the breach you can't see
Detection tells you a secret leaked. A honeytoken tells you someone is using leaked access. It's a decoy AWS credential GitGuardian generates that grants nothing, but alerts the instant anyone tries to authenticate with it (honeytoken docs).
Create one from the API:
curl -s -X POST -H "Authorization: Token $GG_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"ci-deploy-decoy","type":"AWS","description":"planted in build config"}' \
"https://api.gitguardian.com/v1/honeytokens"
Placement is the whole strategy. A honeytoken only fires if an intruder finds it while poking around, so plant it where a real credential would plausibly live and where a legitimate engineer would never call it:
- In a
.env.example, a Terraform variables file or a CI config, the spots an attacker scrapes first. - Inside a private repo you want to know is breached the moment the wrong person clones it.
- In an internal wiki page or S3 bucket that stores "deployment credentials."
A honeytoken trip is a different category from a detection incident. A detection means a secret might be exposed. A trip means someone acted on stolen access. Route trips to an immediate page, not the backlog queue. The alert carries the source IP and user agent of whoever used it, which is your first evidence of the breach.
| Tool | Catches | Misses |
|---|---|---|
| Pre-commit hook | New secrets in the staged diff | Anything already committed; anyone using --no-verify |
| CI scan | Every pushed commit in the range | Secrets that never touch a scanned repo |
| Incidents and API | The full backlog, sorted by validity and severity | What the key can reach in your account |
| Honeytokens | Active use of stolen access | Leaks nobody has exploited yet |
Working one valid incident by hand
The tools above get you to a short list. Here's what happens next, for one incident, with nothing but native tooling. The worklist query returns a critical AWS key, valid, committed to a service repo six days ago.
There are three readings, and each one predicts something different:
- A sandbox key nobody uses. The principal lives in a dev account,
get-access-key-last-usedshows nothing recent, and revoking it breaks nothing. - A live production key, not yet abused. The principal is a real service user, recent use comes only from your own infrastructure's IPs, and revoking it will break that service until the replacement lands.
- A key someone else found. Recent use comes from an address you don't recognize, or from API calls your service never makes.
Check identity and last use from an admin session:
aws iam get-access-key-last-used --access-key-id AKIAEXAMPLE123456789
Then pull what the key has called since the commit, from CloudTrail's 90-day event history:
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=AccessKeyId,AttributeValue=AKIAEXAMPLE123456789 \
--start-time 2026-10-01T00:00:00Z \
--query 'Events[].{time:EventTime,call:EventName,source:EventSource}'
Say the calls are all GetObject from your own NAT gateway: reading two, a production key in active legitimate use. The response is then a sequence that crosses three systems. Issue a second key for the IAM user, write it into the secrets manager, redeploy the consumers, deactivate the leaked key, confirm nothing broke, delete it, and resolve the incident in GitGuardian with secret_revoked: true:
aws iam create-access-key --user-name billing-export-svc
aws secretsmanager put-secret-value --secret-id prod/billing-export/aws --secret-string file://new-key.json
# redeploy the consumers, then:
aws iam update-access-key --user-name billing-export-svc \
--access-key-id AKIAEXAMPLE123456789 --status Inactive
Each step is easy. Doing them in order, for the right principal, without breaking the service, while the rest of the queue waits, is the slow part.
The honest part: where native tooling stops
01Automated
Detection
Hooks, CI and repo scans find the secret reliably and fast.
02Automated
Routing
Playbooks notify, assign and auto-resolve keys that are already invalid.
03Still manual
Response
Blast radius, revoke, rotate, redeploy and verify all happen outside GitGuardian.
Wire all four tools together and you have strong secrets detection automation: leaks blocked at commit, a CI net nobody can skip, a prioritized incident queue, scriptable triage, and tripwires for active abuse. Run it. But know the ceiling before you call it a program.
Detection and workflow are automated. Remediation isn't. GitGuardian tells you a valid AWS key leaked, who should own it and how bad it is. It can't revoke that key in your AWS account, mint a replacement, push the new value into your secrets manager, redeploy the services that read it, and confirm the old one is dead. That revoke, rotate, redeploy and verify loop is the part that closes the exposure, and it's a human sprint across your cloud console, your secrets store and your deploy pipeline, once per incident.
The queue moves at human speed. Playbooks auto-resolve invalidated keys and route alerts, but a valid critical incident still waits on a person to pick it up, trace which account the key opens, and do the rotation by hand. On a busy repo the gap between "GitGuardian alerted" and "credential rotated" is measured in hours or days, which is exactly the window an attacker needs.
Blast radius is off-platform. GitGuardian scores severity from the secret's type, not from what that specific key can reach in your environment. Whether a leaked token opens a sandbox or your production billing account is a question you answer by hand, in the cloud console, every time. The walkthrough above is that question, and it doesn't scale to forty incidents a week.
None of this makes the native tooling optional. It makes it the detection layer, not the response layer. Which leads to the next question: how do you attach blast-radius-aware remediation to every valid incident and every honeytoken trip, without handing a tool unchecked write access to your cloud?
What comes next
Everything you scripted here (the valid-and-critical triage, the honeytoken watch, the revoke, rotate, redeploy loop) can run continuously with an agent attached. That's part three of this series: CloudThinker watches new incidents and honeytoken trips over a read-only GitGuardian token, triages by validity and blast radius, drafts the remediation plan, and executes it step by step in Manual mode, with a named person approving every write.
Or see your own open incidents triaged: Try CloudThinker free with 100 premium credits and no card required.
