secret-and-config-audit
Skill4 starsChanged 20 days agoMITView on GitHub
Scan a repository for likely committed secrets and risky configuration and report each lead with file, line, rule and severity while never printing the matched value. Reach for this skill when auditing or reviewing a codebase for leaked credentials before a release or open-source publication, when triaging a "did we commit a key?" question from CI or a security review, when inventorying which provider token formats a repo could plausibly contain, or when you need a machine-readable (CI-gateable) secret-leak report as JSON. It does not validate credentials against a provider, does not scan git history or remote branches, does not attempt to decode or use anything it finds, and does not repair or remove the secrets it reports.
- Reads credentials
We split this skill into the rules it gives an agent
4 rules. About 0 are common, 2 are distinctive, and 2 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 190
What to do with a hit: treat it as compromised. Revoke/rotate at the provider first, then remove it from the tree — deleting the file does …
- Very specific: line 195
Run scan with a severity threshold that fits the task.
- Very specific: line 206
If a clean scan is important, run rules to understand which token families are covered, and state that the scanner is heuristic.
- Distinctive: line 234
Use this as a structure, not as a claim.
≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
secret-and-config-audit
Heuristic, redacting scanner for committed secrets and risky configuration in a directory tree. It reports leads — file, line, rule, severity and a masked shape of the match — and never emits the original value.
When to use
- Pre-release or pre-publication audit of a repository that will become public.
- A reviewer or CI job asking "does this tree contain a credential-shaped string?".
- Establishing which token formats a codebase is even capable of containing, via
rules. - Producing a JSON report an agent or shell script can gate on (
--fail-on). - Sanity-checking a newly introduced integration file (CI config, terraform,
.npmrc). - Reviewing a working-tree snapshot before sharing a patch, publishing a repository, or opening an incident ticket.
When not to use
- Proving a credential is live. Nothing here contacts a provider.
- Scanning git history, other branches, or a remote. Only the working tree is read.
- Replacing a dedicated tool such as Gitleaks, TruffleHog or
detect-secrets. Those maintain richer fingerprints and entropy checks. - Deciding what to do about a finding. Rotation and cleanup are human decisions.
- Grepping binary artefacts: binary files are skipped by design, not scanned.
Requirements
- Node.js >= 20 (uses
node:fs/promises,TextDecoder,createRequire). - No npm dependencies.
gitonPATHis optional: used for.gitignoreresolution (git check-ignore --stdin -z). Without it, a root-.gitignorefallback reader is used.
Output contract
Success (exit 0) — one line on stdout, nothing on stderr:
{"data":{"root":"/abs/path","files_scanned":12,"files_skipped":3,"bytes_scanned":48213,"findings":[],"counts":{"by_rule":{},"by_severity":{"high":0,"medium":0,"low":0},"placeholder":4},"skipped":{"excluded":2,"extension":1,"gitignored":0,"too_large":0,"binary":0,"unreadable":0,"symlink":1,"other":0},"symlinks":{"count":1,"sample":["escape"]},"placeholders_sample":[{"file":"README.md","line":4,"rule":"github-token","reason":"example"}],"gitignore":{"mode":"git","files_ignored":0},"truncated":false,"exit_recommended":false}}findings[]entries:{file, line, column, rule, severity, kind, match, evidence}.fileis relative toroot;lineis 1-based;columnis 1-based.severityishigh|medium|low.matchis the non-reversible shapeabcd…wxyz(first 4 + last 4 characters). Values shorter than 12 characters render as[redacted:too-short]and are never shown.evidenceis the source line with the match replaced by[redacted], truncated to 240 characters.counts.placeholderis the number of matches suppressed as documentation.placeholders_sample[]shows up to 10 of them withfile,line,rule,reason.skippedbreaksfiles_skippeddown by reason;gitignore.modeisgit|fallback|disabled.exit_recommendedistruewhen at least onehighfinding exists.truncatedistruewhen--max-filesor--max-findingscut the report short.
Failure (exit 1) — one line on stderr, nothing on stdout:
{"error":"directory not found: ./missing"}--helpprints{"data":{"usage":"..."}}(exit0).--versionprints{"data":{"name":"@unreadlogs/skill-secret-and-config-audit","version":"0.1.0"}}(exit0).- With
--fail-on high|medium|lowand a matching finding, the report is still printed to stdout as{"data":...}but the exit code is1and stderr stays empty. This is the only case where a successful report accompanies a non-zero exit.
Commands
| Command | Arguments | Output data |
scan | [dir] (default .), [--exclude a,b], [--include a,b], [--max-file-bytes N], [--max-files N], [--max-findings N], [--min-severity high|medium|low], [--rules a,b], [--no-gitignore], [--fail-on high|medium|low|none] | the scan payload above |
rules | none | {count, rules:[{id, severity, kind, pattern, false_positives}], placeholders:[...]} |
--help | — | {usage} |
--version | — | {name, version} |
Flags in detail:
--exclude a,b— extra comma-separated glob patterns (*,?,**) matched against both the file name and the repo-relative path; skipped files count asskipped.excluded. Added to (never replaces) the built-in directory skip list.--include a,b— comma-separated globs. A matching file is scanned even when its extension is not text-like, and a matching directory is traversed even when it is in the built-in skip list. This is the escape hatch fornode_modules, but it also means--include '**/*'will happily walk a vendor tree.--max-file-bytes N— files larger than this are skipped astoo_largewithout being read. Default1048576.--max-files N— only the first N candidate files are scanned; the rest count asexcludedandtruncatedbecomestrue. Default20000.--max-findings N— scanning stops once N findings exist. Default500.--min-severity LEVEL— report onlyLEVELor worse (high,medium,low). Defaultlow, i.e. report everything. Placeholders are counted before this filter applies, socounts.placeholderdoes not shrink when you raise the bar.--rules a,b— restrict to specific rule ids; an unknown id is an error.--no-gitignore— disables.gitignorefiltering (gitignore.modebecomesdisabled).--fail-on LEVEL— exit1when any finding atLEVELor worse exists (high,medium,low, ornone, the default).
Rules
23 rules. kind groups them; false_positives is the honest caveat for each. rules prints the same table at runtime, including the false-positive notes.
| Rule | Severity | Kind | Pattern summary | Typical false positive |
private-key | high | private-key | -----BEGIN [RSA|DSA|EC|OPENSSH|PGP|ENCRYPTED] PRIVATE KEY----- | fixture keys committed for tests |
credential-uri | high | credential-uri | postgres(ql)://, mysql://, mongodb+srv://, redis://, amqp(s)://, ftp(s)://, sftp:// with user:pass@ | localhost DSNs, ${VAR} placeholders |
basic-auth-url | medium | credential-uri | http(s)://user:password@host | docs with obvious placeholder users |
github-token | high | provider-token | ghp_/gho_/ghu_/ghs_/ghr_, github_pat_, 36+ chars | revoked tokens in changelogs, ...EXAMPLE |
gitlab-token | high | provider-token | glpat- + 20+ chars | none known |
slack-token | high | provider-token | xoxb/xoxa/xoxp/xoxr/xoxs + 10+ chars | xoxe- workspace tokens (not matched) |
slack-webhook | high | provider-token | hooks.slack.com/services/T…/B…/… | disabling a webhook does not revoke the URL |
stripe-key | high | provider-token | sk_live_/sk_test_/rk_live_/rk_test_ + 10+ chars | sandbox keys are still not committable |
anthropic-key | high | provider-token | sk-ant- + 20+ chars | none known |
openai-key | high | provider-token | sk- (excluding sk-ant-) + 20+ chars | other vendors reuse the sk- prefix |
google-api-key | high | provider-token | AIza + 35 chars | none known (OAuth client secrets are not caught) |
sendgrid-key | high | provider-token | SG. + 16+ chars | none known |
twilio-key | high | provider-token | SK/AC + 32 hex chars | account SIDs are semi-public identifiers |
npm-token | high | provider-token | npm_ + 36 chars | none known |
pypi-token | high | provider-token | pypi-AgEIcHlwaS5vcmc + 50+ chars | none known |
shopify-token | high | provider-token | shpat_/shpca_/shppa_/shpss_ + 32 hex chars | none known |
square-token | high | provider-token | sq0atp-/sq0csp-/EAAA + 20+ chars | none known |
telegram-bot-token | high | provider-token | 8-10 digit bot id + : + 35 chars | numeric log timestamps |
aws-access-key | high | provider-token | AKIA/ASIA/ABIA/ACCA + 16 uppercase alnum | key id without its secret is inert but still noise |
jwt | medium | jwt | three base64url segments starting eyJ | public/expired/test tokens dominate this class |
npmrc-token | medium | assigned-secret | _authToken= / _auth= in .npmrc | local Verdaccio registries |
assigned-secret | low | assigned-secret | api_key, secret, token, password, credential(s), … [:=] a 12+ char value | docs prose, JSON Schema constraints |
certificate | low | certificate | -----BEGIN CERTIFICATE----- | benign by design: certificates are public |
A value that matches a rule but looks like documentation is not reported as a finding. It is counted in counts.placeholder and sampled in placeholders_sample. The placeholder reasons are: example, placeholder, redacted, changeme, pronoun-prefix, your-thing-here, x-mask, angle-mask, shell-expansion (${VAR}, $VAR), shell-variable, env-reference (process.env.X, import.meta.env.X, os.environ[…]), printf-format (%s), dummy-word, numeric, and prose.
Limits
| Limit | Default | Flag |
| Maximum bytes read per file | 1048576 (1 MB) | --max-file-bytes |
| Maximum files scanned | 20000 | --max-files |
| Maximum findings returned | 500 | --max-findings |
| Minimum reportable severity | low | --min-severity |
| CI failure threshold | none | --fail-on |
| Evidence line length | 240 characters | not configurable |
| Revealed characters per match | 4 + 4, masked middle | not configurable |
| Matches per line per rule | 1 (the first) | not configurable |
| Placeholder samples kept | 10 | not configurable |
| Symlink samples kept | 20 | not configurable |
| Binary detection window | first 8192 bytes | not configurable |
Directories skipped by default: .git, .hg, .svn, .idea, .vscode-test, .cache, .parcel-cache, .turbo, .svelte-kit, .next, .nuxt, .output, .vercel, .netlify, .terraform, .gradle, .mvn, .venv, venv, env, __pycache__, node_modules, bower_components, vendor, Pods, target, out, dist, build, coverage, .nyc_output, .serverless, .expo, DerivedData, tmp.
Examples
Confirm nothing was committed before publishing a package:
node scripts/index.js scan . --min-severity high --fail-on highExit 1 with the report on stdout means at least one high-severity lead.
See what the scanner is capable of finding before trusting a clean result:
node scripts/index.js rules{"data":{"count":23,"rules":[{"id":"private-key","severity":"high",...}]}} — every rule with its severity and its known false positives.
Audit only the files you are about to change, ignoring vendored trees:
node scripts/index.js scan services/api --exclude '**/*.snap' --min-severity mediumfiles_skipped.excluded tells you how many paths the --exclude patterns removed.
Triage a noisy report down to one provider and cap the output:
node scripts/index.js scan . --rules github-token,gitlab-token --max-findings 20Shape of one finding — note that the value itself is absent and the masked shape cannot be reversed into it:
{
"file": "src/billing.js",
"line": 2,
"column": 18,
"rule": "stripe-key",
"severity": "high",
"kind": "provider-token",
"match": "sk_l…mnop",
"evidence": "const stripeKey = '[redacted]';"
}Notes and limitations
- This is a heuristic scanner. A finding is a lead, not proof of a live credential; a clean scan is not proof of safety. Regexes cannot tell a real key from a 40-character fixture that happens to look like one.
- Values are never printed.
matchis first-4 +…+ last-4. Eight characters of a key are not enough to use it, but they are enough to confirm a finding by eye — treat stdout as sensitive anyway. Values under 12 characters are not shown at all.evidencemasks the match and truncates the line. - False negatives are expected: secrets split across lines, base64 blobs with no keyword, encrypted blobs, and providers without a rule here.
- False positives are expected: see the
false_positivescolumn, and the genericassigned-secretrule especially, which islowseverity for a reason. - Not scanned: git history, submodules, packed objects, files whose extension is unknown, anything skipped by the directory list, anything ignored by
.gitignore(unless--no-gitignore), and every file above--max-file-bytes. - Symlinks are never followed, so no symlinked directory can cause infinite recursion or pull the scan outside
root. Symlinks are counted inskipped.symlinkand sampled insymlinks.sample. - Binary detection uses a NUL byte in the first 8192 bytes plus a fatal UTF-8 decode. Valid-UTF-8 binaries are rare, so a misclassification is possible in both directions; both outcomes are visible in
skipped.binary. .gitignoreresolution prefersgit check-ignore(correct, includes nested.gitignorefiles and negations). Outside a git work tree, or withoutgitonPATH, only the root.gitignoreis honoured and negations (!pattern) are ignored;gitignore.modetells you which path was taken.- What to do with a hit: treat it as compromised. Revoke/rotate at the provider first, then remove it from the tree — deleting the file does not remove it from git history.
Triage workflow
- Decide whether the scope is the whole working tree or a smaller path set. Use
rulesfirst when you need to understand scanner coverage or tune a focused audit. - Run
scanwith a severity threshold that fits the task. Check the file, line, rule, and redacted evidence; never ask the tool or shell to print the original matched value. - Inspect the source locally to determine whether the match is a real credential, a fixture, or a documented placeholder. Do not repeat the value in chat, a ticket, or command output.
- For a likely live credential, recommend revocation/rotation with the provider, then removal and an appropriate history-remediation process. A working-tree scan cannot establish whether a value was previously committed.
- Report the scope, exclusions, truncation state, and key limitations with the findings. A clean result is useful evidence, not a security guarantee.
Detailed audit scenarios
Before publishing or making a repository public
Scan the working tree at the repository root and review the scan counters, ignored-file behavior, exclusions, and truncation. Include configuration and deployment files that may be outside the default extension set only when you understand the scope of --include. Check the intended release artifacts separately if they are generated or bundled; a tree scan does not inspect published packages or build output that was skipped.
If a clean scan is important, run rules to understand which token families are covered, and state that the scanner is heuristic. For a publication or incident with a high assurance requirement, pair it with a dedicated history scanner and provider-side controls.
Review one integration or configuration change
Pass the relevant subtree or use an include/exclude filter for the target files. Check whether .gitignore excluded the new file and whether the result was truncated. Review the redacted finding and inspect source context without copying secret-bearing lines into terminal logs, chat, or the final response. The scanner reports matching content; it does not determine whether the application actually uses the value.
Gate a CI job
Choose --fail-on based on the team's tolerance for false positives. high is the default recommended exit signal (exit_recommended tracks high findings), while medium/low thresholds can be noisy. Preserve the JSON report from stdout: with a matching --fail-on finding, exit code 1 still accompanies a successful report and stderr remains empty. Distinguish that policy failure from a tool error, which emits an error object on stderr.
Pin the scan root and limits in CI, and monitor truncated and skipped counts. A CI gate that silently scans only the first capped set of files can create a false sense of coverage. Avoid --include '**/*' without considering the cost and noise of scanning vendor trees.
Reading a finding without leaking it
Use file, line, column, rule, severity, kind, and redacted evidence to locate a candidate. The masked match still reveals a short prefix and suffix and must be treated as sensitive metadata. Keep raw values out of copied logs. If a candidate is a likely active secret, recommend immediate provider-side revocation or rotation and a history-remediation plan; do not test the credential by attempting to use it. If it is a fixture or placeholder, document why and consider a safer synthetic fixture that does not resemble a usable credential.
Report template
Scope: working tree at <root>; 1,245 files scanned; truncated: false
Finding: high / stripe-key at src/payments/config.js:18
Evidence: redacted match; likely credential-shaped value
Assessment: source context suggests it is configured as a runtime key; live
status was not checked and must not be tested from this scan.
Next step: rotate at the provider, replace with secret injection, and review
history exposure. Scan does not cover previous commits or remote branches.Use this as a structure, not as a claim. Fill it only with observed evidence.
Troubleshooting
| Symptom | Meaning |
{"error":"directory not found: …"} | the path argument does not exist; scan requires a directory, not a file. |
{"error":"unknown rule id(s): …"} | typo in --rules; run rules for the valid ids. |
{"error":"--min-severity must be one of high, medium, low"} | invalid severity; the flag is case-insensitive. |
{"error":"--max-files must be an integer, got: …"} | numeric flags reject units and suffixes. |
files_scanned is 0 | everything was excluded: check --exclude, .gitignore (try --no-gitignore), or the file extensions. |
truncated: true | a --max-files / --max-findings cap was hit; raise it or narrow with --rules. |
gitignore.mode is fallback | not a git work tree, or git is missing; only the root .gitignore was applied. |
Many skipped.excluded / skipped.too_large entries | generated or vendored files were pruned; raise --max-file-bytes or pass --include for a specific tree. |
In this repository12 other files it gives its agents.
- ast-aware-js-repo-auditSkill
- env-doctorSkill
- git-change-intelligenceSkill
- image-transform-pipelineSkill
- json-repair-and-schemaSkill
- launch-auditSkill
- ocr-to-markdownSkill
- osint-investigative-agentSkill
- pdfSkill
- sqlite-agent-toolkitSkill
- streaming-csv-json-normalizerSkill
- workspace-archivesSkill
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
Reviews can't be read right now.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

