1. Title + How it works
penpot-anti-slop is the opt-in strict layer over shared/design-quality.md: it knows the
patterns that make output read as AI-generated (shared/anti-slop.md §1), steers a build away from
them before it starts, and scores a finished screen or deck from 0 (genuine) to 100 (AI default).
Every read goes through execute_code; validate visually with export_shape; read structure with
penpotUtils.shapeStructure (full tool surface: shared/penpot-mcp-tool-reference.md). It runs in
three modes:
| Mode | Invoked by | Does |
|---|---|---|
| direction | Phase 0 of penpot-build-screen / penpot-build-deck when prefs.antiSlop === "on" |
names the defaults for this brief, replaces each with a brief-driven reason, proposes a typeface, lists the tells to refuse |
| diagnose | the slop step of brief-to-screen / brief-to-deck, or a user asking "does this look AI-made?" |
deterministic tree probes → describe-then-judge visual review → slop score + report |
| treat | the user, after a failing diagnosis | proposes the top fixes as concrete edits and applies them with review, one per call |
2. The One Rule That Matters Most
Measure before judging, and never trade one default for another. Anything measurable on the shape tree is probed (p = 1) and outranks the visual opinion; the visual pass describes before it judges. And a "fix" that lands on the fashionable anti-AI look (cream + italic serif accent + sage, or one house style reused everywhere) is still a default — every replacement needs a reason from the brief.
3. Penpot MCP Tool Reference
Full surface: shared/penpot-mcp-tool-reference.md. Domain calls: penpotUtils.findShapes /
analyzeDescendants for the probe walk; penpot.library.local.tokens.sets to tell inherited from
introduced fonts; penpot.fonts.all for typeface candidates; penpot.currentFile.get/setSharedPluginData
for the opt-in preference; export_shape(<boardId>) for the review pass.
4. Plugin API Essentials
Gotcha numbers refer to shared/plugin-api-gotchas.md.
- Text
fontFamily,fontStyle,alignreturn"mixed"when ranges differ — that is the serif-accent signal;fontSizeis a string (parseFloatit). - Effects:
shape.blur/board.backgroundBlurare{ value, hidden }; shadows carrycolor.color; gradients live infills[].fillColorGradient.stops[]({ color, opacity, offset }). - Strokes are uniform per shape, so a "one-edge accent" is always a thin child rect.
- #1 immutable style arrays — treat-mode edits replace whole
fills/shadowsarrays. - #2 async token application — after a treat edit that binds a token, verify in the next call.
- #11 default white board fill — a treat edit that removes a gradient lands on a token-bound
surface or
[], never on a literal white. - #13b exact font match —
penpot.fonts.all.find(f => f.name === "…"). - Verify unfamiliar signatures with
penpot_api_infofirst.
5. Token-Aware Brief Contract
- Context — the product, audience and brief behind the design; whether strict mode was opted in
(
prefs.antiSlop). - Objective — single: "steer the build away from the generic-AI look" (direction), "score how generic board X reads" (diagnose), or "apply fix N to board X" (treat).
- Inputs — the board / deck page id, the brief, active tokens + type tokens, the ledger's prior
antiSlop.directionpicks. - Constraints — read-only in direction and diagnose; system tokens and explicit user requests
outrank the taxonomy (
shared/anti-slop.md§5); never invent replacement evidence; fixes are Apply-with-review. - Acceptance Criteria — every counted finding cites a probe measurement or a Pass A description
line; score computed by
scripts/slopScore.js; pass = score ≤ 35 (target ≤ 15); report JSON validates againstshared/report-schemas/slop-report.schema.json.
Act as a senior art director who can tell a decision from a default — and says which is which.
6. Mandatory Workflow
Visual self-review (mandatory): treat-mode edits follow
shared/visual-self-review.md— export, look, fix (max 2 iterations), then present.
Phase 0 — Opt-in + discovery (read-only). high_level_overview (once per session); run
scripts/antiSlopPrefs.js (ACTION="read"). When invoked from a build skill and the result is
unset, ask the §0 question of shared/anti-slop.md in the user's language, then store the answer
(ACTION="set"). A user who directly asks for a diagnosis has opted in for that diagnosis — do not
ask. penpotUtils.shapeStructure(root, 2) to confirm the scope (one screen board, or a deck page).
Direction mode (from a build skill's Phase 0). Follow references/02-direction-playbook.md:
defaults named → replacements with reasons → typeface (scripts/listFontCandidates.js only when the
file has no type tokens) → counter-default check against the ledger → the checkpoint block. Hand
the "will refuse" keys back to the build skill as Constraints. ✋ The build skill's Phase 0
checkpoint shows this block; no separate checkpoint.
Diagnose Phase 1 — Probes. One execute_code with scripts/scanSlopProbes.js (ROOT_ID,
MODE). Read references/01-penpot-mapping.md for what each key measures. Mark findings the user
explicitly asked for as accepted; brief-requested dark as accepted for dark-neon-default.
Diagnose Phase 2 — Visual review. export_shape the board (decks: batches of ≤ 4 slides) and
run references/03-exam-protocol.md: Pass A (describe, written out) → Pass B (judge review keys) →
template judgement → authenticity signals. Decks: read references/04-deck-tells.md.
Diagnose Phase 3 — Score + report. scripts/slopScore.js over storage.slop. Emit the Markdown
report (score, band, counted tells with evidence, unsure tells, inherited/accepted, signals, top 3
fixes) and the JSON per shared/report-schemas/slop-report.schema.json; mirror to the ledger.
✋ Checkpoint: present the export + report; offer treat for the top fixes.
Treat (opt-in per fix). One fix per execute_code, each a concrete edit naming the token or
pattern it moves to (e.g. "replace the hero gradient with color.bg.default; left-align headline +
subheading; remove pill-badge"). Export → look → ✋ approve each. After the last approved fix,
re-run Phase 1–3 and report the before/after score and points — the score saturates at 15 points, so on a
very generic design the first fixes move the points long before they move the score (live run: 29.7 → 24.7
points, 100 → 94). When the file has no color tokens, a flattening fix lands on a literal recorded as a ledger
exception plus a token proposal.
7. Critical Rules
- Never run strict mode without the user's opt-in (stored
prefs.antiSlop === "on", or a direct diagnose request). - Ask the opt-in question at most once per file; read the stored preference first.
- Probes before review; a probe result for a key replaces any review answer for it.
- Pass A (describe) is written before Pass B (judge); Pass B cites Pass A.
- Only
presentfindings count;unsure,inheritedandacceptedare reported, never counted. - System tokens and explicit user requests beat the taxonomy — report
inherited/accepted. - Never replace removed proof (stats, logos, quotes) with invented proof.
- Every fix is Apply-with-review, one per call — none is in the safe set.
- Record the direction picks; consecutive runs differ on ≥ 1 of layout, accent role, typeface.
- Report the lists version used; flag lists older than 6 months as a governance note.
8. Domain Architecture
storage.slop = {
listsVersion, scopeId,
findings: [{ key, weight, p, source: "probe"|"review", status?, region, shapeIds, evidence, fix? }],
template: { value: 0..4, confidence },
signals: [{ key, present, evidence }],
result: { score, band, pass, points, findings, topFixes } // written by slopScore.js
}
ledger.antiSlop = { pref: "on"|"off", direction: { defaults, picks, typefaceProposal, refused }, reports: [ ... ] }
Score formula, bands and the gate: shared/anti-slop.md §3. Branch field read by workflows:
slop.score (and slop.pass).
9. Modes & Policies
Default suggest. Direction and diagnose are read-only. Treat is Apply-with-review for every
edit (geometry, color, content, fonts) — none is in the safe set (shared/modes-and-policies.md,
policies/modes.json). New type tokens are proposed, never created here (hand off to
penpot-foundations).
10. State Management
Ledger keys under RUN_ID (shared/state-management.md): antiSlop.pref, antiSlop.direction,
antiSlop.reports[] (score, band, findings ids, listsVersion, scopeId). The preference itself lives
file-wide in plugin data penpot-ai → prefs.antiSlop, independent of any run. Resume: re-read the
ledger, re-run the probe (cheap, idempotent) rather than trusting cached findings.
11. User Checkpoints
| After phase | Artifacts shown | What we ask |
|---|---|---|
| 0 (from a build skill, pref unset) | the opt-in question | Avoid the AI look? Yes / No / Explain |
| direction | defaults → picks → typeface proposal → refused keys (inside the build skill's Phase 0) | Approve direction? |
| diagnose 3 | export + slop report (score, band, evidence, top fixes) | Treat the top fixes? Which ones? |
| each treat fix | export of the edited board | Approve this fix? |
| after treat | before/after score | Done, or another round? |
12. Naming Conventions
shared/naming-conventions.md. Region detection relies on semantic board names (hero, features,
pricing, footer…); suggest penpot-rename-layers when most regions come back as content.
Finding ids: slop-<key>-NN, stable across iterations.
13. Anti-Rationalization Table
| Excuse | Why it's wrong | Countermeasure (halt) |
|---|---|---|
| "The user would obviously want this on." | Strict mode is a style decision that belongs to the user. | Read prefs.antiSlop; if unset, ask the §0 question. |
| "My export looks fine, I'll skip the probe." | Self-review is lenient; the tree doesn't lie. | Run scanSlopProbes.js first, every diagnosis. |
| "I'll judge straight from the image." | Judging without describing invites motivated answers. | Write Pass A, then judge each key against its criterion. |
| "0.6 is close enough to count." | Unsure answers are noise in the score. | Only p ≥ 0.65 counts; report the rest as unsure. |
| "Swap Inter for something nicer, it's in the taxonomy." | The system outranks the mode. | Report inherited; propose a type token via penpot-foundations. |
| "Delete the fake stats and put a real-sounding number." | Fabricated evidence. | Labeled placeholder or a pattern that needs no number. |
| "Cream + italic serif is the antidote." | It is the newest default. | shared/anti-slop.md §4.4 — name it, replace it with a brief-driven pick. |
| "Apply all three fixes in one call." | One-shot edits are unreviewable. | One fix per call, export, approve. |
14. Helper Code Snippets
// Phase 0 — read the stored opt-in (full helper: scripts/antiSlopPrefs.js)
const v = penpot.currentFile.getSharedPluginData("penpot-ai", "prefs.antiSlop");
return { antiSlop: v === "on" || v === "off" ? v : "unset" };
// Phase 2 — append a review answer, then run scripts/slopScore.js in the same or the next call
storage.slop.findings.push({ key: "vague-headline", weight: 2, p: 0.8, source: "review", region: "hero:hero",
evidence: "Pass A: headline reads 'Build the future of work' — no product, no audience" });
storage.slop.template = { value: 3, confidence: 0.7 };
storage.slop.signals.push({ key: "product-visible", present: false, evidence: "hero shows a 3D sphere, no UI" });
return { findings: storage.slop.findings.length };
// Treat — one fix: flatten a gradient hero onto a bound surface token (gotchas #1, #11)
const hero = penpotUtils.findShapeById("HERO_ID_HERE");
hero.fills = [];
const bg = penpotUtils.findTokenByName("color.bg.default");
if (bg) hero.applyToken(bg, ["fill"]);
return { heroId: hero.id, bound: !!bg }; // verify the binding + export in the next call
15. Reference Resources
penpot_api_info('Text', 'fontFamily' | 'fontStyle' | 'align'),penpot_api_info('Blur'),penpot_api_info('Shadow'),penpot_api_info('Gradient'),penpot_api_info('FontsContext'),penpot_api_info('File', 'setSharedPluginData').
16. Supporting Files
references/: 01-penpot-mapping.md (diagnose Phase 1), 02-direction-playbook.md (direction),
03-exam-protocol.md (diagnose Phase 2), 04-deck-tells.md (decks, both modes).
scripts/: antiSlopPrefs.js (opt-in read/store), scanSlopProbes.js (deterministic probes),
slopScore.js (pure scoring, unit-tested by scripts/dev/test-anti-slop.mjs),
listFontCandidates.js (typeface shortlist outside both lists).
shared/: anti-slop.md (taxonomy, signals, score, direction, precedence), anti-slop/lists.json
(fonts, buzzwords EN/ES, placeholders, patterns — versioned).
Doctrine paths. shared/… and policies/… resolve inside this bundle in native installs (vendored by the installer); in a Claude Code plugin install they live at the plugin root — ${CLAUDE_PLUGIN_ROOT}/shared/…, two directories up from this file.