Kingfisher Command
Find a leaked secret, prove whether it's still alive, see what it can reach, and kill it — in that order.
Read references/kingfisher.md before responding. It contains bootstrap steps, the full CLI surface (scan, validate, revoke, baseline, config), the kingfisher.yaml schema, platform-target commands with their auth env vars, exit-code semantics, CI templates, and the pre-commit/Husky setup.
The bundled hook is scripts/pre-commit.sh, relative to this SKILL.md. Set skill_dir to the absolute directory containing this loaded skill before standalone setup. In a repository clone that is skills/kingfisher; a portable install carries the script inside platform-skills-kingfisher/scripts/.
This command is interactive by default. With no arguments, run the three-layer wizard below. Never dump a raw kingfisher scan invocation and stop — the useful output is a validated, triaged verdict, not a wall of candidate strings.
Ownership boundary (enforced at the menu)
| Question | Authoritative command |
|---|---|
| "Is there a hardcoded-looking secret in this repo?" (fast, offline, bundled with a CVE scan) | /platform-skills:trivy secrets mode |
| "Is that secret still live? What can it reach? Kill it." | this command |
| "Did a secret leak into Slack, Jira, Confluence, S3, or a whole GitHub org — not just one repo?" | this command |
| "How should secrets be stored/rotated inside my cluster?" | /platform-skills:secrets |
"Is my workflow's secrets: context usage safe?" | /platform-skills:zizmor |
| "Is there a CVE in my image or dependencies?" | /platform-skills:trivy |
| "How do I sign images / generate an SBOM?" | /platform-skills:supply-chain |
Trivy's secrets scanner and Kingfisher are not the same depth of tool: Trivy tells you a string matches a pattern, offline, as part of a broader vuln/license scan. Kingfisher live-validates the candidate against the real provider, tells you if it's actually dangerous, and can map or kill it. Run both if you want a cheap first pass and a specialist second pass — they are not redundant.
Mode dispatch
Parse the first word of $ARGUMENTS as the mode. When $ARGUMENTS is empty, run the three-layer interactive wizard.
| Mode | What it does |
|---|---|
scan | Default local/repo run, live validation on — the everyday check |
audit | Deep sweep for a security review: low confidence, full history, blast-radius, all outcomes |
validate | Check whether one known secret string is still live — no scan |
revoke | Kill one known secret directly through its provider — destructive, confirm first |
baseline | Create or update a baseline so future scans report only new secrets |
triage | Walk existing findings one at a time → validate / revoke / rotate / accept-in-baseline / false-positive |
config | Generate or repair kingfisher.yaml |
ci | Emit a CI gate: hard-fail, SARIF alerts, or Docker-based |
precommit | Add the kingfisher pre-commit hook, which runs the installed binary (or Husky), and state the caveat |
explain | Explain a rule id, an exit code, or a validation outcome |
| (empty) | Three-layer interactive wizard |
Three-layer interactive wizard
Layer 1 — Developer question (intent in their language)
What are you trying to find out?
1. "Did we commit a secret? Check my repo." → scan
2. "Full sweep — I'm doing a security review." → audit
3. "I have a key from somewhere — is it still live?" → validate
4. "Kill this credential now." → revoke
5. "Only show me new secrets, not the old backlog." → baseline
6. "I have findings — help me decide what to do." → triage
7. "Set up a kingfisher.yaml for this repo." → config
8. "Make this a check on every PR." → ci
9. "Catch it before I even commit." → precommit
10. "What does this finding/exit code mean?" → explain
Pattern-only secret scan bundled with a CVE pass? → /platform-skills:trivy
Workflow secrets: context usage? → /platform-skills:zizmor
Storing/rotating secrets inside the cluster? → /platform-skills:secrets
Enter 1–10 or a mode name:
Layer 2 — Scope and target (decides what actually gets scanned, and how invasively)
Ask only what the chosen mode needs.
What should I scan? (press Enter for the recommended value)
• Target [.]: local path, a Git URL, or a platform target —
github/gitlab/bitbucket/gitea/azure/huggingface org or user,
s3/gcs bucket, docker image, jira/confluence/slack/teams/postman search.
Platform targets need an auth token env var (KF_GITHUB_TOKEN, etc.) —
see references/kingfisher.md → Platform-specific targets.
• History [working tree + history]: local paths scan both by default,
which double-counts anything still present in a tracked file (once from
the file, once from the commit it entered on). `--git-history=none`
scans the working tree only; a remote URL target is history-only by
default (bare clone).
• Live validation [on]: confirms whether each candidate still
authenticates. `--no-validate` is faster but static-only — you get
"looks like a key," not "is a key." Keep this on unless you're doing a
quick offline pre-check.
Before touching a platform target, Slack/Jira/Confluence/Teams/Postman search, or --blast-radius, confirm authorization out loud. These are not passive reads: platform-target scans pull private organizational content, and blast-radius mapping authenticates as the discovered credential against a live provider. State plainly that you're only proceeding because the user confirmed access is authorized.
Detect and resolve silently where possible:
- Run
kingfisher --version. Not installed → offerbrew install kingfisher,uv tool install kingfisher-bin==2.13.0, or a pinned version fromreferences/kingfisher.md→ Bootstrap. Do not ask permission to check the version. - Check for
kingfisher.yamlat the target root. It is never auto-loaded — if found, ask whether to pass--config kingfisher.yamlexplicitly; if not found, say defaults apply. - Check for a baseline file (
baseline-file.yml,baseline.json, or similar committed near the repo root). Found → ask whether to pass--baseline-file; a scan without it re-reports the entire existing backlog as if it were new.
Layer 3 — Trade-offs (surface where a wrong default causes real harm)
A few decisions worth making explicitly (Enter accepts the recommendation):
• Gate threshold, for `ci`/`triage` on a repo that's never run this
before [205 — confirmed-live only]:
205 — fail only on a validated, still-active credential (RECOMMENDED
for day one; low noise, every failure is real)
200 — fail on any candidate, live or not (stricter, but a first run
on an established repo typically surfaces a long tail of test
fixtures and documented example keys like AKIAIOSFODNN7EXAMPLE)
Move from 205 to 200 once a baseline absorbs the existing backlog.
• Suppressing a known-fake finding [baseline, not --skip-word]:
A committed baseline is reviewable, repository-scoped, and shows exactly
what was accepted and when. `--skip-word`/`--skip-regex` are pattern-wide
and silently swallow anything matching, including a future real secret
that happens to contain the same word. Reach for skip-word/regex only for
a fixture pattern that recurs across many files (e.g. every test file
uses the literal string `EXAMPLE`); use the baseline for one-off accepts.
• `--alert-include-secret` [off]: puts the literal secret value into the
chat/webhook payload. Recommend leaving this off — a Slack alert channel
is not a secrets vault, and the alert itself becomes a second place the
secret now lives. Turn it on only for a locked-down, need-to-know sink,
and say so explicitly if the user asks for it anyway.
• Docker tag, for `ci` [pinned version, not `latest`]: `:latest` moves
under you; pin `ghcr.io/mongodb/kingfisher:<version>` for reproducible CI.
Ask these only when the situation triggers them:
| Trigger | Question |
|---|---|
revoke requested | "This kills a live credential with no undo. Confirm: validated as still live, owner identified, replacement ready to redeploy? (y/N)" |
audit or platform-target scan requested | "This may authenticate against a live provider or pull private organizational content (Slack/Jira/Confluence/Teams). Confirm you're authorized to scan this target. (y/N)" |
ci mode, repo has never run kingfisher | "First run on this repo typically surfaces a long tail of test fixtures. Gate on 205 (confirmed-live) and build a baseline, or gate on 200 immediately?" |
baseline mode, --manage-baseline about to run with a narrower --rule/--exclude/path scope than a prior baseline run | "This baseline run's scope is narrower than usual. --manage-baseline replaces this repository's section with exactly what this run finds — narrower scope can prune real, still-valid entries. Proceed, or widen scope to match the original baseline run?" |
config mode about to write a file | "Write kingfisher.yaml? (y/N)" — show the full proposed file first |
| Repo already has a kingfisher CI workflow | "A kingfisher workflow already exists at <path>. Update it in place, or add a second one?" |
Finding is assumed (not verified_active) and the user is treating it as confirmed | State plainly: assumed is high-confidence but not network-checked. Offer to re-run with validation on, or kingfisher validate directly on that string. |
Never ask about: output format for a quick local check (default pretty; ask only for ci/automation, where sarif/json/toon matter), whether to install kingfisher (just do it via brew or a pinned uv tool install, per Bootstrap), or whether to check for a kingfisher.yaml/baseline file (always check silently).
Mode: scan
The everyday run. Live validation on, default rule set, working tree + history for local targets.
# Local checkout
kingfisher scan /path/to/code
# Skip history if you only care about the current working tree
kingfisher scan /path/to/code --git-history=none
# Fast offline pre-check (static only, no network)
kingfisher scan /path/to/code --no-validate
Report in this order:
- Verdict — one line: clean, candidates only (no live secrets), or confirmed-live secrets present.
- Counts by outcome —
verified_active(network-confirmed),assumed(high-confidence, not checked), everything else grouped as "other" — this distinction is the whole point of running Kingfisher instead of a pattern scanner. - Findings, live-confirmed first, each with
file:line, rule id (betterleaks.*/veles.*), and outcome. - History note — if this was a local path scan, mention that filesystem + history means a tracked-file secret can appear twice; if that inflates the count, offer
--git-history=nonefor a cleaner working-tree-only view. - Next step — one concrete command:
validate/revokefor a live secret,triageif there are several,baselineif this is the first run on an established repo.
Mode: audit
Deep sweep for a human security review. Not a CI gate.
kingfisher scan /path/to/repo \
--confidence low \
--validation-filter all \
--blast-radius
State up front, before running: blast-radius mapping authenticates against each validated credential's provider — confirm authorization (Layer 2 gate). Low confidence surfaces more candidates, including likely false positives; this mode is for a human to triage, not a pass/fail signal.
Present findings in three buckets: confirmed live (act now), candidate, unconfirmed (needs validate or manual review), provider-mapped (blast-radius resolved an identity and reachable resources — highest priority regardless of confidence, because the access is now known concretely).
Mode: validate
Direct check on one known secret — no scan, no file context needed.
kingfisher validate --rule github-pat "ghp_..."
echo "ghp_..." | kingfisher validate --rule github -
# Validator needs an extra component
kingfisher validate --rule aws-access-token \
--var AWS_SECRET_ACCESS_KEY="secret_key" \
"AKIAEXAMPLE000000000"
Use the shortest rule selector that resolves (github-pat, not betterleaks.github-pat — the prefix is optional). Report the verdict plainly: live or not, and if live, hand off to revoke immediately rather than waiting to be asked — a validated-live credential is a standing incident until it's rotated or killed.
Mode: revoke
Destructive. No dry-run, no undo. State the blast radius before running, every time: this kills a live credential; anything currently authenticating with it — including production workloads the requester may not know about — loses access the instant it succeeds. Rollback is "issue a new credential and redeploy it," not "undo this command."
Required gate, no exceptions: confirm (1) the credential was validated as still live, (2) an owner or system has been identified, (3) a replacement is ready to redeploy. If any of the three is unconfirmed, say so and stop — do not revoke "to be safe."
kingfisher revoke --rule github-pat "ghp_..."
kingfisher revoke --rule aws-access-token \
--var AKID=AKIAIOSFODNN7EXAMPLE \
"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
After a successful revoke, re-run validate on the same string to confirm it's actually dead — a revocation call returning success is not the same as the provider having applied it instantly on every downstream cache.
Mode: baseline
Track only new secrets against an established, repository-aware baseline.
# Create — low confidence to capture every existing candidate once
kingfisher scan /path/to/code \
--confidence low \
--manage-baseline \
--baseline-file ./baseline-file.yml
# Use on every later scan
kingfisher scan /path/to/code --baseline-file ./baseline-file.yml
State the two gotchas from references/kingfisher.md → Baseline before running an update: --manage-baseline replaces the scanned repository's section with exactly what that run finds (narrower scope than the original creation run silently prunes still-valid entries), and a failed repository worker leaves the whole file untouched rather than partially updating it. Recommend committing the baseline file and reviewing diffs to it in PR review, the same way a zizmor.yml ignore: list gets reviewed — an unreviewed, ever-growing baseline is a backlog nobody is actually looking at.
Mode: triage
Walk findings one at a time. For each, in order:
- Confirm the outcome.
verified_active→ live, act now.assumed→ high-confidence but not network-checked; offervalidateto confirm before deciding urgency. Anything else → likely noise, but don't assume — check why validation was skipped or inconclusive (missing endpoint config, rate-limited, validator doesn't exist for this rule) before dismissing it. - If live:
validateif not already confirmed → identify owner and rotation path →revoke. - If a true positive but rotation is already in flight: accept it into the baseline with the rotation ticket noted, don't just delete the finding from view.
- If a genuine false positive (test fixture, documented example key): accept via baseline for a one-off, or
--skip-word/--skip-regexinkingfisher.yamlfor a pattern that recurs across many files. Never disable an entire rule family to silence one fixture — that hides every future real secret matching that rule too. - If it's real but the fix is "this needs a design change" (e.g., a whole class of workloads still using static keys) — say so plainly and scope it as separate work, don't fold it into this triage pass.
Every acceptance needs a stated reason — a bare baseline entry or skip-word with no context is indistinguishable from an accident later.
Mode: config
Generate or repair kingfisher.yaml. Show the complete proposed file and get approval before writing.
Generate it from the flags the user already wants as scan defaults — don't hand-write the YAML:
kingfisher config init \
--confidence high \
--redact \
--exclude vendor/ \
--exclude '**/node_modules/**' \
--format sarif \
--output ./kingfisher.sarif \
> kingfisher.yaml
Never pass --alert-webhook to config init. A webhook URL is a bearer credential, and kingfisher.yaml is meant to be committed — baking a webhook in serializes that credential straight into git. Leave webhooks out of the generated file and inject them at scan time from a CI secret (--alert-webhook "$SLACK_SECURITY_WEBHOOK") instead, per references/kingfisher.md → CI pattern.
State plainly, once, because it trips people: this file is never auto-discovered. Every invocation, local or CI, needs --config kingfisher.yaml explicitly or the file has no effect. Walk through: confidence/redaction defaults, exclude globs, skip-words for known fixture patterns, and output format/path — and remind that alert webhooks belong at runtime, not in this file, and that scan-target inputs (paths, org/user/bucket flags, auth tokens) are deliberately not config-overridable; they stay on the CLI.
Mode: ci
Ask which shape is wanted before generating:
How should this gate behave?
a. Hard-fail check → the job goes red on findings (you choose 200 vs 205)
b. SARIF alerts → findings become code-scanning alerts; job stays green,
blocking is a ruleset decision, not a workflow-exit decision
c. Both → alerts for visibility, plus a separate failing check
d. Docker-based → no runner-side install, pin the image tag
Enter a, b, c, or d:
There is no official mongodb/kingfisher-action — say this plainly if the user expects one. Copy the matching template from references/kingfisher.md → CI integration (Options A–C), which install a pinned, checksum-verified release binary or run a pinned Docker image, and already SHA-pin actions/checkout and upload-sarif.
Ask the rollout question from Layer 3 before finalizing threshold: gate on 205 (confirmed-live) for a repo's first run, or 200 (any candidate) if a baseline already absorbs the backlog. Never default a first-time gate to 200 without asking — see references/kingfisher.md → "Rolling this out on a repo that has never run it."
Mode: precommit
# .pre-commit-config.yaml
repos:
- repo: https://github.com/mongodb/kingfisher
rev: v2.13.0
hooks:
- id: kingfisher
args: [scan, ., --staged, --quiet, --no-update-check, --redact]
The full args list is required because pre-commit replaces the hook's default arguments. The kingfisher hook runs the binary on PATH, so install it first (Layer 2 check). Never use kingfisher-auto or kingfisher-docker: when the hook runs, they download the binary (latest by default, unverified) or pull ghcr.io/mongodb/kingfisher:latest.
State the caveat every time: a local hook cannot stop --no-verify or a clone that skipped hook install. It's a fast first line, not the control — the CI gate (ci mode) is what actually enforces this. Offer the Husky variant if the repo is Node.js-based (references/kingfisher.md → Pre-commit and Husky).
Mode: explain
Given a rule id (betterleaks.github-pat), an exit code, or a validation outcome, produce:
- What it is — the mechanism (a rule's detection pattern, or what an exit code/outcome actually asserts).
- Why it matters here — in terms of this specific finding or gate, not generically.
- What to do next —
validate,revoke,baseline, or "this needs a design change," per the triage decision tree. - Version note, if relevant — e.g. the
kingfisher.*→betterleaks.*/veles.*rename in v2.0, and that the compatibility alias is scheduled for removal.
Never invent a rule id, exit code, or flag. Pull from references/kingfisher.md; if it's not there, say so and point at the linked upstream docs rather than guessing.
Agent behavior
Intent classification
Before asking anything, classify the free-text request:
| Intent signals | Mode |
|---|---|
| "scan for secrets", "did we leak a key", "check my repo/history" | scan |
| "full sweep", "security review", "audit", "before I ship", "map blast radius" | audit |
| "is this key still valid/live", "check this credential" | validate |
| "revoke", "kill this token/key", "rotate this now" | revoke |
| "only show new secrets", "ignore existing findings", "track new leaks" | baseline |
| "too noisy", "false positive", "help me decide", "which of these matter" | triage |
| "kingfisher.yaml", "set defaults for scanning", "config" | config |
| "CI", "PR check", "block merges on secrets", "gate" | ci |
| "pre-commit", "before I commit", "local hook", "husky" | precommit |
| "what does this mean", "why is this flagged", a bare rule id or exit code | explain |
| "scan our GitHub org / Slack / Jira / S3 bucket / Docker image" | scan or audit, with the platform target from Layer 2, after confirming authorization |
| "hardcoded secret pattern check as part of a CVE scan" | → /platform-skills:trivy secrets mode |
| "is my workflow's secrets: usage safe" | → /platform-skills:zizmor |
| "how should I store/rotate secrets in my cluster" | → /platform-skills:secrets |
Never do these
- Never run
revokewithout the three-part confirmation (validated live, owner identified, replacement ready). No exceptions, even under time pressure — a wrong revoke is an outage, not a rollback. - Never run
--blast-radius, a platform-target scan, or a Slack/Jira/Confluence/Teams search without confirming authorization first. These authenticate against or pull from live systems the requester may not have unrestricted rights to inspect. - Never treat an
assumedfinding as network-confirmed. Say "high-confidence, not live-checked" every time one is reported, not just the first time. - Never gate a first-time CI rollout on exit code
200without surfacing the "long tail of fixtures" trade-off from Layer 3 — an established repo's first run will otherwise fail every PR on day one and get routed around. - Never suggest disabling an entire rule family to silence one fixture. Use a baseline entry or a scoped
skip-word/skip-regexinstead — disabling hides every future real secret matching that rule too. - Never turn on
--alert-include-secretwithout saying, explicitly, that the literal secret value will land in the alert payload. - Never invent a rule id, CLI flag, exit code, or
kingfisher.yamlkey. Everything here is verified against Kingfisher v2.0.0; anything not inreferences/kingfisher.mdneeds the linked upstream docs checked first. - Never claim there's an official
mongodb/kingfisher-action. There isn't, as of this writing — the CI templates are install-script or Docker based.
Always do these
- State the validation outcome distinction on every report: live-confirmed vs. high-confidence-unconfirmed vs. everything else. This is the entire value proposition over a pattern-only scanner — don't let it get flattened into "N findings."
- Confirm authorization before any privileged action — blast-radius, revoke, or a platform-target/SaaS-content scan — every time, not just the first time in a session.
- Recommend a baseline before recommending a hard CI gate on any repo that hasn't run Kingfisher before.
- After a
revoke, recommend re-validating the same string to confirm the provider actually applied it. - Hand off to
/platform-skills:secretswhen the conversation shifts from "did this leak" to "how should we store this properly going forward."
Cross-references
/platform-skills:trivy— fast offline secret-pattern scanning bundled with CVE/license checks; first-pass complement, not a substitute/platform-skills:secrets— ESO/Sealed Secrets design and rotation runbooks for secrets living inside the cluster/platform-skills:zizmor— GitHub Actionssecrets:context safety; audits workflow definitions, not file contents/platform-skills:supply-chain— Cosign signing, SBOM, SLSA provenance for what a pipeline produces/platform-skills:github-actions— the CI workflow itself: OIDC, token scoping, job graph design/platform-skills:preflight— production-readiness sweep across mixed file types, including a secrets check/platform-skills:self-improve— after closing out a real finding here, log the incident with/platform-skills:self-improve log