secrets-in-output
Skill27 starsChanged 19 days agoApache-2.0View on GitHub
Nothing that can carry a secret value may reach a tool result, because the transcript is a synced file re-sent on every turn. Check an env var by presence and length only, list names not values, read a .env file by its keys, capture provider status output and API responses into a variable and print a named allowlist, and dry-run the output shape on a dummy value first. Triggers on any command, script or API call that names an env var, a .env file, a key, token, password or secret, or a credential-carrying header. Does not fire on whether a changed value is live (env-change-verification) or on comparing a secret (constant-time-secret-compare).
- Reads credentials
In this repository56 other files it gives its agents.
- ai-surface-disciplineSkill
- blast-radius-grepSkill
- checkpoint-logSkill
- commission-the-rosterSkill
- committee-reviewSkill
- confirm-before-pushSkill
- constant-time-secret-compareSkill
- context-economySkill
- contract-reviewSkill
- cross-agent-reviewSkill
- date-parse-utc-safeSkill
- db-migration-verificationSkill
- deliverable-integritySkill
- dependency-upgrade-verificationSkill
- earn-every-lineSkill
- email-delivery-verificationSkill
- enforce-invariants-in-buildSkill
- env-change-verificationSkill
- findings-are-evidenceSkill
- fixtures-from-realitySkill
- flag-deferred-itemsSkill
- guard-the-spend-pathsSkill
- handoverSkill
- honest-failure-surfacingSkill
- legal-citation-integritySkill
- legal-notice-handlingSkill
- live-data-surgerySkill
- live-state-firstSkill
- lock-at-the-chokepointSkill
- mass-red-triageSkill
- measure-per-routeSkill
- no-silent-data-dropSkill
- one-real-rideSkill
- outbound-side-effect-idempotencySkill
- parallel-work-reconSkill
- pin-to-what-was-seenSkill
- plan-firstSkill
- price-the-spendSkill
- prove-it-can-failSkill
- reproduce-the-real-buildSkill
- rerun-before-verdictSkill
- safe-smokesSkill
- server-side-authoritySkill
- skill-library-builderSkill
- spawn-from-declared-stateSkill
- statute-to-obligations-registerSkill
- substantiate-outward-claimsSkill
- team-loopSkill
- tool-layer-bytesSkill
- trace-one-recordSkill
- unordered-eventsSkill
- unslop-codeSkill
- unslop-textSkill
- unslop-uiSkill
- verified-citationsSkill
- verify-the-effectSkill
The file
We split this skill into the rules it gives an agent
3 rules. About 0 are common, 1 is distinctive, and 2 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
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
- Very specific: line 33
Never a :- or - fallback on a secret (${X:-unset} prints X when it IS set, which is the case you were checking for), never echo $X, never a …
- Very specific: line 58
Dry-run the output shape on a dummy value first: Before a command that prints anything derived from a secret, run it with the variable …
- Distinctive: line 68
Treat the value as exposed the moment it is in a tool result, whatever the permission mode and whoever was watching.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 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
Secrets in output
Two slips in two days, both in the ICC repo, both by an agent doing routine checks. On 18 September
2026 a Sonnet verification agent ran a bare npx supabase status and the local stack's keys went
into its transcript. On 19 September the main session ran a loop over SUPABASE_* names with
${!v:-unset} to learn which were set, and the VALUES of an account-wide personal access token and
another project's service-role key landed in the Claude Code transcript (the local ~/.claude/projects
JSONL and whatever the desktop app syncs). Nothing in the library covered it; the handover convention
"read the token into a shell variable without printing it" was prose. ICC LESSONS_LEARNED L-045.
The transcript is not a terminal that scrolls away. It is a file, it is synced, it is re-sent to the model on every later turn, and it outlives the session. Anything that reaches a tool result reaches all of that.
The rules
Presence and length, never the value. The only question a check may answer is "is it set, and how long is it".
[ -n "$SUPABASE_ACCESS_TOKEN" ] && echo "SUPABASE_ACCESS_TOKEN set (${#SUPABASE_ACCESS_TOKEN} chars)" || echo "unset"if ($env:SUPABASE_ACCESS_TOKEN) { "set ($($env:SUPABASE_ACCESS_TOKEN.Length) chars)" } else { "unset" }Never a
:-or-fallback on a secret (${X:-unset}prints X when it IS set, which is the case you were checking for), neverecho $X, never a prefix (${X:0:6}is the value too).- Names, not values, when listing.
env | cut -d= -f1,compgen -v | grep SUPABASE,Get-ChildItem Env: | Select-Object Name. A bareenv,printenv,set,export -p,declare -p,Get-ChildItem Env:or[Environment]::GetEnvironmentVariables()prints every value on the machine, and piping it intogrepprints the matching lines whole. - A .env file by its keys.
cut -d= -f1 .envorgrep -o '^[A-Z_]*=' .envanswers "which keys are configured".cat .env,head .env,Get-Content .env,grep KEY .envand the Read tool on.envput the whole file in the transcript..env.exampleis documentation and is fine. - Provider status and config commands are listings too.
supabase statusprints the stack's keys in its table andsupabase status -o envprints them asKEY=value; capture it (eval "$(npx supabase status -o env)", or> "$TMP/status.env") or filter it (| grep -viE 'key|secret|token|jwt').netlify env:list,vercel env pull,gh secret,az keyvault secret show,aws ssm get-parameter --with-decryptionandstripe config --listbelong to the same class: know what each prints before it runs. - An API response is untrusted output. A GET of a config endpoint can return the credentials it
holds: the Supabase Management API's
/config/authanswers 243 fields includingsmtp_pass, hook secrets and provider secrets. Never print a raw response. Parse it into a variable and print a named allowlist of the fields you need, asscripts/supabase-auth-config.mjsdoes. Verbose HTTP clients echo the request too:curl -vprints theAuthorizationheader it sends. - Dry-run the output shape on a dummy value first. Before a command that prints anything derived
from a secret, run it with the variable pointed at a known string (
X=dummy-value-123) and read what comes out. If the dummy appears, the real one would have. - A value that must be used goes into a variable or a file the tool result never shows. Read it
with
X=$(cat file)orread -r X < file, pass it as a header or an env var to the process that needs it, and let only that process's non-secret output reach the transcript.
When it has already happened
Treat the value as exposed the moment it is in a tool result, whatever the permission mode and
whoever was watching. Say so plainly in the same turn: which secret, which scope (a project key or an
account-wide token), and where the transcript lives. Give the rotation steps for that provider. The
owner decides whether to rotate; the record of the decision goes in the repo's lessons file with the
line skill that should have prevented this: secrets-in-output.
The hook
hooks/secret-echo-guard.mjs (PreToolUse, Bash|PowerShell, plus Read for .env files) denies
the command-string shapes above before they run and hands back the safe form in its reason. It is a
deny, not an ask, because in a bypass-permissions session an ask is auto-approved and shown to nobody,
and unattended is exactly where the slips happened. What the hook cannot see, this skill still owns: a
value copied into another variable and printed (k=$KEY; echo $k), any script file run by name
(the hook never reads its contents, so an echo $KEY inside bash phase.sh passes), a recursive
grep over a folder holding a secret file (grep -rn TOKEN .), and every API response.
Why
A secret in a transcript is not a near miss; it is a disclosure with an unknown audience, and the only cheap moment to prevent it is before the command runs. Presence-and-length costs nothing, answers the real question, and cannot leak. Evidence: ICC L-045 (19 September 2026), the 18 September Sonnet slip recorded in the same repo's 2026-09-18 handover entry.
Provenance and maintenance
Date stamp: 2026-09-20. Distilled from the two ICC incidents above; the hook's asserted cases are in
hooks/secret-echo-guard.test.mjs (the two incident commands are its first two deny cases).
| Fact | Re-verify with | Expected |
| The hook is wired for both command tools | node hooks/check-index.test.mjs | PASS secret-echo-guard.mjs is wired for both Bash and PowerShell |
| The hook denies the two incident shapes and passes the safe form | node hooks/secret-echo-guard.test.mjs | ALL PASS |
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
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.

