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: 4 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,10 @@ Same as above, plus:
`references/` file rather than presenting it as original.
- No owner-private names, products, or internal paths in skill or agent text — this
ships to every installer. `scripts/check-private-terms.sh` enforces this against
`scripts/private-terms.txt`.
`scripts/private-terms.txt`, plus a private list the maintainers hold. A PR can
also get a warning that a line may use a private product's vocabulary: that
isn't a failure, but check the line was written for this repo and not lifted
from elsewhere.

## Running the checks locally

Expand Down
4 changes: 2 additions & 2 deletions plugins/hardening/skills/privacy-audit/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ The rule is explicit: **a privacy policy is required if any user data is collect
- Any Zod schema in a route handler with fields beyond `id`/`createdAt` — `grep -rn "z.object" --include="*.ts"` and read what each schema captures.
- Stripe customer/subscription metadata (`stripe.customers.create`, `metadata:` blocks) — Stripe stores whatever you attach.
- PostHog `capture()`/`identify()` calls — check `properties:` payloads for anything beyond anonymous event data (email, name, IP if not stripped).
- File uploads (campaign assets, avatars) — where do they land (S3/R2/local disk) and is there EXIF/metadata scrubbing?
- File uploads (user documents, avatars) — where do they land (S3/R2/local disk) and is there EXIF/metadata scrubbing?
- BYOK products: the user's own Anthropic/OpenAI API key — this is the most sensitive field in the product. Confirm it is encrypted at rest, never logged, and never returned in any API response after initial save.

2. **Build the data map.** For every field found, record: field name → source (which form/event) → storage location (table.column, PostHog event property, Stripe metadata key, log line) → retention (indefinite / N days / until account deletion) → who else sees it (third-party processor: Stripe, PostHog, email provider, Sentry/Better Stack).
- Table format works well here — one row per data category (email, name, BYOK key, IP, campaign content, payment method) with those five columns.
- Table format works well here — one row per data category (email, name, BYOK key, IP, user-created content, payment method) with those five columns.

3. **Check privacy-policy alignment.** Read the live privacy policy (or draft from `legal-docs` skill if none exists) and confirm every row in the data map is disclosed. Flag:
- Data collected but not mentioned in the policy (the common miss: PostHog session recordings, IP addresses in logs, third-party sub-processors not listed).
Expand Down
33 changes: 33 additions & 0 deletions scripts/check-private-terms.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@
# repository secret for CI on the owner's own pushes/PRs. Empty on
# fork PRs, which then run with the public list only.
#
# Warning tier: an overlay line that starts with "warn:" is a warning, not a
# failure. It is for a private product's VOCABULARY (its domain nouns), which
# names miss: a worked example lifted from the product can leak it without
# naming it. Some of those words are also ordinary English, so a hit asks a
# person to look rather than blocking the PR. In GitHub Actions each hit
# becomes a ::warning annotation on the PR (file and line only); locally the
# matching line is printed too, since a local terminal is private. Warning
# patterns never run against docs/.
#
# One extra check: docs/ (internal planning material, never shipped) is
# scanned against ONLY the local/secret overlay, never the public list --
# docs/ legitimately discusses the owner's private products by name, so the
Expand Down Expand Up @@ -64,6 +73,20 @@ fi
cat "$OVERLAY_FILE" >> "$PATTERNS_FILE"

fail=0
warned=0

report_warning() {
# $1 = grep hits (file:line:text), never echoed to CI logs with the text
echo "$1" | while IFS= read -r hit; do
file=$(echo "$hit" | cut -d: -f1)
line=$(echo "$hit" | cut -d: -f2)
if [ "${GITHUB_ACTIONS:-}" = "true" ]; then
echo "::warning file=$file,line=$line::Possible private-product vocabulary (from the private list). Check this line was not lifted from a private product."
else
echo " $hit" >&2
fi
done
}

: > "$SCAN_LIST_ALL"
: > "$SCAN_LIST_NO_README"
Expand All @@ -77,6 +100,14 @@ if [ -s "$SCAN_LIST_ALL" ]; then
while IFS= read -r pattern; do
case "$pattern" in
''|'#'*) continue ;;
warn:*)
hits=$(xargs -0 grep -rniIE "${pattern#warn:}" < "$SCAN_LIST_ALL" 2>/dev/null || true)
if [ -n "$hits" ]; then
echo "WARNING: private-product vocabulary (from the private list) found; check these lines:" >&2
report_warning "$hits"
warned=1
fi
continue ;;
esac
if [ "$pattern" = "Stylus Nexus" ]; then
scan_list="$SCAN_LIST_NO_README"
Expand Down Expand Up @@ -110,6 +141,7 @@ if [ -d docs ] && [ -s "$OVERLAY_FILE" ]; then
case "$pattern" in
''|'#'*) continue ;;
esac
case "$pattern" in warn:*) continue ;; esac
hits=$(grep -rniIE "$pattern" docs 2>/dev/null || true)
if [ -n "$hits" ]; then
echo "PRIVATE TERM (from the private list) found in docs/ at:" >&2
Expand All @@ -121,5 +153,6 @@ elif [ -d docs ]; then
echo "no private-terms overlay available -- skipping docs/ check" >&2
fi

[ "$warned" = 1 ] && echo "warnings above are not failures: confirm each line is generic, or rewrite it" >&2
[ "$fail" = 0 ] && echo "no private terms found in plugins/*/skills, plugins/*/agents, plugins/*/README.md, or docs/"
exit "$fail"
Loading