← back to Cli Printing Press
fix(skills): add secret leak prevention rules (#107)
fc94557cb3cd5601818f2d2e4a3ad97e58108596 · 2026-04-01 18:23:03 -0700 · Trevin Chow
* fix(skills): add secret leak prevention rules to printing-press and publish skills
Two-layer defense against API keys and credentials leaking into artifacts:
1. Generation time (exact-value scan): If the user provided an API key,
scan all manuscript artifacts for the exact value before archiving.
Zero false positives — checks the specific string, not patterns.
Auto-redacts if found.
2. Publish time (best-effort): If gitleaks/trufflehog is installed, run
it on the staged directory. Otherwise lightweight checks for .env,
session-state.json, and un-redacted README examples. Warn-only — the
user decides whether flagged items are real secrets or false positives.
Also:
- HAR archive stripping now removes auth request headers (Authorization,
Cookie, X-API-Key) not just response bodies
- Session state cleanup order documented as mandatory
- Cardinal rules: never write secret VALUES (names are fine)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(skills): use python for literal secret redaction instead of awk
awk's gsub interprets regex metacharacters in the search string (+, ., /)
which breaks on JWT tokens and base64-encoded API keys. Python's
str.replace is truly literal — no regex, no delimiter issues.
Values passed via environment variables to avoid shell quoting problems.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(skills): add minimum key length guard to secret scan
Empty strings cause grep -F "" to match everything and str.replace("")
to insert between every character — catastrophic over-redaction. Short
keys (< 16 chars) would redact legitimate content. Real API keys are
20+ characters. Added ${#API_KEY_VALUE} -ge 16 guard.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(skills): strip Set-Cookie from HAR response headers
Auth flows return session tokens via Set-Cookie response headers.
The HAR scrubber only stripped request headers, leaving response
Set-Cookie values in archived manuscripts. Now strips both request
and response auth headers, plus response cookies.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Files touched
M skills/printing-press-publish/SKILL.mdM skills/printing-press/SKILL.mdA skills/printing-press/references/secret-protection.md
Diff
commit fc94557cb3cd5601818f2d2e4a3ad97e58108596
Author: Trevin Chow <trevin@trevinchow.com>
Date: Wed Apr 1 18:23:03 2026 -0700
fix(skills): add secret leak prevention rules (#107)
* fix(skills): add secret leak prevention rules to printing-press and publish skills
Two-layer defense against API keys and credentials leaking into artifacts:
1. Generation time (exact-value scan): If the user provided an API key,
scan all manuscript artifacts for the exact value before archiving.
Zero false positives — checks the specific string, not patterns.
Auto-redacts if found.
2. Publish time (best-effort): If gitleaks/trufflehog is installed, run
it on the staged directory. Otherwise lightweight checks for .env,
session-state.json, and un-redacted README examples. Warn-only — the
user decides whether flagged items are real secrets or false positives.
Also:
- HAR archive stripping now removes auth request headers (Authorization,
Cookie, X-API-Key) not just response bodies
- Session state cleanup order documented as mandatory
- Cardinal rules: never write secret VALUES (names are fine)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(skills): use python for literal secret redaction instead of awk
awk's gsub interprets regex metacharacters in the search string (+, ., /)
which breaks on JWT tokens and base64-encoded API keys. Python's
str.replace is truly literal — no regex, no delimiter issues.
Values passed via environment variables to avoid shell quoting problems.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(skills): add minimum key length guard to secret scan
Empty strings cause grep -F "" to match everything and str.replace("")
to insert between every character — catastrophic over-redaction. Short
keys (< 16 chars) would redact legitimate content. Real API keys are
20+ characters. Added ${#API_KEY_VALUE} -ge 16 guard.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(skills): strip Set-Cookie from HAR response headers
Auth flows return session tokens via Set-Cookie response headers.
The HAR scrubber only stripped request headers, leaving response
Set-Cookie values in archived manuscripts. Now strips both request
and response auth headers, plus response cookies.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---
skills/printing-press-publish/SKILL.md | 44 ++++++++++
skills/printing-press/SKILL.md | 16 ++++
.../printing-press/references/secret-protection.md | 97 ++++++++++++++++++++++
3 files changed, 157 insertions(+)
diff --git a/skills/printing-press-publish/SKILL.md b/skills/printing-press-publish/SKILL.md
index 49656abf..f494e6e2 100644
--- a/skills/printing-press-publish/SKILL.md
+++ b/skills/printing-press-publish/SKILL.md
@@ -442,6 +442,50 @@ gh pr create \
Display the PR URL prominently.
+## Secret & PII Protection
+
+Before creating the PR, verify that no secrets leaked into the packaged CLI.
+
+**This matters because the library repo is public.** A leaked API key in a PR is
+a security incident — anyone can see it, even if the PR is later closed.
+
+### What the machine checks (deterministic)
+
+The generation skill (`/printing-press`) runs an exact-value scan during Phase 5.5
+if the user provided an API key. By the time publish runs, the machine's own
+mistakes should already be caught. But the user may have edited files between
+generation and publish.
+
+### What publish checks (best-effort, warn-only)
+
+1. **If `gitleaks` or `trufflehog` is installed**, run it on the staged directory:
+ ```bash
+ if command -v gitleaks >/dev/null 2>&1; then
+ gitleaks detect --source "<staging-dir>/library" --no-git --verbose 2>&1
+ elif command -v trufflehog >/dev/null 2>&1; then
+ trufflehog filesystem "<staging-dir>/library" 2>&1
+ fi
+ ```
+ These tools use vendor-specific patterns (Steam keys, Stripe keys, GitHub
+ tokens) with low false-positive rates. Their findings are warnings — the
+ user reviews and decides.
+
+2. **If no scanning tool is installed**, do a lightweight check:
+ - Verify no `.env` files, `session-state.json`, or `config.toml` with
+ real credentials exist in the staged directory
+ - Check README examples use `"your-key-here"` placeholders, not real values
+ - Check manuscripts (if included) don't contain auth headers or cookie values
+
+3. **Never include** in the staged directory:
+ - `.env` files
+ - `session-state.json`
+ - Config files with real credentials
+ - HAR captures with un-stripped auth headers
+
+If any issues are found, warn the user and ask whether to proceed. The user
+makes the final call — they may have intentionally included something the scan
+flagged (e.g., a test fixture with a fake key). Don't block silently.
+
## Error Handling
- **`gh` not authenticated:** Detect in Step 1, tell user to run `gh auth login`
diff --git a/skills/printing-press/SKILL.md b/skills/printing-press/SKILL.md
index c3da276d..f7163826 100644
--- a/skills/printing-press/SKILL.md
+++ b/skills/printing-press/SKILL.md
@@ -90,6 +90,22 @@ See the `printing-press-polish` skill for details. It runs diagnostics, fixes ve
- YAML, JSON, local paths, and URLs are all valid spec inputs for the verification tools.
- Maximum 2 verification fix loops unless the user explicitly asks for more.
+## Secret & PII Protection (Cardinal Rules)
+
+**These rules are non-negotiable. They apply at ALL times during a run.**
+
+API key **values**, token **values**, passwords, and session cookies must NEVER
+appear in any artifact: source code, manuscripts, proofs, READMEs, HARs, or
+anything committed to git. Env var **names** (e.g., `STEAM_API_KEY`) and
+placeholders (e.g., `"your-key-here"`) are safe.
+
+During Phase 5.5 (archiving) and before publishing, read and apply
+[references/secret-protection.md](references/secret-protection.md) for:
+- Exact-value scanning and auto-redaction of artifacts
+- HAR auth stripping (headers, query strings, cookies)
+- API key handling rules during the run
+- Session state cleanup ordering
+
## Orientation & Briefing
Before setup, check whether the user provided arguments. Handle two cases:
diff --git a/skills/printing-press/references/secret-protection.md b/skills/printing-press/references/secret-protection.md
new file mode 100644
index 00000000..45ea678d
--- /dev/null
+++ b/skills/printing-press/references/secret-protection.md
@@ -0,0 +1,97 @@
+# Secret & PII Protection — Implementation Details
+
+Read this file during Phase 5.5 (Archive Manuscripts) and before any publish step.
+The cardinal rules in SKILL.md apply at all times. This file has the implementation.
+
+## Exact-value scan before archiving
+
+The skill knows the API key if the user provided one. Before archiving manuscripts,
+scan all artifacts for the exact key value. This has zero false positives — it checks
+for the specific string, not guessed patterns.
+
+Use `grep -F` (fixed string) and `awk` for replacement — NOT bare `grep`/`sed` —
+because API keys often contain regex metacharacters (`+`, `/`, `.`, `=`) that would
+cause `grep` to match wrong text and `sed` to corrupt files.
+
+```bash
+# Guard: skip if key is empty or too short (< 16 chars). Short strings
+# would over-redact legitimate content. Real API keys are 20+ chars.
+if [ -n "$API_KEY_VALUE" ] && [ ${#API_KEY_VALUE} -ge 16 ]; then
+ LEAK_FOUND=false
+ for dir in "$RESEARCH_DIR" "$PROOFS_DIR" "$DISCOVERY_DIR"; do
+ if [ -d "$dir" ] && grep -rF "$API_KEY_VALUE" "$dir" 2>/dev/null; then
+ LEAK_FOUND=true
+ fi
+ done
+ if [ "$LEAK_FOUND" = true ]; then
+ echo "BLOCKING: API key value found in manuscript artifacts. Auto-redacting."
+ REDACT_TO="\$${API_KEY_ENV_VAR:-API_KEY}"
+ for dir in "$RESEARCH_DIR" "$PROOFS_DIR" "$DISCOVERY_DIR"; do
+ [ -d "$dir" ] || continue
+ find "$dir" -type f -print0 | while IFS= read -r -d '' f; do
+ if grep -qF "$API_KEY_VALUE" "$f" 2>/dev/null; then
+ # Use python for truly literal replacement — awk's gsub and perl's
+ # s/// both interpret regex metacharacters (+, ., /) in the key,
+ # which breaks on JWT tokens and base64-encoded secrets.
+ REDACT_OLD="$API_KEY_VALUE" REDACT_NEW="$REDACT_TO" python3 -c "
+import sys, os
+old, new, path = os.environ['REDACT_OLD'], os.environ['REDACT_NEW'], sys.argv[1]
+with open(path) as f: content = f.read()
+with open(path, 'w') as f: f.write(content.replace(old, new))
+" "$f"
+ fi
+ done
+ done
+ echo "Auto-redacted. Verify before proceeding."
+ fi
+fi
+```
+
+## Strip auth from HAR captures before archiving
+
+Credentials can appear in four locations within HAR files:
+- **Request headers:** `Authorization: Bearer <token>`, `Cookie: session=...`
+- **Response headers:** `Set-Cookie: session=<token>` (from auth flows)
+- **Query strings:** `?key=<value>`, `?api_key=<value>`, `?access_token=<value>`
+- **Cookies:** session tokens, auth cookies (both request and response)
+
+The archive step must strip all four, plus response bodies (for size):
+
+```bash
+jq 'del(.log.entries[].response.content.text) |
+ # Remove auth headers from requests
+ (.log.entries[].request.headers) |= [.[] |
+ select(.name | test("^(Authorization|Cookie|Set-Cookie|X-API-Key|X-Auth-Token)$"; "i") | not)
+ ] |
+ # Remove Set-Cookie from responses (contains session tokens from auth flows)
+ (.log.entries[].response.headers) |= [.[] |
+ select(.name | test("^(Set-Cookie)$"; "i") | not)
+ ] |
+ # Redact auth-like query string params
+ (.log.entries[].request.queryString) |= [.[] |
+ if (.name | test("^(key|api_key|apikey|token|secret|access_token|password)$"; "i"))
+ then .value = "<REDACTED>"
+ else . end
+ ] |
+ # Remove cookies entirely (they often contain session tokens)
+ (.log.entries[].request.cookies) |= [] |
+ (.log.entries[].response.cookies) |= []
+ ' "$har" > "${har}.stripped" 2>/dev/null && mv "${har}.stripped" "$har"
+```
+
+## API key handling during the run
+
+When the user provides an API key (Phase 0 API Key Gate or inline):
+- Store it only in a shell variable, never in a file
+- Pass it to commands via environment variable, not via flags visible in process lists
+- In dry-run output, the key may appear in query params — this is expected for
+ debugging but must NOT be captured in proof artifacts
+- When writing live smoke results to proofs, write the test outcomes (PASS/FAIL)
+ but never the request URLs that contain the key in query params
+
+## Session state cleanup
+
+Session state files (`session-state.json`) contain browser cookies and auth tokens.
+The Phase 5.5 archive block removes them with `rm -f "$DISCOVERY_DIR/session-state.json"`.
+This removal is mandatory and must happen BEFORE the `cp -r "$DISCOVERY_DIR"` command.
+If the order is reversed, cookies leak into manuscripts.
← 2c9d57d7 feat(cli): add printing-press polish --remove-dead-code (#10
·
back to Cli Printing Press
·
fix(cli): publish skill registry format and manuscript resol b037ac01 →