secret-scan-onboarding
Why It Exists
A repo can leak a credential into something it publishes (a mobile bundle, a web build, a package tarball) without anyone committing a secret on purpose. This skill wires a CI job that runs two scanners, gitleaks and trufflehog, over the artifact before the step that signs, uploads or deploys it, and fails the job on a real finding.
The scanners are installed from a small published package (@catesworks/next-starters) that carries the shared
classification rules, a generated gitleaks config and two filter scripts. Everything the rules cannot tell apart from a
real secret (test fixtures, documented placeholders) goes through an allowlist, and the allowlist is where this
tooling can quietly stop protecting you.
What It Does
- Two job-block templates, one for mobile builds and one for Vercel deploys, plus the shared recipe both use: install
the scripts into a scratch directory, install the scanners, run gitleaks then trufflehog with
continue-on-error, gate on the filtered private-finding count, and make the deploy jobneeds:the scan. - A rule that matters: the raw scanner count is not the answer. Every match is re-classified, and only private-pattern findings fail the job. Raw values are never printed, only counts and file and line.
- Exact version pins (no floating range), so a bad release cannot roll out fleet-wide on an unrelated CI run. A new release means bumping the pin in each consumer.
How To Use It
- Pick the variant: mobile (
fastlanelanes) or Vercel (git archive HEADscan), or adapt the skeleton. - Copy the job block, set the pinned package version, and grant the consumer repo read access to the package.
- Make the deploy job depend on the scan, then dispatch the workflow once and confirm the scan runs and the gate fails on a planted test secret.
Gotchas & Invariants
Allowlist by path hides real secrets; bind exceptions to the line
An early allowlist silenced two files by path. Those names (an environment.ts that declares an app's env schema, and a
telemetry test fixture) exist in a dozen or more repos with different contents, though only one copy of each ever
produced a finding. A real key pasted into any of the others would have been reported clean.
Exceptions are now bound to path and a hash of the full source line. The obvious alternative, hashing the scanner's matched value, does not work: the generated rules match only a prefix (a Stripe live-key prefix, a PEM private-key header), so the matched text is identical for every key sharing it and a real key would hash the same as the reviewed placeholder. The line hash changes whenever the reviewed line is edited or a new key is added, and the finding comes back.
The filter must not guess what it scanned
The filter reads the line from the finding's own commit (git cat-file blob <Commit>:<File>), not the working tree, with
replace refs ignored. Anything it cannot establish exactly is reported, never suppressed: a blank or malformed
commit id, an unreadable file, a bad line range, a failing git. Repository detection ignores a stale inherited
GIT_DIR, GIT_WORK_TREE and similar variables, and a scan is treated as "not a git repo" only if git says so, no
.git exists in the working directory or any parent, and the caller set no repo-locating variable.
Scan the files you add
A test file that contains secret-shaped fixtures trips the generated rules itself and turns the gate red after merge. Run the scanner over a change before opening the PR, including documentation: prose that spells out a key prefix or a private-key header is a finding too.
Local runs can differ from CI
Some global git configs make gitleaks drop all commit metadata, so the reviewed lines show up as findings locally. To
reproduce CI, run with GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null.
Related Skills
- secrets-source-of-truth: where the secrets this gate protects actually live.
- prod-deploy: the deploy the scan gates.
- mobile-expo-app: the mobile build the first variant covers.