* docs: add PRD for design detector hook integration Plans a PostToolUse hook for Claude Code and Codex that runs the existing design detector after every relevant file write and feeds findings back to the agent as advisory system-reminder context. No implementation in this commit; covers UX, technical design, build pipeline changes, distribution, coverage tradeoffs, and rollout. Co-authored-by: Cursor <cursoragent@cursor.com> * docs: revise hook PRD with best-practices review Folds in the P0/P1/P2 findings from an online best-practices critique against the official Claude Code and Codex hook references plus 10+ 2026 community guides and similar prior-art tools (claw-hooks, claude-code-hooks-mastery). Key changes: - Exec form everywhere (Codex snippet was shell form), with Windows rationale. - Default timeout dropped from 10s to 5s. - Re-entrancy guard (CLAUDE_HOOK_DEPTH) and per-file edit counter. - Session-scoped finding dedup promoted from open question to v1. - Per-language inline-ignore syntax map (HTML/JSX/CSS/JS). - Hard-skip rules for sensitive paths and generated/lock files. - Honest framing about Claude Code lacking per-plugin hook disable. - Honest framing about Bash-written files being invisible in v1. - Codex Windows-not-supported call-out, feature flag note, trust ceremony detail. - Optional NDJSON audit log via IMPECCABLE_HOOK_LOG. - Findings cap lowered 8 → 5 with attention-budget rationale. - Versioned envelope ([impeccable@1]) on rendered template. - Expanded test plan, decision log, and stdin payload appendix. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(hooks): ship the design detector hook for Claude Code and Codex Implements docs/hooks-prd.md: a PostToolUse hook that runs the impeccable design detector after every Edit/Write/MultiEdit on a UI file and pushes findings into the agent's next-turn context as a short system reminder. Silent on clean files. Never blocks an edit. Why this matters: today, design slop (side-tab borders, gradient text, purple/cyan palettes, bounce easing, etc.) only gets caught when a human notices or someone explicitly runs /impeccable audit. The hook closes the loop at the moment slop is written. What ships in v1 - skill/scripts/hook.mjs: PostToolUse entry. Reads stdin, runs the detector in-process (no `npx impeccable` cold start), emits hookSpecificOutput.additionalContext when fresh findings exist. - skill/scripts/hook-lib.mjs: extracted helpers (config, cache, filter, render, audit log, runHook orchestrator). 100% unit-testable. - skill/scripts/hook-session-start.mjs: SessionStart greeting, gated by a project-scannable probe + 30-day throttle. - skill/scripts/hook-admin.mjs: backs /impeccable hooks on/off/status/ignore-rule/ignore-file/reset. Hardening built in - Re-entrancy guard (IMPECCABLE_HOOK_DEPTH) so the hook can never recursively spawn itself. - Hard-skip regexes for sensitive paths (.env, .pem, id_rsa, secrets, credentials, .git) and generated/lock/build output. These fire before the file is even read; cannot be turned off via config. - Path-traversal check on the inbound file_path. - Session-scoped dedup keyed by (session, file, rule, line) so the same finding never lands in context twice. Prevents the ~12.5K wasted tokens per chatty session called out in the PRD. - Per-(session, file) edit counter with a one-shot suppression notice on the 7th edit, silent after. - Fail-open contract: every error path returns exit 0 with no stdout. Optional NDJSON audit log via IMPECCABLE_HOOK_LOG. Three kill switches (precedence high to low): 1. IMPECCABLE_HOOK_DISABLED env var (1/true/yes/on, case-insensitive) 2. .impeccable/hook.json `enabled: false` 3. /impeccable hooks off slash command (writes the JSON) Inline ignores are language-aware. `// impeccable: ignore <rule>` for JS/TS, `<!-- impeccable: ignore <rule> -->` for HTML/Vue/Svelte/Astro, `{/* impeccable: ignore <rule> */}` for JSX/TSX, `/* impeccable: ignore <rule> */` for CSS. `*` matches any rule. Directive applies to the next non-blank line. Same shape as ESLint, Stylelint, Biome. Build pipeline - scripts/lib/transformers/hooks.js: per-provider hooks.json builders, plus the slim .codex-plugin/plugin.json manifest. - providers.js: emitHooks: 'claude' for claude-code, emitHooks: 'codex' for codex and agents. Codex also emits emitCodexPlugin. - factory.js: emits hooks/hooks.json next to the skills tree. - build.js: syncs hooks/ into harness roots and into the slim plugin/ subtree; writes .codex-plugin/plugin.json. Build is idempotent (verified: 98 staged files unchanged across two runs). Claude Code wiring uses exec form (command + args) and the ${CLAUDE_PLUGIN_ROOT} placeholder. Matcher: Edit|Write|MultiEdit. `if:` glob filters to UI extensions before spawning Node. PostToolUse timeout 5s, SessionStart timeout 3s. Codex wiring uses ${PLUGIN_ROOT} (Codex's native placeholder), matcher Edit|Write|apply_patch, no `if:` analog (the script does the extension filter). macOS and Linux only; hooks are disabled on Windows in current Codex builds. The trust ceremony and feature flag are documented in README.md. Routing - /impeccable hooks lives outside the 23-command router table on purpose: it is plumbing, not a design skill. The hidden routing slot is added to SKILL.md alongside pin/unpin so the LLM knows to dispatch it. The 23-command count and all stale-count validators remain happy. Tests - tests/hook.test.mjs: 38 unit tests covering env parsing, config load + defaults + malformed, cache round-trip + GC, ignoreRules/minSeverity/inline ignores (all four languages), globbing with **/*/{a,b}, render template with cap + clamp + 0-line prefix drop, audit log NDJSON, payload event-name parameterization, re-entrancy, kill switches, sensitive-path + generated-path + traversal skips, allowlist filter, config ignoreFiles, edit counter cycle including the 7th-edit notice, MultiEdit and apply_patch payload shapes, detector throw swallow, malformed stdin, missing file race. - tests/hook-build.test.mjs: 18 integration tests covering hook manifest shape (matcher, timeouts, exec form, if: glob, placeholders), Codex differences (${PLUGIN_ROOT}, no if:, no SessionStart), Codex plugin manifest (no inline hooks field to avoid the duplicate-file error), routing across the hooksJsonFor table, and presence of all three committed artifacts plus the bundled detector the runtime relative-import path depends on. Full suite: 175 bun tests + 186 node tests, all green. Docs - README.md: new "Design hook" section explaining default behavior, per-project / global / inline disable paths, the JSON schema knobs, the audit log debug flag, and the slop / a11y coverage split. - HARNESSES.md: flips the `hooks` row for Codex from No -> Yes (Claude was already Yes), adds a per-harness hook-surface table with the manifest location and matcher each provider uses. Open questions from the PRD intentionally deferred to v2: Bash-write blind spot, effort-aware suppression, Stop-hook session summary, per-rule severity, async hook mode. None block v1. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix Codex hook scanning: apply_patch paths and co-located stylesheets Parse file targets from Codex apply_patch command bodies, co-scan imported and sibling CSS when UI components are edited, drop the git-sweep PostToolUse group, and align Codex SessionStart manifest and trust docs with the official hooks spec. Co-authored-by: Cursor <cursoragent@cursor.com> * Gitignore hook session cache and drop local test HTML Hook dedup/throttle state in .impeccable/hook.cache.json is per-project runtime data like other .impeccable/ sidecars. Remove an untracked bad-nested-flexbox scratch page from site/public/. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix Claude Code hook: drop Edit-only if filter so Write/MultiEdit fire Claude's if permission rule binds to one tool name, so Edit(*.{…}) never spawned the hook on Write or MultiEdit despite the matcher listing them. Extension filtering now lives in hook-lib on both Claude and Codex. Co-authored-by: Cursor <cursoragent@cursor.com> * Surface Cursor design findings via stop-hook followup Replace dropped postToolUse additional_context with afterFileEdit recording and a one-shot stop followup_message so anti-pattern nudges reach the agent. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix design hook packaging and scans * Fix Cursor hook pending bucket fallback * Fix Sass hook scan coverage * Fix Cursor hook review findings * Fix session start dead hook normalization * Fix hook config and relative scan paths * Remove SessionStart design hook * Remove redundant afterFileEdit normalization * Fix Cursor suppression and module style scans * Fix sensitive path hook filter * Fix disabled Cursor stop hook emission * Refresh hook harness artifacts * Fix Cursor hook manifest install * Add hook ignore-value support * Ignore hook runtime files locally * Fix Codex plugin hook packaging * fix: address PR review bot findings Block numeric hook depth counters from re-entering. Avoid following stylesheet imports from traversal-looking hook targets. * fix: gate ignore-value suggestions by supported rules Only render exact ignore-value commands when the same finding can be suppressed by ignoreValues. * Package Codex plugin as hook-only * Remove Codex plugin packaging * Recover hook install probe plumbing * Remove Codex hook packaging follow-up doc * Remove extra hook docs and skill wording changes * Install real design hooks via skills CLI * Add provider hook smoke runner * Fix Cursor hook delivery with preToolUse gate * Simplify Cursor hook install to preToolUse * Clarify confirmed hook exceptions * Persist hook ignores in shared config * Guard font hook exceptions * Fix hook install after main rebase * Fix hook scan target handling * fix: address hook review findings * Address hook review feedback * Stabilize DeepSeek insert live fixture * Fix Cursor hook Python shell write bypass --------- Co-authored-by: Cursor <cursoragent@cursor.com>
5.8 KiB
/impeccable hooks
Manage the design detector hook for the current project.
The hook runs the impeccable design detector on direct file edits to design-relevant files (.tsx, .jsx, .html, .vue, .svelte, .astro, .css, .scss, .sass, .less, .ts, .js). Claude Code and Codex use PostToolUse and push a short system reminder into the agent's context after the edit; findings get a correction prompt, pending issues get a re-nudge, and clean UI-ish files get a short ack unless IMPECCABLE_HOOK_QUIET=1 is set. Plain .ts and .js files are still scanned, but stay quiet unless the detector finds something. Cursor uses preToolUse to block bad proposed writes before they land and stays silent when it allows a clean write.
This command toggles the hook per project by editing .impeccable/hook.json. Local-only ignore policy lives in .impeccable/hook.local.json, which is gitignored. To disable globally, set IMPECCABLE_HOOK_DISABLED=1 in your shell environment.
Supported harnesses: Claude Code (.claude/settings.json in the project), Codex (.codex/hooks.json in the project), and Cursor (.cursor/hooks.json in the project).
On Cursor, preToolUse checks proposed Write/Edit/Shell write content and denies only when the real detector finds an issue. The denial message is visible to the agent as the tool error, so the agent can reconsider before the bad write lands.
Routing
The first argument is the action. Defaults to status.
| Action | What it does |
|---|---|
status |
Print current state, shared/local config paths, ignored rules / files / values, env override. |
on |
Set enabled: true in .impeccable/hook.json. |
off |
Set enabled: false in .impeccable/hook.json. |
ignore-rule <id> |
Append <id> to ignoreRules; for overused-font, requires --all-values. |
ignore-file <glob> |
Append <glob> to ignoreFiles. |
ignore-value <id> <value> [--shared] [--reason "..."] |
Append a rule/value suppression to shared .impeccable/hook.json. |
ignore-value <id> <value> --local [--reason "..."] |
Append a private rule/value suppression to .impeccable/hook.local.json. |
reset |
Delete the project config, dedup cache, and Cursor pending queue. |
Flow
-
Resolve the action from the user's argument. If no action was given, default to
status. -
Invoke the admin script and pass the user's output through verbatim:
node {{scripts_path}}/hook-admin.mjs <action> [args...] -
If
<action>isoff, follow up with a one-line note: "Done. New edits will not trigger the design hook in this project until you run{{command_prefix}}impeccable hooks on." -
If
<action>ison, follow up with: "Done. The design hook will fire after the next Edit/Write/MultiEdit on a UI file." -
If
<action>isignore-value,ignore-file, orignore-rule, just print the script output. The default scope is shared.impeccable/hook.json; add--localonly when the user explicitly asks for a private exception. -
If
<action>isstatus, just print the script output. Do not add commentary unless the user asked a follow-up question.
Intentional findings
The hook itself never writes ignore config. Persist an exception only after the user explicitly confirms the flagged issue is intentional, and always go through hook-admin.mjs.
Prefer the narrowest exception:
- If the finding line shows an exact
ignore-valuecommand, run that command. This writes shared.impeccable/hook.jsonby default. - For
overused-font, useignore-valuewhen the user confirms a specific font. Do not useignore-rule overused-fontfor a specific font. - If the finding has no value-specific command, such as
side-tab, preferignore-file <path>for the current file. - Use
ignore-rule <id>only when the user asks to suppress that whole rule across the project. For broad overused-font suppression, useignore-rule overused-font --all-valuesonly when the user asks to ignore overused fonts generally. - Do not add source comments such as
impeccable: ignore; inline comments pollute code and are not a supported suppression mechanism.
Example value-specific exception:
node {{scripts_path}}/hook-admin.mjs ignore-value overused-font Inter --shared --reason "User confirmed Inter is intentional"
Example whole-rule font exception:
node {{scripts_path}}/hook-admin.mjs ignore-rule overused-font --all-values --reason "User asked to ignore overused fonts generally"
Example file-scoped exception:
node {{scripts_path}}/hook-admin.mjs ignore-file "src/legacy/Card.tsx"
Constraints
- Never modify
.impeccable/hook.jsonor.impeccable/hook.local.jsonby hand from this command. Always go throughhook-admin.mjsso writes stay validated and the file shape stays consistent. - Do not edit the hook scripts themselves (
hook.mjs,hook-lib.mjs,hook-before-edit.mjs) from this flow. Those are skill plumbing. - Cursor can block a proposed write when the detector finds a real issue. Claude Code and Codex do not block the edit; they emit a post-edit reminder instead. Disabling stops both blocking and reminders.
- The hook is bundled with the Impeccable skill and installed through project-local manifests:
.claude/settings.json,.codex/hooks.json, and.cursor/hooks.json. On Codex, the user must approve the hook via/hooksthe first time. On Cursor, confirm hooks are enabled under Settings -> Hooks.
Failure modes
- If
.impeccable/hook.jsonor.impeccable/hook.local.jsonis unreadable or malformed, the hook ignores that file and uses the remaining valid config/defaults.hook-admin.mjs statuswill show malformed files as ignored. - If the user asks to "disable the hook" globally, suggest both options:
IMPECCABLE_HOOK_DISABLED=1env var (one-shot, follows the shell), and{{command_prefix}}impeccable hooks off(persistent for this project, committable).