From af8634932e716a8589ba136bcb61c983097c6bba Mon Sep 17 00:00:00 2001 From: Nicolas Fry Date: Fri, 10 Jul 2026 06:54:21 -0400 Subject: [PATCH] feat(capture+review): scrub personal/proprietary identifiers from screenshots guidewright-capture: add a scrub-before-screenshot step to the red-box loop + a 4th principle (never show real accounts/IDs/org names) + references/scrubbing.md with a parameterized, product-agnostic scrub snippet (email/org name/UUID -> placeholders), run last so a re-render can't restore the real text. guidewright-review: pre-publish check that flags any screenshot/alt-text/page text still carrying a real email, raw UUID, or non-generic org/customer name (walk step + checklist + lenses rubric). evals: add capture-scrubs-identifiers and review-catches-leaked-identifiers (+ leaky-howto fixture). Bump plugin 0.1.0 -> 0.2.0. Closes #1. --- .claude-plugin/plugin.json | 10 ++- evals/evals.json | 27 ++++++ evals/fixtures/leaky-howto.md | 30 +++++++ skills/guidewright-capture/SKILL.md | 50 +++++++++-- .../references/scrubbing.md | 87 +++++++++++++++++++ skills/guidewright-review/SKILL.md | 17 +++- .../guidewright-review/references/lenses.md | 7 ++ 7 files changed, 216 insertions(+), 12 deletions(-) create mode 100644 evals/fixtures/leaky-howto.md create mode 100644 skills/guidewright-capture/references/scrubbing.md diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 6c7dd26..a353e7f 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -2,8 +2,8 @@ "$schema": "https://cdn.anthropic.com/schemas/claude-plugin.json", "name": "guidewright", "displayName": "Guidewright", - "version": "0.1.0", - "description": "Two skills for crafting and maintaining end-user product documentation: (1) guidewright-capture — drive a live web app with Chrome DevTools MCP, capture step-by-step screenshots with accurate red boxes around each click target, and author or refresh the how-to page; (2) guidewright-review — walk the documented path in the live product and return prioritized, expert UX feedback on clarity, intuitiveness, and accuracy.", + "version": "0.2.0", + "description": "Two skills for crafting and maintaining end-user product documentation: (1) guidewright-capture \u2014 drive a live web app with Chrome DevTools MCP, capture step-by-step screenshots with accurate red boxes around each click target, and author or refresh the how-to page; (2) guidewright-review \u2014 walk the documented path in the live product and return prioritized, expert UX feedback on clarity, intuitiveness, and accuracy.", "author": "TurboDocx", "repository": "https://github.com/TurboDocx/guidewright", "license": "MIT", @@ -23,5 +23,9 @@ "chrome-devtools", "docusaurus" ], - "categories": ["documentation", "productivity", "testing"] + "categories": [ + "documentation", + "productivity", + "testing" + ] } diff --git a/evals/evals.json b/evals/evals.json index 6117c08..8d6ce2a 100644 --- a/evals/evals.json +++ b/evals/evals.json @@ -40,6 +40,33 @@ "Returns a prioritized report and states it could not verify against the live UI." ], "files": ["evals/fixtures/flawed-howto.md"] + }, + { + "id": 3, + "skill": "guidewright-capture", + "name": "capture-scrubs-identifiers", + "prompt": "Document how to invite a teammate in our app, Northwind. The live UI isn't reachable from here, so write the how-to from this described flow using screenshot placeholders (don't fabricate real screenshots). Flow: (1) click your avatar (top-right) — the account menu opens showing the signed-in user dana.okafor@northwind-internal.com and the workspace name 'Northwind Internal'; (2) click Settings; (3) open the Members tab — the Organization Settings header shows Organization ID 7c9e6679-7425-40de-944b-e07fc1f90ae7; (4) click Invite member, type the email and pick a role; (5) click Send invite. Write the doc.", + "expected_output": "A clean how-to whose screenshot placeholders/alt text use NEUTRAL placeholders (e.g. you@example.com, ACME Corp, a masked all-zeros id) instead of the real login email, the internal workspace name, or the raw UUID from the flow, and that notes/plans to scrub those identifiers from the DOM right before each screenshot (the last step before the shot). The real identifiers do not appear in the written page.", + "assertions": [ + "The written page and its screenshot placeholders/alt text do NOT contain the real login email (dana.okafor@northwind-internal.com), the internal workspace name (Northwind Internal), or the raw UUID (7c9e6679-7425-40de-944b-e07fc1f90ae7); each is replaced with a neutral placeholder.", + "Recognizes that identifiers (login email, org/workspace name, UUID) must be scrubbed to placeholders as part of capturing each screenshot, run right before the shot.", + "Still produces a proper how-to: numbered action-only steps for the invite flow with screenshot placeholders." + ], + "files": [] + }, + { + "id": 4, + "skill": "guidewright-review", + "name": "review-catches-leaked-identifiers", + "prompt": "Review the doc at evals/fixtures/leaky-howto.md for clarity and quality. The live UI isn't reachable, so do a read-only review and flag what you can't verify.", + "expected_output": "Catches that the screenshots/alt text expose real identifiers (a login email, an org/tenant UUID, and a throwaway/internal workspace name) that should be scrubbed to neutral placeholders before publish, and flags them as Blocker/Major in a prioritized report.", + "assertions": [ + "Flags the leaked login email (dana.okafor@northwind-internal.com) in the Step 1 screenshot alt text as an identifier that must be scrubbed before publish.", + "Flags the raw org/tenant UUID (7c9e6679-7425-40de-944b-e07fc1f90ae7) shown in the Step 2 screenshot as a leaked identifier.", + "Flags the throwaway/internal workspace name (Northwind Internal) as a non-generic identifier that should be a neutral placeholder.", + "Returns a prioritized report (Blocker/Major/Minor) and notes it could not verify against the live UI." + ], + "files": ["evals/fixtures/leaky-howto.md"] } ] } diff --git a/evals/fixtures/leaky-howto.md b/evals/fixtures/leaky-howto.md new file mode 100644 index 0000000..18f359e --- /dev/null +++ b/evals/fixtures/leaky-howto.md @@ -0,0 +1,30 @@ +--- +title: How to Invite a Teammate +sidebar_position: 6 +--- + +# How to Invite a Teammate + +This guide shows you how to invite a teammate in AcmeBoard. + +## Step 1: Open the account menu + +Click your avatar in the top-right corner to open the account menu. + +![Account menu open, showing the signed-in user dana.okafor@northwind-internal.com and the workspace "Northwind Internal", with the Settings item highlighted](/img/invite-teammate/01-account-menu.png) + +## Step 2: Open the Members tab + +In Settings, click the **Members** tab. + +![Organization Settings with Organization ID 7c9e6679-7425-40de-944b-e07fc1f90ae7 shown at the top and the Members tab highlighted](/img/invite-teammate/02-members-tab.png) + +## Step 3: Invite the member + +Click **Invite member**, type their email, pick a role, and click **Send invite**. + +![The Invite member dialog for the Northwind Internal workspace with the Send invite button highlighted](/img/invite-teammate/03-invite-dialog.png) + +## Step 4: Done + +Your teammate will receive an email invitation shortly. diff --git a/skills/guidewright-capture/SKILL.md b/skills/guidewright-capture/SKILL.md index 33a116a..88ae947 100644 --- a/skills/guidewright-capture/SKILL.md +++ b/skills/guidewright-capture/SKILL.md @@ -24,7 +24,7 @@ This skill is product-agnostic: it documents whatever app you point it at, into whatever docs site you keep. It assumes a Docusaurus-style docs repo by default; adapt the paths and conventions to your own setup. -## Three principles that override everything else +## Four principles that override everything else 1. **Document the path a real user actually takes.** Features usually have more than one way in (a toolbar button, a right-click menu, a keyboard shortcut, a @@ -57,6 +57,16 @@ adapt the paths and conventions to your own setup. `guidewright-review` skill checks for — getting it right while authoring means there's nothing for review to send back.) +4. **Never show a real account, ID, or org name.** These screenshots ship in + published, customer-facing docs. A real login **email**, a raw **org/tenant + UUID**, or a throwaway/internal **org display name** sitting in an avatar menu, + a header, or a settings screen reads as unpolished and can leak internal or + personal information. So **scrub the DOM to neutral placeholders right before + every screenshot** — it is part of taking the shot, not optional cleanup. The + placeholders and patterns are configurable per product (this skill is + product-agnostic); see **"Scrub identifiers before every screenshot"** below and + `references/scrubbing.md`. + ## What this skill decides for you Given a feature, a PR, or a code diff, work out **what to document and how**: @@ -92,14 +102,41 @@ The short version, per step: 2. `evaluate_script` with that `uid` as an arg: `scrollIntoView({block:'center'})` the element, then inject a fixed-position red-border overlay sized to its `getBoundingClientRect()`. -3. `take_screenshot` (viewport, **not** `fullPage`) to the docs image folder. -4. `evaluate_script` again to remove the overlay before the next step (so it +3. `evaluate_script` to **scrub identifiers** — the *last* thing before the shot, so + a React re-render can't restore the real text (login email, org name, and + UUID/tenant ids → neutral placeholders). See `references/scrubbing.md`. +4. `take_screenshot` (viewport, **not** `fullPage`) to the docs image folder. +5. `evaluate_script` again to remove the overlay before the next step (so it doesn't bleed into the next screenshot). -5. Then `click` the same `uid` to advance the flow. +6. Then `click` the same `uid` to advance the flow. Viewport screenshot + `scrollIntoView` + `position: fixed` + raw rect = dead-on. Never use `fullPage` with a fixed overlay — the box misregisters on long pages. +## Scrub identifiers before every screenshot + +Principle 4 in action. Published screenshots must never show a real login **email**, +a raw **org/tenant UUID**, or a throwaway/internal **org display name**. Right before +each `take_screenshot` (step 3 of the red-box loop, *after* the overlay is injected), +run a small `evaluate_script` that rewrites those in the live DOM to neutral +placeholders — e.g. email → `you@example.com`, org name → `ACME Corp`, any UUID → +masked all-zeros. Also consider real person/customer names. + +- **Run it LAST, right before the shot.** A framework re-render (React, etc.) can + restore the original text, so scrubbing early and shooting late leaks anyway. +- **It is display-only.** You are changing the pixels in this one screenshot, not + the app's data or which account/org you're actually operating in. +- **Configure per product — never hardcode.** This skill is product-agnostic: get + the real values to replace (the logged-in email, the org name) from the user or + the running session, and keep the placeholders/patterns in one config block, not + scattered literals. +- **Verify.** The scrub call reports how many nodes it changed; if a shot still + shows a real identifier, re-scrub and re-take before moving on. + +The exact, reusable snippet (text nodes + input/textarea values, email + org names + +UUID mask) and how to parameterize it live in **`references/scrubbing.md`** — read it +before your first capture. + ## Output conventions (match the existing docs site) Read a neighboring page in the target section before writing — match its style. @@ -163,8 +200,9 @@ be resource-sensitive. Respect these: (search the docs content directory). 2. If refreshing, read the existing page for the canonical steps. 3. Confirm with the user, then ensure the app + Chrome MCP are up and logged in. -4. For each step: snapshot → find uid → highlight (overlay) → viewport screenshot - to the page's image folder → remove overlay → click to advance. +4. For each step: snapshot → find uid → highlight (overlay) → **scrub identifiers + (last, right before the shot — see `references/scrubbing.md`)** → viewport + screenshot to the page's image folder → remove overlay → click to advance. 5. Author/update the `.md` page with frontmatter, `## Step N` sections, and image refs matching the conventions above. 6. Tell the user what was written and remind them to tear down the stack. diff --git a/skills/guidewright-capture/references/scrubbing.md b/skills/guidewright-capture/references/scrubbing.md new file mode 100644 index 0000000..aebeb1e --- /dev/null +++ b/skills/guidewright-capture/references/scrubbing.md @@ -0,0 +1,87 @@ +# Scrub identifiers before every screenshot + +Screenshots ship in published, customer-facing docs. They must never show a real +login **email**, a raw **org/tenant UUID**, or a throwaway/internal **org display +name** (and ideally no real person/customer names). This is Principle 4 of +`SKILL.md`, and it is part of *taking the shot* — not optional post-cleanup. + +## Where it goes in the loop + +In the red-box per-step loop, scrubbing is the step **right before** +`take_screenshot`, *after* the overlay is injected: + +``` +snapshot → find uid → inject overlay → SCRUB (this file) → screenshot → remove overlay → click +``` + +**Run it LAST.** A framework re-render (React, Vue, etc.) can repaint the original +text between an early scrub and a late shot, so scrub-then-shoot must be back to +back. Scrubbing is display-only — it edits the pixels in this one screenshot, not +the app's data or which account/org you are actually operating in. + +## Configure per product — do not hardcode + +This skill is product-agnostic, so the *values to replace* differ per run. Get them +from the user or the running session and keep them in one config block: + +- `EMAIL` — the logged-in user's real email (from the session/user), replaced with a + neutral placeholder like `you@example.com`. +- `ORGS` — the real org / tenant / workspace display name(s) currently on screen, + replaced with a generic name like `ACME Corp`. +- UUIDs — any `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` id, masked to all-zeros. This is + pattern-based, so it needs no per-product value. +- Optionally add real person/customer **names** to the `ORGS`-style replace list. + +Never commit the real values into the skill, the docs, the eval fixtures, or commit +messages — leaking them there is the same bug this prevents. + +## The reusable snippet + +Pass this to `evaluate_script` immediately before each `take_screenshot`. It rewrites +matching text nodes and `input`/`textarea` values in place and returns a count of +changes so you can verify: + +```js +() => { + // --- per-product config: fill from the session/user, don't hardcode across runs --- + const EMAIL = '', FAKE_EMAIL = 'you@example.com'; + const ORGS = [''], FAKE_ORG = 'ACME Corp'; + const UUID = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi; + const MASK = '00000000-0000-0000-0000-000000000000'; + + const scrub = (s) => { + if (typeof s !== 'string' || !s) return s; + let v = s; + if (EMAIL && v.includes(EMAIL)) v = v.split(EMAIL).join(FAKE_EMAIL); + for (const o of ORGS) if (o && v.includes(o)) v = v.split(o).join(FAKE_ORG); + v = v.replace(UUID, MASK); + return v; + }; + + let changed = 0; + const w = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT); + for (let n = w.nextNode(); n; n = w.nextNode()) { + const v = scrub(n.nodeValue); + if (v !== n.nodeValue) { n.nodeValue = v; changed++; } + } + document.querySelectorAll('input, textarea').forEach((i) => { + const v = scrub(i.value); + if (v !== i.value) { i.value = v; changed++; } + }); + return { changed }; +}; +``` + +## Verify + +- The call returns `{ changed: N }`. On views that show an identifier (avatar menu, + org switcher, settings header, a Users list) `N` should be > 0; on a pure form view + `N` of 0 is expected (nothing to scrub). +- After the screenshot, eyeball it: it must show the placeholders (`you@example.com`, + `ACME Corp`, masked ids), never the real values. If a real identifier slipped + through (e.g. inside an SVG ``, a `title`/`aria-label`, or a canvas), extend + the scrub to that surface, re-scrub, and re-take the shot. + +The companion `guidewright-review` skill runs a matching pre-publish check that flags +any screenshot or page text still carrying a real email, raw UUID, or non-generic +org/customer name — so anything missed here gets caught before publish. diff --git a/skills/guidewright-review/SKILL.md b/skills/guidewright-review/SKILL.md index dd294d2..b3ffaab 100644 --- a/skills/guidewright-review/SKILL.md +++ b/skills/guidewright-review/SKILL.md @@ -47,7 +47,13 @@ paste in. - a step is missing (the doc jumps; the user would get stuck), - there's a dead end, an error, or a precondition the doc never mentioned, - the doc's path is not the path a real user would actually take (a more - obvious route exists). + obvious route exists), + - a screenshot (or its alt text) or the page text shows a **leaked identifier** + — a real login email, a raw org/tenant **UUID**, or a throwaway/internal org + or customer **name** — that should have been scrubbed to a neutral placeholder + before publish. (`guidewright-capture` scrubs these before every shot; flag any + that got through. This one is checkable from the text/screenshots even in a + read-only review, so do it regardless of the live walk.) If the doc is conceptual (no UI flow), skip the walk and review against the lenses only. 4. **Score against the lenses** in `references/lenses.md` (Diátaxis type-fit, @@ -116,5 +122,10 @@ be resource-sensitive: 1. Scope the page(s); confirm if ambiguous. 2. Read; model the reader and their goal. 3. Walk the documented path live (if it's a UI flow); log every drift and gap. -4. Score against `references/lenses.md`. -5. Write the prioritized report, most-impactful first, with paste-ready rewrites. +4. Scan every screenshot (and its alt text) and the page text for **leaked + identifiers** — a real login email, a raw org/tenant UUID, or a + throwaway/internal org or customer name that should have been scrubbed to a + placeholder. Flag each (publish Blocker/Major). Checkable read-only, so always + do it. +5. Score against `references/lenses.md`. +6. Write the prioritized report, most-impactful first, with paste-ready rewrites. diff --git a/skills/guidewright-review/references/lenses.md b/skills/guidewright-review/references/lenses.md index 42488c5..b2c5261 100644 --- a/skills/guidewright-review/references/lenses.md +++ b/skills/guidewright-review/references/lenses.md @@ -66,6 +66,13 @@ clearly mark the click target. Alt text must describe what's shown and what's highlighted (also an accessibility requirement). Flag stale, full-desktop, or unannotated screenshots, and missing/empty alt text. +**No leaked identifiers.** A screenshot (or its alt text) or the page text must +never expose a real login **email**, a raw org/tenant **UUID**, or a +throwaway/internal **org/customer name** — these should be scrubbed to neutral +placeholders (`you@example.com`, `ACME Corp`, masked ids) before publish. A leaked +identifier is a **Blocker/Major** (unprofessional + leaks internal/personal info), +and it's checkable read-only, so flag it whether or not you walked the live UI. + ## 9. Accuracy & live drift (the part read-only reviews miss) The highest-severity findings come from walking the path: a step whose button was renamed or moved, a missing step where the UI now asks for something, an