[object Object]

← 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

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 →