# Design hook

> Provider-native edit hooks that scan writes for detector findings, intentional-finding workflow, quiet/disabled controls, and native-platform skip behavior.

- Repository: pbakaus/impeccable
- GitHub: https://github.com/pbakaus/impeccable
- Human docs: https://grok-wiki.com/public/docs/pbakaus-impeccable-adadc04d8de4
- Complete Markdown: https://grok-wiki.com/public/docs/pbakaus-impeccable-adadc04d8de4/llms-full.txt

## Source Files

- `skill/reference/hooks.md`
- `skill/scripts/hook.mjs`
- `skill/scripts/hook-before-edit.mjs`
- `skill/scripts/hook-lib.mjs`
- `plugin/hooks/hooks.json`
- `docs/HARNESSES.md`
- `cli/lib/impeccable-config.mjs`

---

---
title: "Design hook"
description: "Provider-native edit hooks that scan writes for detector findings, intentional-finding workflow, quiet/disabled controls, and native-platform skip behavior."
---

The design hook is a provider-native edit-time gate that runs Impeccable’s anti-pattern detector on UI file writes. Install and update wire a harness-specific manifest that invokes either `hook.mjs` (post-edit + Stop) or `hook-before-edit.mjs` (Cursor pre-write deny). Shared scan logic lives in `skill/scripts/hook-lib.mjs`; project lifecycle is managed through `/impeccable hooks` → `hook-admin.mjs` and `.impeccable/config.json` / `config.local.json`.

The hook is **web-only**. When `PRODUCT.md` sets `## Platform` to `ios`, `android`, or `adaptive`, both entry points skip scanning (`skipped: 'native-platform'`). Manual `npx impeccable detect` is separate: `hook.enabled` does not turn CLI scans off.

## Runtime surface

| Piece | Path / role |
|---|---|
| Post-edit + Stop entry | `skill/scripts/hook.mjs` → `runHook` / `runStopHook` |
| Cursor pre-write gate | `skill/scripts/hook-before-edit.mjs` |
| Shared library | `skill/scripts/hook-lib.mjs` |
| Admin CLI for the skill | `skill/scripts/hook-admin.mjs` (via `/impeccable hooks`) |
| Manifest emitters | `scripts/lib/transformers/hooks.js` |
| Plugin package hooks | `plugin/hooks/hooks.json` (`${CLAUDE_PLUGIN_ROOT}`) |
| Detector | `cli/engine/detect-antipatterns.mjs` (or skill-bundled `detector/detect-antipatterns.mjs`) |

**Contract:** hooks always exit `0` and must not break an agent turn. Detector throws, malformed stdin, missing files, and unsupported Node versions degrade to allow / silence (with optional audit log).

```text
Harness edit event
        │
        ├─ Cursor preToolUse ──► hook-before-edit.mjs
        │                         allow | deny (permission JSON)
        │
        └─ PostToolUse/postToolUse ──► hook.mjs (runHook)
              immediate-tier findings → additionalContext
              clean / pending acks (unless quiet)
              session cache under .impeccable/hook.cache.json
                        │
                        ▼ (Claude / Codex / Grok Stop only)
                   runStopHook — full rule set on touched UI files
```

## Supported harnesses

| Harness | Event | Script | Manifest | Behavior |
|---|---|---|---|---|
| Claude Code | `PostToolUse` + `Stop` | `hook.mjs` | `.claude/settings.local.json` (preferred; gitignored) or shared `settings.json` | After Edit/Write/MultiEdit; deep pass on Stop |
| Codex | `PostToolUse` + `Stop` | `hook.mjs` | `.codex/hooks.json` | Matcher `Edit\|Write\|apply_patch`; requires `/hooks` trust |
| Grok Build | `PostToolUse` + `Stop` | `hook.mjs` | `.grok/hooks/impeccable.json` | Claude matchers alias to Grok tools; needs `/hooks-trust` or `--trust` |
| GitHub Copilot | `postToolUse` only | `hook.mjs` | `.github/hooks/impeccable.json` (committed) | Full per-edit rules (no Stop context channel); matcher `edit\|create\|apply_patch` |
| Cursor | `preToolUse` only | `hook-before-edit.mjs` | `.cursor/hooks.json` | Denies bad proposed writes; silent allow on clean |

