Add design detector PostToolUse hook for Claude Code and Codex#170
Open
abdulwahabone wants to merge 19 commits into
Open
Add design detector PostToolUse hook for Claude Code and Codex#170abdulwahabone wants to merge 19 commits into
abdulwahabone wants to merge 19 commits into
Conversation
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>
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>
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>
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>
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>
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>
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>
Resolve conflicts while keeping Cursor stop-hook followup, hook tests, and hooks routing in SKILL.src.md. Adopt main's context-aware command menu, SKILL.src.md source layout, Codex nested agents, and expanded test suite. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit c98962b. Configure here.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

PREVIEW
Summary
hookSpecificOutput.additionalContext(never blocks the turn).apply_patchfile paths fromtool_input.command(notfile_path), and co-scan co-located/imported stylesheets when a JSX/TSX component is edited soApp.jsxpatches don't miss slop instyles.css.Edit|Write|MultiEdit|apply_patchon Claude;Edit|Write|apply_patchon Codex.agents) — git-status sweep removed after field testing showed noise > signal.hooks/hooks.jsonfor Claude,.agents, and marketplaceplugin/; includes hook admin skill command, PRD, README install notes, and 50+ unit tests.Anti-patterns detected
The hook scans
.tsx,.jsx,.html,.vue,.svelte,.astro,.css,.scss,.less,.ts, and.jsafter each direct edit. It uses the same detector asnpx impeccable detect:.html/.htm): regex + page-level analyzers viadetectText.detectHtml(layout, contrast, typography, DOM structure).Page-level rules (marked below) only fire when the file looks like a full page (
<!doctype>,<html>, or<head>). For deeper coverage on rendered apps (dev-server URLs, browser layout), use/impeccable audit.side-tabborder-accent-on-roundedoverused-fontsingle-fontflat-type-hierarchygradient-textai-color-palettenested-cardsmonotonous-spacingeverything-centeredbounce-easingdark-glowicon-tile-stackitalic-serif-displayhero-eyebrow-chiprepeated-section-kickerspure-black-whitegray-on-colorlow-contrastlayout-transitionline-lengthcramped-paddingbody-text-viewport-edgetight-leadingskipped-headingjustified-texttiny-textall-caps-bodywide-tracking*
repeated-section-kickersis advisory severity — hidden by default (minSeverity: warning). Surface it with"minSeverity": "advisory"in.impeccable/hook.json.29 rules total — 14 on source files (9 line-level + 5 page-level), all 29 on HTML. Component-level
.tsx/.jsxedits get the regex slice; co-located stylesheet scanning helps catch CSS-side tells the component import pulls in.Note
Medium Risk
Large new hook layer runs on every UI file edit and injects imperative model instructions; mitigated by non-blocking exits and path/sensitivity guards, but behavior and noise impact warrant careful review.
Overview
Adds an automatic design-detector hook that runs after agent file edits on UI-related paths and nudges the model with findings (always exit 0, never blocking edits).
Harness wiring:
.agents/.claudePostToolUse→hook.mjs; Cursor usesafterFileEdit(hook-after-edit.mjsqueues emissions) plusstop(hook-stop.mjs,loop_limit: 1) because inlineadditional_contextis unreliable there.Core logic (
hook-lib.mjs): resolves edited files (including Codexapply_patchpaths and Cursorpath), expands scans to imported/co-located stylesheets, runs the bundled detector, filters/dedupes via.impeccable/hook.json+ session cache, and emits fresh / pending / clean / suppression messages with caps and sensitive-path skips.Operator surface: new
/impeccable hookscommand (hook-admin.mjs,reference/hooks.md) for on/off/status/ignore/reset; skill routing updated. Detector file walker and regex pass now treat.sasslike other CSS preprocessors.Reviewed by Cursor Bugbot for commit 43d02d9. Bugbot is set up for automated code reviews on this repo. Configure here.