ux-guardrails
Prose design rules are followed probabilistically and dropped under iteration
pressure; a deterministic check is not. This skill ships that check —
scripts/ux_lint.mjs, a single-file, zero-dependency Node lint — plus the hook
wiring that runs it on every UI write and refuses to finish a session with
critical violations outstanding.
When to use / when NOT to use
Use to install or run mechanical UX enforcement in a landing-page project:
spacing on the grid, arbitrary-value and raw-palette bans, WCAG contrast on
design-token pairs, focus-visibility, slop tells (purple gradients, gradient
text, glassmorphism, transition: all).
Not for: choosing the palette/type/tokens (visual-design-system), picking the
overall look (landing-page-art-direction), a full WCAG 2.2 audit with manual
passes (landing-page-accessibility), motion design (scroll-motion), or
generic test/format hooks (that's ordinary repo tooling).
Severity model (two tiers, on purpose)
- critical — objectively wrong, blocks: disabled zoom,
<img> without
alt, outline-none with no focus-visible replacement, positive tabindex,
token pairs below 4.5:1. The agent must fix these.
- advisory — drift and slop tells, informs but never blocks: off-scale
spacing, arbitrary text sizes, raw palette classes, gradient text, slow
transitions. Per-edit nagging on taste rules has been observed to make agents
conservative and samey — so taste stays advisory, and the brief wins: an
intentional exception is suppressed explicitly, never fought.
Escape hatches (use these; never weaken the script): a
ux-guardrails-ignore <rule-id> comment on or above the line, or
ignoreRules/ignoreFiles in .ux-guardrails.json.
Workflow
Install into the consumer project. Copy the script and wire both hooks:
mkdir -p .claude/hooks
cp "${CLAUDE_SKILL_DIR}/scripts/ux_lint.mjs" .claude/hooks/ux_lint.mjs
node .claude/hooks/ux_lint.mjs --self-test # must print SELF-TEST PASSED
Merge into .claude/settings.json (merge — do not clobber existing hooks):
{
"hooks": {
"PostToolUse": [
{ "matcher": "Edit|Write|MultiEdit",
"hooks": [{ "type": "command",
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/ux_lint.mjs\"",
"timeout": 10 }] }
],
"Stop": [
{ "hooks": [{ "type": "command",
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/ux_lint.mjs\"",
"timeout": 30 }] }
]
}
}
Configure only if the project's scale differs. Optional
.ux-guardrails.json at the project root:
{ "spacingStep": 4, "contrastMin": 4.5, "maxFontFamilies": 3,
"transitionMaxMs": 500, "ignoreRules": [], "ignoreFiles": [] }
Defaults match this repo's visual-design-system budgets. WCAG floors are
the one thing config should never be used to relax.
Run it in CI too (same script, exit 1 on criticals):
node .claude/hooks/ux_lint.mjs src/ app/ styles/
Token contrast needs the pairing convention. Contrast is computed only
for CSS custom properties following the shadcn --x / --x-foreground
convention (plus --background/--foreground), with resolvable literal
values (hex, rgb, hsl, oklch — no var() chains, color-mix, or alpha).
Anything unresolvable is skipped silently by design — a lint that
false-positives on values it can't parse gets uninstalled.
How the hooks behave (what to tell the user)
- PostToolUse lints only the file just written. Criticals → exit 2, stderr
becomes the agent's repair instruction (the write already landed —
PostToolUse steers the next step, it cannot undo). Advisories → injected as
context, never block.
- Stop lints all git-changed UI files (capped at 20). Criticals → exit 2
blocks finishing, once — it honors
stop_hook_active, so a stuck
false positive can never loop the session.
- Cross-file budgets (font-family count) run only at Stop/CLI where the whole
changeset is visible; per-file they'd be meaningless.
Output spec
Findings as file:line [SEVERITY] [rule-id] message, grouped by file, ending
with a N critical, M advisory budget line and the ignore instruction. The
full rule catalog with thresholds and rationale: references/rules.md.
Gotchas
- Never "fix" a finding by editing the lint script, the hook config, or by
scattering ignore comments. An intentional design exception gets one
documented ignore; three ignores of the same rule mean the config (or the
design) is wrong.
- The Stop gate reads
git diff — in a project without git it silently checks
nothing (PostToolUse still covers every write).
--self-test exercises every rule against embedded fixtures; run it after
any update of the script copy.
- Advisory findings are signals, not noise: resolve each one by either fixing
the code or ignoring it explicitly with a reason.
- The purple-gradient / gradient-text tells are heuristics about defaults —
a brand that genuinely wants purple overrides them (ignore + a line in
DESIGN.md), which is exactly the "brief wins" rule.
References
references/rules.md — every rule id, severity, exact trigger/threshold, and
the research it's derived from.
1---2name: ux-guardrails3description: Enforces UX quality mechanically - a zero-dep lint (spacing scale, typography budget, WCAG token contrast, AI-slop tells) wired as Claude Code PostToolUse/Stop hooks that block criticals. Use when asked to enforce design rules automatically, lint UI code, add a UX/design hook, or stop off-scale spacing and raw palette classes. Not for choosing tokens, full WCAG audits, or CI test hooks.4---56# ux-guardrails78Prose design rules are followed probabilistically and dropped under iteration9pressure; a deterministic check is not. This skill ships that check —10`scripts/ux_lint.mjs`, a single-file, zero-dependency Node lint — plus the hook11wiring that runs it on every UI write and refuses to finish a session with12critical violations outstanding.1314## When to use / when NOT to use1516Use to install or run mechanical UX enforcement in a landing-page project:17spacing on the grid, arbitrary-value and raw-palette bans, WCAG contrast on18design-token pairs, focus-visibility, slop tells (purple gradients, gradient19text, glassmorphism, `transition: all`).2021Not for: choosing the palette/type/tokens (`visual-design-system`), picking the22overall look (`landing-page-art-direction`), a full WCAG 2.2 audit with manual23passes (`landing-page-accessibility`), motion design (`scroll-motion`), or24generic test/format hooks (that's ordinary repo tooling).2526## Severity model (two tiers, on purpose)2728- **critical** — objectively wrong, blocks: disabled zoom, `<img>` without29 `alt`, `outline-none` with no focus-visible replacement, positive tabindex,30 token pairs below 4.5:1. The agent must fix these.31- **advisory** — drift and slop tells, informs but never blocks: off-scale32 spacing, arbitrary text sizes, raw palette classes, gradient text, slow33 transitions. Per-edit nagging on taste rules has been observed to make agents34 conservative and samey — so taste stays advisory, and **the brief wins**: an35 intentional exception is suppressed explicitly, never fought.3637Escape hatches (use these; never weaken the script): a38`ux-guardrails-ignore <rule-id>` comment on or above the line, or39`ignoreRules`/`ignoreFiles` in `.ux-guardrails.json`.4041## Workflow42431. **Install into the consumer project.** Copy the script and wire both hooks:4445 ```bash46 mkdir -p .claude/hooks47 cp "${CLAUDE_SKILL_DIR}/scripts/ux_lint.mjs" .claude/hooks/ux_lint.mjs48 node .claude/hooks/ux_lint.mjs --self-test # must print SELF-TEST PASSED49 ```5051 Merge into `.claude/settings.json` (merge — do not clobber existing hooks):5253 ```json54 {55 "hooks": {56 "PostToolUse": [57 { "matcher": "Edit|Write|MultiEdit",58 "hooks": [{ "type": "command",59 "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/ux_lint.mjs\"",60 "timeout": 10 }] }61 ],62 "Stop": [63 { "hooks": [{ "type": "command",64 "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/ux_lint.mjs\"",65 "timeout": 30 }] }66 ]67 }68 }69 ```70712. **Configure only if the project's scale differs.** Optional72 `.ux-guardrails.json` at the project root:7374 ```json75 { "spacingStep": 4, "contrastMin": 4.5, "maxFontFamilies": 3,76 "transitionMaxMs": 500, "ignoreRules": [], "ignoreFiles": [] }77 ```7879 Defaults match this repo's `visual-design-system` budgets. WCAG floors are80 the one thing config should never be used to relax.81823. **Run it in CI too** (same script, exit 1 on criticals):8384 ```bash85 node .claude/hooks/ux_lint.mjs src/ app/ styles/86 ```87884. **Token contrast needs the pairing convention.** Contrast is computed only89 for CSS custom properties following the shadcn `--x` / `--x-foreground`90 convention (plus `--background`/`--foreground`), with resolvable literal91 values (hex, rgb, hsl, oklch — no `var()` chains, `color-mix`, or alpha).92 Anything unresolvable is skipped **silently by design** — a lint that93 false-positives on values it can't parse gets uninstalled.9495## How the hooks behave (what to tell the user)9697- **PostToolUse** lints only the file just written. Criticals → exit 2, stderr98 becomes the agent's repair instruction (the write already landed —99 PostToolUse steers the next step, it cannot undo). Advisories → injected as100 context, never block.101- **Stop** lints all git-changed UI files (capped at 20). Criticals → exit 2102 blocks finishing, **once** — it honors `stop_hook_active`, so a stuck103 false positive can never loop the session.104- Cross-file budgets (font-family count) run only at Stop/CLI where the whole105 changeset is visible; per-file they'd be meaningless.106107## Output spec108109Findings as `file:line [SEVERITY] [rule-id] message`, grouped by file, ending110with a `N critical, M advisory` budget line and the ignore instruction. The111full rule catalog with thresholds and rationale: `references/rules.md`.112113## Gotchas114115- **Never "fix" a finding by editing the lint script, the hook config, or by116 scattering ignore comments.** An intentional design exception gets one117 documented ignore; three ignores of the same rule mean the config (or the118 design) is wrong.119- The Stop gate reads `git diff` — in a project without git it silently checks120 nothing (PostToolUse still covers every write).121- `--self-test` exercises every rule against embedded fixtures; run it after122 any update of the script copy.123- Advisory findings are signals, not noise: resolve each one by either fixing124 the code or ignoring it explicitly with a reason.125- The purple-gradient / gradient-text tells are heuristics about *defaults* —126 a brand that genuinely wants purple overrides them (ignore + a line in127 DESIGN.md), which is exactly the "brief wins" rule.128129## References130131- `references/rules.md` — every rule id, severity, exact trigger/threshold, and132 the research it's derived from.