Other harnesses ship the skill without a documented hook surface. When no automatic hook is active, `context.mjs` can emit `MANUAL_DETECTOR_REQUIRED` so the agent runs a detector pass at session end.

### Install and consent

`npx impeccable install` / `update` installs the skill payload and, unless `--no-hooks`, offers to wire the design hook (default yes). Consent is stored per developer as `hook.consent` (`accepted` | `declined`) in gitignored `.impeccable/config.local.json`.

| Platform step | Requirement |
|---|---|
| Codex | Approve the project hook via `/hooks` after install/update (re-prompt possible when the manifest changes) |
| Cursor | Hooks enabled under Settings → Hooks; restart if reload fails |
| Grok | Project folder trust (`/hooks-trust` or `--trust`) |
| Copilot CLI | Commit `.github/hooks/impeccable.json` on the default branch; trust the folder |
| Node runtime | Node **22+** on the hook shell’s PATH (manifest probe exits 0 and may emit a one-shot `systemMessage` on Claude/Codex when too old) |

Plugin installs use `plugin/hooks/hooks.json` with `${CLAUDE_PLUGIN_ROOT}` (Grok aliases that to `GROK_PLUGIN_ROOT`).

## What gets scanned

Built-in extensions:

`.tsx` `.jsx` `.html` `.htm` `.vue` `.svelte` `.astro` `.css` `.scss` `.sass` `.less` `.ts` `.js`

- Clean/pending **acks** fire only for UI-ish extensions (`ACK_EXTS`); plain `.ts` / `.js` stay quiet unless findings exist.
- Add server templates via `detector.extensions` (no admin subcommand; edit config directly), e.g. `{ "ext": ".blade.php", "engine": "html" }`. Match is suffix-based (supports double extensions). `engine` is `html` or `text` (default `html`).
- Hard skips (not configurable): sensitive paths (`SENSITIVE_PATH`), generated/build/lock paths (`GENERATED_PATH`), path traversal, files outside the project root, files over `limits.maxFileBytes` (default 131072).

## Two-tier rules

Default `hook.perEditRules` is `"immediate"`. Per-edit PostToolUse only surfaces **immediate-tier** rules; remaining findings wait for the Stop deep pass.

**Immediate tier (`IMMEDIATE_TIER_RULES`):**  
`broken-image`, `text-overflow`, `clipped-overflow-container`, `body-text-viewport-edge`, `low-contrast`, `gray-on-color`, `tiny-text`, `gradient-text`, `dark-glow`, `design-system-font`, `design-system-color`, `design-system-radius`, `design-system-font-size`.

**Stop deep pass (`runStopHook`):** full detector set over UI files touched this session, deduped against per-edit cache, max `STOP_MAX_FILES` (20), timeout 30s. Silent when nothing new. Skips when Claude sets `stop_hook_active: true` (prevents consecutive-block loops).

Tiering is **off** for Cursor and GitHub Copilot (`perEditTieringActive` returns false) so non-immediate rules are not silently dropped. Set `hook.perEditRules: "all"` to restore full rules on every edit for harnesses that support Stop.

Advisory rules (e.g. `em-dash-overuse`) are **excluded** from the hook unless `detector.advisoryRules: "include"`.

## Emission kinds (post-edit)

| Kind | When | Output shape (Claude/Codex/Grok) |
|---|---|---|
| `fresh` | New findings not already in session cache | `[impeccable@1] Design hook findings...` + fix/ignore guidance via `hookSpecificOutput.additionalContext` |
| `pending` | Known unresolved findings still present | Re-nudge listing known issues |
| `clean` | No immediate findings; once per file per session | Short ack + steer line (UI extensions only) |
| `suppression` | Edit count for the file exceeds `EDIT_COUNT_THRESHOLD` (6) | Stops further per-file hints; points at audit |

- **Quiet mode** (`hook.quiet` or `IMPECCABLE_HOOK_QUIET`): no clean/pending acks; findings still emit.
- **Cursor:** deny JSON `{ permission: "deny", user_message, agent_message }` when findings exist; after the same finding signature is denied more than 6 times, allow with a loop-break warning.
- **GitHub payload:** top-level `additionalContext`.
- Caps: `limits.maxFindings` (default 5), `limits.maxChars` (default 8000).

