From 922b4f3aaeab670ae81a0f08fe4a9445066c4432 Mon Sep 17 00:00:00 2001 From: Omar Eid Date: Mon, 13 Jul 2026 12:55:31 -0500 Subject: [PATCH] feat(skills): privacy-review skill + end-of-session Stop hook Encode the hard-won lesson that premium gating hides the BODY, not the FACT: the title, slug/URL, description, premium_teaser, tags, and any listing README all ship PUBLIC in clear. A gated "Targeting Director at LangChain" doc still broadcasts a job search via its public fields. - privacy-review skill: the mental-model table (what premium encrypts vs what ships public), the two-pass review (clear-fields leak audit + third-party-lens read as employer/competitor/recruiter/search engine), the dispositions (publish / de-identify into a case study / draft:true / keep off the repo + purge git), and the purge procedure. - privacy-review-reminder.sh Stop hook: fires at session end when premium/career/personal content was touched and no privacy review ran, so nothing sensitive ships by accident. Ships a --selftest (3 assertions) per the every-hook-proves-it-bites tenet; registered in settings.json. - manage-premium-content: a prominent "premium is NOT a privacy guarantee" cross-link at the top. Verified: hook --selftest passes, make validate-hook-tests green (33 hooks), and 3 live-fire tests (fires on sensitive+no-review, silent otherwise). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01WRtyLha5c6gN3VJ6cEqRPu --- .claude/hooks/privacy-review-reminder.sh | 67 ++++++++++ .claude/settings.json | 5 + .../skills/manage-premium-content/SKILL.md | 8 ++ .claude/skills/privacy-review/SKILL.md | 124 ++++++++++++++++++ 4 files changed, 204 insertions(+) create mode 100755 .claude/hooks/privacy-review-reminder.sh create mode 100644 .claude/skills/privacy-review/SKILL.md diff --git a/.claude/hooks/privacy-review-reminder.sh b/.claude/hooks/privacy-review-reminder.sh new file mode 100755 index 000000000..39768c4d5 --- /dev/null +++ b/.claude/hooks/privacy-review-reminder.sh @@ -0,0 +1,67 @@ +#!/bin/bash +# Stop hook: if this session touched PERSONAL / CAREER / EMPLOYER-CONFIDENTIAL or premium:true +# content, nudge to run the privacy-review skill BEFORE anything gets published/deployed, so a +# sensitive doc is never shipped by accident. The load-bearing lesson: premium gating encrypts the +# BODY only; the title, slug/URL, description, teaser, tags, and any listing README ship in CLEAR. +# Premium hides the CONTENT, not the FACT. Advisory; always exits 0 (never blocks). +# +# It reads the session transcript (path on stdin from the Stop payload) and fires ONLY when +# sensitive-content signals appear AND the privacy-review skill was NOT already run this session. +# +# --selftest (MUST be BEFORE `input=$(cat)`, or it hangs waiting on stdin): proves the hook FLAGS a +# transcript with a premium/career signal, and IGNORES a transcript with neither. + +if [ "$1" = "--selftest" ]; then + SELFTEST_HOOK="$0" + # shellcheck source=lib/selftest.sh + source "$(dirname "$0")/lib/selftest.sh" + + # Helper: write a temp transcript file and emit the Stop payload pointing at it. + _mk() { local f; f=$(mktemp); printf '%s' "$1" > "$f"; printf '{"transcript_path":"%s"}' "$f"; } + + # BAD (should FLAG): a transcript that marks a doc premium AND is about a career/job doc, with NO + # privacy-review mention โ†’ the hook must print the reminder to stderr. + assert_flags "$(_mk 'editing docs/journey/career-development/foo.mdx with premium: true and job-search outreach')" \ + 'privacy.review' 'flags premium career content with no review' + + # CLEAN (should IGNORE): an unrelated transcript (no sensitive signal) โ†’ stays quiet. + assert_ignored "$(_mk 'refactored the FlowDiagram component and fixed a mermaid selector')" \ + 'ignores unrelated session' + + # CLEAN (should IGNORE): sensitive signal present BUT the privacy-review skill already ran โ†’ quiet. + assert_ignored "$(_mk 'career-development premium: true doc; ran privacy-review skill and kept it off the repo')" \ + 'ignores when privacy-review already ran' + + selftest_report; exit $? +fi + +input=$(cat) +transcript=$(printf '%s' "$input" | jq -r '.transcript_path // empty') + +# No transcript โ†’ can't tell; stay silent. +[ -n "$transcript" ] && [ -f "$transcript" ] || exit 0 + +# Did this session touch SENSITIVE content? Signals: a premium mark, or career/job-search/personal +# vocabulary near content edits. Kept broad on purpose (a false nudge is cheap; a missed leak is not). +if ! grep -qiE 'premium:[[:space:]]*true|premium_teaser|career-development|job.?search|\brecruiter\b|outreach|(personal|confidential)[- ]?(finance|health|family|material)|resume|cover letter' "$transcript" 2>/dev/null; then + exit 0 +fi + +# Already reviewed this session? If the privacy-review skill was invoked/referenced, assume the +# loop closed and stay quiet to avoid nagging. +if grep -qiE 'privacy.review|could an employer see|premium hides the .*not the fact' "$transcript" 2>/dev/null; then + exit 0 +fi + +cat >&2 <<'EOF' +๐Ÿ” privacy-review reminder: this session touched sensitive/personal/premium content. + Before publishing, un-drafting, or deploying it, run the privacy-review skill. + Remember the mental model: premium gating encrypts the BODY only. The title, slug/URL, + description, premium_teaser, tags, and any README that LISTS the doc all ship PUBLIC in + clear. Premium hides the CONTENT, not the FACT (a gated "Targeting Director at LangChain" + doc still broadcasts the job search via its title + URL + teaser). + Two passes: (1) audit the clear-fields for leaks; (2) read it as an employer / competitor / + recruiter / search engine. Then pick a disposition: publish ยท de-identify into a case study ยท + draft:true ยท or keep OFF the repo (local file + purge git). This is advisory; it will not block. +EOF +exit 0 diff --git a/.claude/settings.json b/.claude/settings.json index 776bbf24f..eac00ea6d 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -211,6 +211,11 @@ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/mermaid-render-check.sh\"", "timeout": 90 + }, + { + "type": "command", + "command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/privacy-review-reminder.sh\"", + "timeout": 15 } ] } diff --git a/.claude/skills/manage-premium-content/SKILL.md b/.claude/skills/manage-premium-content/SKILL.md index 4100e4d96..59040f4a6 100644 --- a/.claude/skills/manage-premium-content/SKILL.md +++ b/.claude/skills/manage-premium-content/SKILL.md @@ -7,6 +7,14 @@ description: The editorial POLICY for premium/gated content on the Bytes of Purp This skill owns **whether** content should be gated โ€” the judgement, not the mechanics. +> โš ๏ธ **Premium is NOT a privacy guarantee.** Gating encrypts the rendered BODY only; the `title`, +> `slug`/URL, `description`, `premium_teaser`, `tags`, and any README that LISTS the doc all ship +> PUBLIC in clear. Premium hides the CONTENT, not the FACT (a gated "Targeting Director at LangChain" +> doc still broadcasts a job search via its title + URL + teaser). If the *existence* or *subject* is +> the sensitive thing (career/job-search, employer-confidential, personal), premium is the WRONG tool +> โ€” run the **`privacy-review`** skill and consider `draft: true`, de-identifying into a case study, or +> keeping it OFF the repo entirely. + - **How to MARK a doc premium** (frontmatter, ``, build) โ†’ `author-post`. - **How premium is ENFORCED** (compile-time encryption, the blocking deploy gate) โ†’ `validate-docs-structure.js` V1 + `scripts/verify-premium-encrypted.js` V5; architecture diff --git a/.claude/skills/privacy-review/SKILL.md b/.claude/skills/privacy-review/SKILL.md new file mode 100644 index 000000000..79226de51 --- /dev/null +++ b/.claude/skills/privacy-review/SKILL.md @@ -0,0 +1,124 @@ +--- +name: privacy-review +description: Before publishing personal, career, or otherwise sensitive content, review what a THIRD PARTY (an employer, a competitor, a recruiter, a search engine) could infer from what ships PUBLIC, even when the body is premium-gated. The load-bearing mental model, premium gating encrypts the rendered BODY only; the title, slug/URL, description (og/meta/share), premium_teaser, tags, and any README/index that LISTS the doc all ship in CLEAR to everyone. Premium hides the CONTENT, not the FACT. So a gated "Targeting Director at LangChain" doc still tells the world you are job-hunting at LangChain via its title, URL, and teaser. TRIGGERS before publishing/un-drafting/deploying any career, job-search, personal-finance, health, family, employer-confidential, or otherwise sensitive doc; when marking something premium:true; or when the user asks "could an employer see this?", "is this safe to publish?", "will this leak?", "privacy review". Runs a CLEAR-FIELDS leak audit + a THIRD-PARTY-lens read, then recommends: publish as-is, de-identify into a generic case study, draft:true (recover but do not ship), or keep OFF the repo entirely (hand over as a local file + purge git). Pairs with manage-premium-content (the premium how/why), publish-site (the un-draft gate), deploy-site (V5 body-leak gate). A Stop hook (privacy-review-reminder) nudges at session end when sensitive content was touched. +--- + +# Privacy review (what actually ships public) + +Before any sensitive doc goes live, ask the one question that is easy to get wrong: **what can a +third party learn from this, even if the body is locked?** This skill encodes the mental model and +the checklist so the answer is never a lucky guess. + +The lesson this skill exists to prevent (a real catch): a `premium: true` career doc titled +"Targeting: Director, Deployed Engineering (LangChain)" would have shipped, and although the *body* +was encrypted, the **title, URL, description, and teaser were all public in clear**, broadcasting an +active job search at a named company (and, in the body's own frontmatter/adjacent README, the current +employer). Premium gating hid the drafts, not the fact. + +## The mental model: premium gating encrypts the BODY, nothing else + +The source of truth is `plugins/rehype-premium-encrypt.js`: for a `premium: true` doc it encrypts +**only the rendered body HTML** (keeping plaintext out of both the HTML and the JS bundle). Everything +else is untouched and PUBLIC. Concretely: + +| Field / surface | Premium doc: is it PUBLIC? | Notes | +|---|---|---| +| **Body content** | ๐Ÿ”’ ENCRYPTED | The only thing gated. Anonymous readers see the teaser + lock. | +| **`title`** | ๐Ÿ”ด PUBLIC | Renders in `

