Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -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 .)

Expand All @@ -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 .

Expand Down
12 changes: 9 additions & 3 deletions PROTOCOL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
68 changes: 68 additions & 0 deletions scripts/check-leakage-shapes.sh
Original file line number Diff line number Diff line change
@@ -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
40 changes: 31 additions & 9 deletions scripts/scrub-mirror-history.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,32 @@
# pushes from managed workstations — run locally if needed).
#
# Usage:
# bash scripts/scrub-mirror-history.sh audit <mirror-clone>
# bash scripts/scrub-mirror-history.sh scrub <mirror-clone> # destructive
# bash scripts/scrub-mirror-history.sh audit <clone>
# bash scripts/scrub-mirror-history.sh scrub <clone> # destructive rewrite
# bash scripts/scrub-mirror-history.sh push <clone> <remote-url>
#
# After scrub:
# cd <mirror-clone> && git push --force --mirror <public-mirror-url>
# 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 <<EOF
Usage:
bash scripts/scrub-mirror-history.sh audit <mirror-clone>
bash scripts/scrub-mirror-history.sh scrub <mirror-clone>
bash scripts/scrub-mirror-history.sh audit <clone>
bash scripts/scrub-mirror-history.sh scrub <clone>
bash scripts/scrub-mirror-history.sh push <clone> <remote-url>

audit — list commits that touched ${TARGET_PATH}
scrub — run git filter-repo --invert-paths (rewrites history in <mirror-clone>)
scrub — run git filter-repo --invert-paths (rewrites history in <clone>)
push — force-push a scrubbed bare clone (--mirror if bare, else --force --all)
EOF
}

Expand All @@ -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() {
Expand All @@ -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
57 changes: 57 additions & 0 deletions tests/leakage-shapes.bats
Original file line number Diff line number Diff line change
@@ -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"* ]]
}
Loading