## Native platform skip

`resolveProjectPlatform(cwd)` loads the same product context as the skill (`loadContext` + `extractPlatform`). If platform is `ios`, `android`, or `adaptive`, the hook returns without scanning. Native apps still use the same file extensions the hook watches; skipping avoids web HTML/CSS findings that contradict native platform guidance.

## Configuration

Shared (committed) `.impeccable/config.json` and local (gitignored) `.impeccable/config.local.json` merge; local overrides shared for the same keys. Malformed files are ignored.

### `hook` keys

| Key | Type | Default | Meaning |
|---|---|---|---|
| `enabled` | boolean | `true` | Automatic hook execution only |
| `quiet` | boolean | `false` | Suppress clean/pending acks |
| `auditLog` | string path \| null | `null` | NDJSON log path |
| `perEditRules` | `"immediate"` \| `"all"` | `"immediate"` | Per-edit rule set |
| `consent` | `"accepted"` \| `"declined"` | (local) | Install consent, recorded by CLI / `hooks on` |
| `limits.maxFindings` | number | `5` | Cap findings in the prompt |
| `limits.maxChars` | number | `8000` | Cap message size |
| `limits.maxFileBytes` | number | `131072` | Skip larger files |

### `detector` keys used by the hook

| Key | Meaning |
|---|---|
| `ignoreRules` | Suppress rule ids project-wide |
| `ignoreFiles` | Suppress all rules for matching globs |
| `ignoreValues` | Value- and file-scoped suppressions |
| `extensions` | Extra template/source suffixes + engine |
| `designSystem.enabled` | Design-system drift checks (default on) |
| `advisoryRules` | `"exclude"` (default) or `"include"` |

Legacy env overrides (win over config when set):

| Variable | Effect |
|---|---|
| `IMPECCABLE_HOOK_DISABLED` | Skip all hook work (`1`/`true`/`yes`/`on`) |
| `IMPECCABLE_HOOK_QUIET` | Quiet acks |
| `IMPECCABLE_HOOK_LOG` | NDJSON audit path |
| `IMPECCABLE_HOOK_DEPTH` / `CLAUDE_HOOK_DEPTH` | Re-entrancy guard |
| `IMPECCABLE_HOOK_DEBUG` | stderr on unexpected errors |
| `IMPECCABLE_HOOK_HARNESS` | Force harness id for payload shape |

Session state: `.impeccable/hook.cache.json` (dedupe, edit counts, touched files for Stop). Cursor may use pending/denial tracking in the same cache family.

## Manage with `/impeccable hooks`

Routes to `node …/hook-admin.mjs <action>`. Prefer this over hand-editing config (except `detector.extensions`).

| Action | Effect |
|---|---|
| `status` (default) | Enabled state, config paths, ignores, env override, cache path |
| `on` | `hook.enabled: true`, local consent `accepted`, repair provider manifests |
| `off` | `hook.enabled: false` |
| `ignore-rule <id>` | Append to `detector.ignoreRules` (`overused-font` needs `--all-values`) |
| `ignore-file <glob>` | Suppress every rule on matching files |
| `ignore-value <id> <value> [--shared\|--local] [--reason] [--file]` | Narrowest value/file suppressions |
| `reset` | Remove project hook config, cache, Cursor pending queue |

Example config shape:

```json
{
  "hook": {
    "enabled": true,
    "quiet": false,
    "perEditRules": "immediate",
    "auditLog": null
  },
  "detector": {
    "ignoreRules": [],
    "ignoreFiles": [],
    "ignoreValues": [],
    "extensions": [{ "ext": ".blade.php", "engine": "html" }],
    "designSystem": { "enabled": true },
    "advisoryRules": "exclude"
  }
}
```

## Intentional findings workflow

The hook **never** writes ignore config. Persist exceptions only after the user confirms a finding is intentional, via `hook-admin.mjs` (or `npx impeccable ignores`).

Prefer narrowest first:

