From b3b2f111886cfcf04fc6b383bdca205de27560e5 Mon Sep 17 00:00:00 2001 From: James Arama Date: Wed, 29 Jul 2026 22:20:02 -0700 Subject: [PATCH] feat(leakage): add shape scan for public CI and expand mirror scrub tooling Add a secret-free structural checker for fork PRs and public runners, wire it into lint.yml, and extend the mirror history scrub script with push support. Documents the shape layer in PROTOCOL.md; mirror history scrub executed separately. Co-authored-by: Cursor --- .github/workflows/lint.yml | 5 +++ CHANGELOG.md | 17 +++++++++ Makefile | 5 ++- PROTOCOL.md | 12 ++++-- scripts/check-leakage-shapes.sh | 68 +++++++++++++++++++++++++++++++++ scripts/scrub-mirror-history.sh | 40 ++++++++++++++----- tests/leakage-shapes.bats | 57 +++++++++++++++++++++++++++ 7 files changed, 191 insertions(+), 13 deletions(-) create mode 100755 scripts/check-leakage-shapes.sh create mode 100644 tests/leakage-shapes.bats diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index ac8989c..5ce621f 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -16,6 +16,11 @@ jobs: run: make check-consult-grammar - name: tests (leakage mechanism runs on synthetic tokens) run: make test + # Secret-free shape scan — always runs on public runners and fork PRs. + # Catches in-tree denylist files and corporate email shapes without the + # real company token list. + - name: leakage shape scan + run: make check-leakage-shapes # The company token data exists only in trusted environments. On public # runners the scan below is structurally skipped — a visible "skipped" # step status, never a silent green. diff --git a/CHANGELOG.md b/CHANGELOG.md index 6838695..1228fa0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,23 @@ > **How to update:** The pre-commit hook (`scripts/pre-commit.sh`) is auto-installed by `install.sh`. It runs a fast leakage check (`scripts/check-no-leakage.sh`) and re-renders the generated `CLAUDE.md.generated` and `AGENTS.md.generated` files on every commit. A pre-push gate (`scripts/pre-push.sh`, also auto-installed) scans every outgoing commit's tree and metadata before it leaves the machine. CI runs lint, consult-grammar, and the test suite (including synthetic-token leakage mechanism tests) on every PR; the company-token scan is a separate step that runs only when guard data is present on the runner. If you bypass the hooks or work in a context where hooks cannot run, keep this file current manually. Each release heading links to the diff on the public mirror. +## v1.15.2 — 2026-07-29 (shape CI + mirror history scrub) + +### Added + +- `scripts/check-leakage-shapes.sh` — secret-free structural leakage scan for public CI and fork PRs: flags in-tree `leakage-tokens.txt`, `dotfiles-guard/` material, and corporate email shapes in `.claude/` publication surfaces without embedding company-specific token values. +- `tests/leakage-shapes.bats` — coverage for the shape scan. +- `make check-leakage-shapes` — Makefile target wired into `.github/workflows/lint.yml` as an always-on CI step. + +### Changed + +- `scripts/scrub-mirror-history.sh` — gains a `push` subcommand for force-pushing scrubbed bare mirrors; audit output lists release tags to re-apply. +- `PROTOCOL.md` — documents the shape scan as the fork-PR coverage layer alongside the structurally skipped company-token scan. + +### Ops + +- Public mirror history rewritten to remove `scripts/leakage-tokens.txt` from all commits (`git filter-repo` + force-push). Fork owners may need to rebase. + ## v1.15.1 — 2026-07-29 (publication-gate follow-ups) ### Added diff --git a/Makefile b/Makefile index 303e1fd..c209335 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: test lint check-leakage gen-docs gen-boundaries gen-role-guards gen all +.PHONY: test lint check-leakage check-leakage-shapes gen-docs gen-boundaries gen-role-guards gen all DOTFILES_DIR := $(shell realpath .) @@ -21,6 +21,9 @@ lint: ## Run shellcheck on all scripts (repo-r check-leakage: ## Scan for company-specific tokens bash scripts/check-no-leakage.sh . +check-leakage-shapes: ## Secret-free structural scan (public CI / fork PRs) + bash scripts/check-leakage-shapes.sh . + check-consult-grammar: ## Positive grammar check for consult-instructions bash scripts/check-consult-grammar.sh . diff --git a/PROTOCOL.md b/PROTOCOL.md index db0ef71..c3fffd4 100644 --- a/PROTOCOL.md +++ b/PROTOCOL.md @@ -147,9 +147,15 @@ author/committer identity and the commit message. The pre-commit hook is retained as a fast working-tree early warning; the pre-push gate is what guards publication. -**Public CI** runs the mechanism tests on synthetic tokens and shows the -company-token scan as a structurally skipped step — visible non-execution, -never a silent green. +**Public CI** runs the mechanism tests on synthetic tokens, a **shape scan** +(`scripts/check-leakage-shapes.sh`) that always executes on public runners, and +shows the company-token scan as a structurally skipped step — visible +non-execution, never a silent green. + +The shape scan is secret-free: it flags in-tree denylist/guard paths and +corporate email shapes in `.claude/` publication surfaces without embedding any +company-specific token values. It is the fork-PR coverage layer where the real +denylist cannot exist. The token file format is unchanged: one token per line, `#` comments. diff --git a/scripts/check-leakage-shapes.sh b/scripts/check-leakage-shapes.sh new file mode 100755 index 0000000..69b00c9 --- /dev/null +++ b/scripts/check-leakage-shapes.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +# check-leakage-shapes.sh — secret-free structural leakage scan for public CI. +# +# Runs on every PR/push where the real company-token list is unavailable. +# Detects publication-risk *shapes* (in-tree denylist files, guard dirs, +# corporate email addresses in agent/skill prose) without embedding any +# company-specific token values. +# +# Usage: bash scripts/check-leakage-shapes.sh [DIR] (default ".") +# +# Exit codes: +# 0 — clean +# 1 — at least one shape violation (file/line only, matched content withheld) + +set -euo pipefail + +SCAN_DIR="${1:-.}" + +# Domains that are safe fixtures in docs and tests — not publication risks. +SAFE_EMAIL_DOMAINS='example\.com|example\.org|test\.com|localhost|users\.noreply\.github\.com' + +FOUND=0 + +_report() { + FOUND=1 + echo "LEAKAGE-SHAPE: $1 — line(s): ${2}(content withheld)" >&2 +} + +# --- Shape 1: in-tree denylist or guard material must never be committed --- +while IFS= read -r -d '' path; do + case "$path" in + */leakage-tokens.txt|*/dotfiles-guard/*) ;; + *) continue ;; + esac + _report "$path" "entire file " +done < <(find "$SCAN_DIR" \ + \( -name 'leakage-tokens.txt' -o -path '*/dotfiles-guard/*' \) \ + -not -path '*/.git/*' \ + -type f \ + -print0 2>/dev/null) + +# --- Shape 2: corporate email addresses in Claude publication surfaces --- +# Scan agent/skill/shared markdown only; withhold matched content in output. +EMAIL_PATTERN='[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}' +while IFS= read -r -d '' file; do + grep -qI '' "$file" 2>/dev/null || continue + while IFS= read -r line; do + [ -n "$line" ] || continue + lineno="${line%%:*}" + rest="${line#*:}" + domain="$(printf '%s' "$rest" | grep -ioE "$EMAIL_PATTERN" | head -1 | cut -d@ -f2 | tr '[:upper:]' '[:lower:]')" || true + [ -n "$domain" ] || continue + if printf '%s' "$domain" | grep -qiE "^(${SAFE_EMAIL_DOMAINS})$"; then + continue + fi + _report "$file" "${lineno} " + done < <(grep -inE "$EMAIL_PATTERN" "$file" 2>/dev/null || true) +done < <(find "$SCAN_DIR/.claude" \ + \( -path '*/agents/*.md' -o -path '*/skills/*/SKILL.md' -o -path '*/_shared/*.md' \) \ + -type f \ + -print0 2>/dev/null) + +if [ "$FOUND" -ne 0 ]; then + echo "" >&2 + echo "Leakage shape check FAILED. Matched content is withheld by design." >&2 + exit 1 +fi +exit 0 diff --git a/scripts/scrub-mirror-history.sh b/scripts/scrub-mirror-history.sh index 02fabc1..557dedd 100755 --- a/scripts/scrub-mirror-history.sh +++ b/scripts/scrub-mirror-history.sh @@ -8,27 +8,32 @@ # pushes from managed workstations — run locally if needed). # # Usage: -# bash scripts/scrub-mirror-history.sh audit -# bash scripts/scrub-mirror-history.sh scrub # destructive +# bash scripts/scrub-mirror-history.sh audit +# bash scripts/scrub-mirror-history.sh scrub # destructive rewrite +# bash scripts/scrub-mirror-history.sh push # -# After scrub: -# cd && git push --force --mirror -# Notify fork owners; re-tag v1.15.0 if the tag pointed at pre-scrub SHAs. +# Clone may be a normal working tree or a bare mirror (preferred for --mirror push). +# +# After scrub + push: +# Notify fork owners; re-tag release SHAs if tags pointed at pre-scrub commits. set -euo pipefail TARGET_PATH='scripts/leakage-tokens.txt' MODE="${1:-}" CLONE="${2:-}" +REMOTE="${3:-}" _usage() { cat < - bash scripts/scrub-mirror-history.sh scrub + bash scripts/scrub-mirror-history.sh audit + bash scripts/scrub-mirror-history.sh scrub + bash scripts/scrub-mirror-history.sh push audit — list commits that touched ${TARGET_PATH} -scrub — run git filter-repo --invert-paths (rewrites history in ) +scrub — run git filter-repo --invert-paths (rewrites history in ) +push — force-push a scrubbed bare clone (--mirror if bare, else --force --all) EOF } @@ -40,6 +45,9 @@ _audit() { if [ -n "${SCRUB_PICKAXE:-}" ]; then git -C "$CLONE" log -1 -S "$SCRUB_PICKAXE" --oneline --all -- "$TARGET_PATH" 2>/dev/null || true fi + echo "" + echo "=== Tags (re-apply after scrub if SHAs change) ===" + git -C "$CLONE" tag -l 'v1.*' 2>/dev/null || true } _scrub() { @@ -51,11 +59,25 @@ _scrub() { git -C "$CLONE" filter-repo --path "$TARGET_PATH" --invert-paths --force echo "" echo "Done. Verify with: git -C \"$CLONE\" log --oneline --all -- $TARGET_PATH" - echo "Then force-push the mirror and re-apply tags as needed." + _audit +} + +_push() { + [ -n "$REMOTE" ] || { echo "ERROR: remote URL required for push" >&2; exit 1; } + if [ -f "$CLONE/HEAD" ] && [ -d "$CLONE/objects" ]; then + echo "Force-pushing branches and tags to $REMOTE ..." + git -C "$CLONE" push --force "$REMOTE" 'refs/heads/*:refs/heads/*' + git -C "$CLONE" push --force "$REMOTE" 'refs/tags/*:refs/tags/*' + else + echo "Force-pushing all refs from working clone to $REMOTE ..." + git -C "$CLONE" push --force --all "$REMOTE" + git -C "$CLONE" push --force --tags "$REMOTE" + fi } case "$MODE" in audit) [ -n "$CLONE" ] || { _usage; exit 1; }; _audit ;; scrub) [ -n "$CLONE" ] || { _usage; exit 1; }; _scrub ;; + push) [ -n "$CLONE" ] || { _usage; exit 1; }; _push ;; *) _usage; exit 1 ;; esac diff --git a/tests/leakage-shapes.bats b/tests/leakage-shapes.bats new file mode 100644 index 0000000..3a08b85 --- /dev/null +++ b/tests/leakage-shapes.bats @@ -0,0 +1,57 @@ +#!/usr/bin/env bats +# Tests for scripts/check-leakage-shapes.sh — secret-free structural scan. + +load 'test_helper' + +@test "check-leakage-shapes.sh exists and is executable" { + [ -f "$CORE_DIR/scripts/check-leakage-shapes.sh" ] + [ -x "$CORE_DIR/scripts/check-leakage-shapes.sh" ] +} + +@test "clean tree exits 0" { + run bash "$CORE_DIR/scripts/check-leakage-shapes.sh" "$CORE_DIR" + [ "$status" -eq 0 ] +} + +@test "in-tree leakage-tokens.txt is flagged" { + mkdir -p "$SCRATCH/bad/scripts" + echo "xyzzy" > "$SCRATCH/bad/scripts/leakage-tokens.txt" + run bash "$CORE_DIR/scripts/check-leakage-shapes.sh" "$SCRATCH/bad" + [ "$status" -eq 1 ] + [[ "$output" == *"leakage-tokens.txt"* ]] + [[ "$output" == *"content withheld"* ]] +} + +@test "dotfiles-guard material in tree is flagged" { + mkdir -p "$SCRATCH/bad/dotfiles-guard" + echo "marker" > "$SCRATCH/bad/dotfiles-guard/company-context" + run bash "$CORE_DIR/scripts/check-leakage-shapes.sh" "$SCRATCH/bad" + [ "$status" -eq 1 ] + [[ "$output" == *"dotfiles-guard"* ]] +} + +@test "example.com email in skill prose is allowed" { + mkdir -p "$SCRATCH/ok/.claude/skills/demo" + printf '%s\n' '---' 'name: demo' '---' 'Contact user@example.com for help.' \ + > "$SCRATCH/ok/.claude/skills/demo/SKILL.md" + run bash "$CORE_DIR/scripts/check-leakage-shapes.sh" "$SCRATCH/ok" + [ "$status" -eq 0 ] +} + +@test "non-safe corporate email shape in skill prose is flagged" { + mkdir -p "$SCRATCH/bad/.claude/skills/demo" + printf '%s\n' '---' 'name: demo' '---' 'Escalate to owner@acme-corp.com immediately.' \ + > "$SCRATCH/bad/.claude/skills/demo/SKILL.md" + run bash "$CORE_DIR/scripts/check-leakage-shapes.sh" "$SCRATCH/bad" + [ "$status" -eq 1 ] + [[ "$output" == *"SKILL.md"* ]] + [[ "$output" == *"content withheld"* ]] +} + +@test "failure output never echoes the matched email" { + mkdir -p "$SCRATCH/bad/.claude/agents" + echo 'Reach dev@secret-org.io for access.' > "$SCRATCH/bad/.claude/agents/demo.md" + run bash "$CORE_DIR/scripts/check-leakage-shapes.sh" "$SCRATCH/bad" + [ "$status" -eq 1 ] + [[ "$output" != *"secret-org.io"* ]] +}