`, ``, `og:title`, the sidebar, on-site search, Google. | +| **`slug` / URL** | ๐Ÿ”ด PUBLIC | The path itself is readable (and often descriptive). | +| **`description`** | ๐Ÿ”ด PUBLIC | Feeds `og:description` (SEO + social preview) AND the ShareButton text. | +| **`premium_teaser`** | ๐Ÿ”ด PUBLIC | Shown in clear ABOVE the lock, by design. Falls back to `description` if unset. | +| **`tags`** | ๐Ÿ”ด PUBLIC | Tag pages list the doc; reinforce the topic (e.g. `job-search`). | +| **A README / hub that LISTS the doc** | ๐Ÿ”ด PUBLIC (unless it too is premium) | The index page's link text + framing prose ship in clear. | +| **`draft: true`** | โœ… NOT SHIPPED | The whole doc (incl. frontmatter) is excluded from the prod build. | + +> **Premium hides the CONTENT, not the FACT.** If the *existence* or *subject* of the doc is the +> sensitive thing, premium is the wrong tool. Use `draft: true` (recover but never ship), de-identify +> the subject, or keep it off the repo entirely. + +## The review: two passes + +### Pass 1, the CLEAR-FIELDS leak audit + +For the doc (and any README/hub that lists it), read ONLY the public-in-clear surfaces and ask what +each one reveals to someone who never signs in: + +- `title` ยท `slug`/URL ยท `description` ยท `premium_teaser` ยท `tags` ยท the listing README's link text + + framing. + +Flag anything that names or implies: a **target** (a company/role you want), your **current +employer** or confidential project, a **named person** (recruiter, colleague, customer), **customer +names**, **compensation**, **health/finance/family** specifics, or simply the **fact** that you are +doing something private (job-hunting, leaving, negotiating). + +### Pass 2, the THIRD-PARTY-lens read + +Read the whole thing as each adversarial reader in turn and write down what they would conclude: + +- **Your current employer** finds this page. What do they learn? (Are you looking to leave? Did you + expose internal metrics, roadmap, or customer names?) +- **The target company / a competitor** finds it. What do they learn about your position, your + leverage, your other options? +- **A named third party** (recruiter, customer, colleague) finds themselves quoted or named. Are they + OK being here? Did you publish a private message? +- **A search engine / an LLM** indexes the public fields. What does a search for your name, or the + company, surface? + +If any answer is "something I would not say to that person's face on a public stage," it is a leak. + +## The dispositions (pick one, most-protective first) + +1. **Keep it OFF the repo entirely.** For raw personal or employer-confidential material (customer + names, internal metrics, comp, a named recruiter, private messages), do not publish it in any form. + Save it as a LOCAL file outside the repo, close/don't-open the PR, delete the branch (local + + remote), and **purge the git object store** so it is unrecoverable (see "Purge" below). This is the + right call more often than it feels. +2. **De-identify into a generic case study.** If the METHOD is teachable but the SPECIFICS are + sensitive, rewrite so a reader learns the craft without learning your private situation: replace the + real target/employer/customers/people/comp with an invented archetype, shift present-tense + "targeting X" to illustrative past, retitle to a durable-skill frame. A *partial* scrub is worse + than none (it reads as a leak someone tried to hide), so de-identify THOROUGHLY or don't. +3. **`draft: true`.** Recover the content onto master but keep it OFF the live site (drafts are + excluded from the prod build). Use when you want it version-controlled but not yet (or never) + public. NOTE: the source still lives in the public repo's git history, so this protects against the + LIVE SITE, not against someone reading the repo. For repo-secret material, use disposition 1. +4. **Publish as-is.** Only when Pass 1 and Pass 2 both come back clean, no sensitive clear-field, no + adversarial reader learns anything you would not say publicly. + +## Purge (when a sensitive doc must leave the repo) + +Getting a doc out of a branch is not enough; it lingers as dangling git objects until collected. + +1. Save the content to a LOCAL file OUTSIDE the repo FIRST (e.g. `~/Desktop/...`), verify it is intact. +2. Close the PR; `git checkout master`; delete the branch local (`git branch -D`) AND remote + (`git push origin --delete <branch>`). +3. Purge the object store: `git reflog expire --expire=now --all` then `git gc --prune=now`. Verify + the commit no longer resolves (`git cat-file -e <sha>` fails) and the file is unretrievable. +4. **Confirm it never reached the live site**: check `gh-pages` (`git ls-tree -r gh-pages | grep <slug>`) + and the built `build/` dir. Distinguish the real doc from incidental TAG pages (a company used as a + tech *tag* on other posts produces a `tags/<x>.html` that is NOT your doc). + +> If sensitive content was ever pushed to a SHARED remote branch that others may have fetched, deleting +> the branch does not un-fetch it. Treat any exposed secret (a real credential, a customer contract) as +> compromised: rotate/notify, don't just scrub (mirrors the repo's "rotate, don't scrub" rule). + +## How to run it + +1. Identify the doc(s) and any README/hub listing them. +2. **Pass 1** (clear-fields audit) then **Pass 2** (third-party-lens read); write down each finding. +3. Recommend a disposition (most-protective first). When it is career/job-search/employer-confidential, + default to disposition 1 or 2, not "publish." +4. If the disposition is off-repo, run the **Purge** steps and verify the live site is clean. +5. If a real reusable rule emerged, capture it back into THIS skill (the mental-model table, the + adversarial-reader list) so the guard sharpens over time. + +## Cross-links + +- **`manage-premium-content`** โ€” the premium POLICY (should this be premium?) + the marking MECHANICS + (`premium: true`, teaser, the encrypt plugin, the V5 deploy gate). This skill is the PRIVACY lens on + top: premium is a tool, not a privacy guarantee. +- **`publish-site`** โ€” the un-draft + deploy step; run this review BEFORE un-drafting sensitive content. +- **`deploy-site`** โ€” the V5 gate scans built HTML+JS for premium BODY leaks; it does NOT judge whether + the public clear-fields leak the FACT. That judgment is this skill. +- **The Stop hook `privacy-review-reminder`** nudges at session end when premium/career/personal + content was touched, so an accidental publish is caught before you walk away.