1. Exact `ignore-value` command printed next to a value-specific finding (fonts, easing, etc.).
2. `ignore-value <id> "*" --file <path>` for a single rule on one still-reviewable surface.
3. `ignore-file <path>` only when the whole file is out of design scope (fixture, generated, deliberate slop demo).
4. `ignore-rule <id>` only for project-wide rule suppression when the user asks.
5. Inline comments only when the waiver must travel with a standalone export: `impeccable-disable <rule>`, `impeccable-disable-line`, `impeccable-disable-next-line` (optional reason after `:` or `--`). Honored by the detector unless `--no-inline-ignores` / `--no-config`.

```bash
node skill/scripts/hook-admin.mjs ignore-value overused-font Inter --shared --reason "User confirmed Inter is intentional"
node skill/scripts/hook-admin.mjs ignore-value design-system-font-size "*" --file "src/overlay/widget.js" --reason "Widget owns its type scale"
node skill/scripts/hook-admin.mjs ignore-file "src/legacy/Card.tsx"
```

Do not “fix” intentional design solely to silence the hook, and do not add inline disables to skip real defects.

## Operational checklist

<Steps>
  <Step title="Install skill and hook">
    Run `npx impeccable install` (or provider plugin install). Accept hook consent or pass `--no-hooks`. Complete harness trust (Codex `/hooks`, Grok `/hooks-trust`, Cursor Hooks on).
  </Step>
  <Step title="Verify Node and status">
    Ensure Node 22+ is on the agent shell PATH. Run `/impeccable hooks status` (or `node skill/scripts/hook-admin.mjs status`) and confirm `state: enabled` with no unexpected `IMPECCABLE_HOOK_DISABLED`.
  </Step>
  <Step title="Edit a UI file">
    After Edit/Write on a scanned extension, expect either findings context, a clean ack (unless quiet), or Cursor deny. Set `hook.auditLog` or `IMPECCABLE_HOOK_LOG` if you need NDJSON traces.
  </Step>
  <Step title="Handle findings">
    Fix real issues, or after explicit user confirmation persist the narrowest ignore. For full taste/copy rules deferred under tiering, wait for Stop or run `npx impeccable detect` / `/impeccable audit`.
  </Step>
</Steps>

## Failure modes and skips

| Situation | Hook behavior |
|---|---|
| `hook.enabled: false` or `IMPECCABLE_HOOK_DISABLED` | No scan |
| Native platform | No scan |
| Missing/unsupported Node | Guarded command exits 0; Claude/Codex may show one `systemMessage` |
| Detector missing / throws | Allow / silent; audit may record `detector-missing` / `detector-threw` |
| Malformed config JSON | File ignored; defaults + remaining config used |
| >6 edits on same file (post-edit) | Suppression notice once, then quiet for that file this session |
| No automatic hook in harness | Skill may require a manual detector run at end |

## Relation to CLI detect

| | Design hook | `npx impeccable detect` |
|---|---|---|
| Trigger | Harness edit/Stop events | Explicit CLI |
| Controlled by `hook.enabled` | Yes | No |
| Shares detector ignores / designSystem | Yes | Yes (unless `--no-config`) |
| Blocks writes | Cursor only | No (exit codes for CI) |

## Next

<CardGroup>
  <Card title="Installation" href="/installation">
    Install paths, provider selection, and hook consent on install/update.
  </Card>
  <Card title="Provider harnesses" href="/provider-harnesses">
    Hook capability matrix and trust steps per AI harness.
  </Card>
  <Card title="Manage detector ignores" href="/manage-detector-ignores">
    ignoreRules, ignoreFiles, ignoreValues, and inline disable comments.
  </Card>
  <Card title="Configuration reference" href="/configuration-reference">
    Full `.impeccable/config.json` and config.local.json keys.
  </Card>
  <Card title="Environment variables" href="/environment-variables">
    IMPECCABLE_HOOK_* and related env overrides.
  </Card>
  <Card title="Modes and platform" href="/modes-and-platform">
    Native platform values that skip the design hook.
  </Card>
  <Card title="Detector rules" href="/detector-rules">
    Rule registry, categories, and finding shape.
  </Card>
  <Card title="Detect in CI" href="/detect-in-ci">
    Non-interactive detect without relying on the edit hook.
  </Card>
</CardGroup>
