Commit Graph
19 Commits
Author SHA1 Message Date
Paul Bakaus 4f7fdeca6b Build: ship launcher-only release zips by default
IMPECCABLE_BUNDLE_ENGINE=1 opts in to staging the engine binaries into the
dist skill copies. Bundling every target into every provider copy put
dist/universal.zip near 340 MB, past the 25 MB Cloudflare Pages file cap
that impeccable install downloads through.

Prepared with AI assistance (Claude Code).
2026-08-31 19:59:21 -07:00
Paul Bakaus 27b727a92f Tests: tighten the hook command guard assertion
Prepared with AI assistance (Claude Code).
2026-08-31 19:59:20 -07:00
Paul Bakaus 11a1ea64a6 Build: ship the launcher instead of bundling the JS engine
readSourceFiles no longer copies cli/engine into the skill; the scripts
payload is the launcher (executable bit preserved through dist, plugin/, and
universal.zip), impeccable.cmd, VERSION (synced from ENGINE_VERSION on every
build), the page JS, and command-metadata.json. Hook manifests call
`<scripts>/impeccable hook` behind an existence guard (Codex adds a
commandWindows sibling calling impeccable.cmd; Cursor runs hook-before-edit;
GitHub keeps the git rev-parse form; Grok mirrors Claude); the Node probe and
systemMessage notice are gone. build:release fetches the pinned engine for
every target (lenient) and stages bin/<os-arch>/ into the dist skill copies
after root harness dirs and plugin/ were synced, so git-delivered trees stay
launcher-only. The detection-rule count check reads the vendored
extension/detector/antipatterns.json and is skipped when absent.
build:browser is a stub; the codex prefix rewrite leaves
`{{scripts_path}}/impeccable` alone.

Prepared with AI assistance (Claude Code).
2026-08-31 19:57:32 -07:00
Paul Bakaus 611147a333 Sync marketplace Claude hook repair
Keep the committed marketplace repair script aligned with Claude Code's supported Edit and Write tools, and strengthen regression coverage after automated review.\n\nThis change was prepared with AI assistance under maintainer authorization.
2026-08-19 10:04:14 -07:00
Paul Bakaus 7d5c60d291 Remove stale Claude MultiEdit matcher
Claude Code now folds multi-edit behavior into Edit, so keep generated and repaired hook manifests aligned with the current Edit and Write tools. Grok keeps its compatibility matcher unchanged.

AI assistance was used to implement and validate this change.
2026-08-19 09:28:28 -07:00
Rex LorenzoandGitHub 14d2641685 Fix: keep the node runtime probe clear of cmd.exe metacharacters (#458)
Volta's Windows shims exec through `cmd /C`, which re-parses the argument
list, so the `>=` inside the probe's `node -e` payload was read as output
redirection. The command died with "The filename, directory name, or volume
label syntax is incorrect" before node started, the guard read that as a
missing runtime, and the hook it exists to protect was disabled on every
PostToolUse and Stop. A user on a supported Node 24 got a one-time notice
telling them to install Node 22, then silence.

Clamping with Math.min is the same floor test in the same ES5-only syntax,
with no character cmd.exe can claim. Verified through the Volta shim on Node
24.16.0 and 22.18.0 (exit 0) and against a real Node 20.6.1 binary (exit 1),
so the floor is unchanged. Adds a regression test asserting no `<`, `>`, or
newline reaches any generated `node -e` payload.

Upstream cause: volta-cli/volta#1791.

Prepared with AI assistance (Claude Code).
2026-08-03 15:02:29 -07:00
fd9076f4f0 Enforce the engines floor in the probe instead of a capability check
The probe asked whether node could load ESM, while the notice promised a
Node 22 floor and package.json engines declares >=22.12.0. Reviewers kept
flagging the gap, and they were right to: a 14.18-to-21 runtime passed the
probe on the strength of one import while the hook and its detector bundle
are only ever exercised on the engines floor, so "can load our code" was a
weaker claim than the one being made for it.

Check the floor directly: parseInt(process.versions.node) >= 22, in
ES5-only syntax that parses on any node old enough to fail it. Probe and
notice now derive from one NODE_MAJOR_FLOOR constant, so they cannot
disagree, and the archaeology about node: scheme support and pre-15
unhandled-rejection semantics goes with the import it explained.

Add the missing contract test: every generated hook command carries the
probe, the notice appears exactly where a harness can render it (Claude
and Codex, project and plugin), and the expected floor is read from
package.json engines rather than repeated by hand.

Verified against a fake pre-22 node, no node, and a real node: one notice
then the marker holds it silent, exit 0 in every failure shape, and the
hook's own exit code still passes through on a supported runtime.

Co-Authored-By: Claude Fable 5 (via Cursor) <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-26 22:17:19 +05:00
Abdul WahabandClaude Opus 5 0c19098754 Guard the remaining harness manifests against a dead node runtime
Bugbot caught the Codex plugin builder still invoking node directly, and the
same reasoning covers GitHub Copilot and Grok Build: all three shipped the
exact failure this branch exists to stop, and sat visibly inconsistent with
their guarded siblings.

Route them through guardedNode with no notice, matching Codex and Cursor.
GitHub gains a second property from it: outside a git repository
`$(git rev-parse --show-toplevel)` expands to nothing, so the old command
handed node a path that could not exist and failed the turn. The file test
now short-circuits that to exit 0.

Every builder carries the probe; only the two Claude manifests carry the
notice, which is the only harness whose response shape is confirmed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 20:58:49 +05:00
Paul BakausandClaude Fable 5 73819ff573 Stop hook-build test from asserting an unbuilt dist artifact
The "Codex project hooks reference hook.mjs in the .codex skill payload"
test asserted dist/codex/.codex/skills/impeccable/{SKILL.md,hook.mjs}
exist. dist/ is gitignored, and CI's test:core step runs before the
Build step, so the fresh checkout has no dist/ when the assertion runs.
It only passed locally against a stale dist/. This turned every
sync-generated-output push on main red.

The dist/codex bundle's self-consistency is already covered by
build.test.js, which runs an actual build into a temp dir and verifies
the codex payload lands at .codex/skills/. Drop the two dist assertions;
the test keeps verifying the tracked outputs (the .codex/hooks.json path
and the .agents/skills payload) that exist at test:core time.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:28:12 -07:00
Paul BakausandClaude Fable 5 bcf354cd0c Fix Codex hook path so .codex-directory installs run the detector
The committed .codex/hooks.json hardcoded .agents/skills/impeccable/scripts/
hook.mjs. On a .codex-directory install the skill payload lives at .codex/
skills/..., so the guarded command ([ ! -f X ] || node X) found no file and
silently no-opped, leaving the design detector dead for those users.

Derive the hook payload path from the emitting provider's own configDir rather
than hardcoding .agents:

- buildCodexHooksManifest(skillDir) now builds `${skillDir}/skills/impeccable/
  scripts/hook.mjs`; hooksJsonFor threads each provider's configDir through. The
  Codex provider (configDir .codex) emits .codex/skills; the root sync and the
  self-consistent dist/codex bundle both point at their own payload.
- CLI installer: project-scope hook rewriting now derives the provider's own
  project-relative path instead of preserving the bundle token. The Codex bundle
  ships a .codex/skills command, but the CLI lays the skill at .agents/skills, so
  the installed .codex/hooks.json is rewritten to .agents/skills (Claude keeps
  its ${CLAUDE_PROJECT_DIR} token; global installs keep the absolute rewrite).

Per-provider hook payload path after the fix:

  Emission                              hook path
  dist/codex/.codex/hooks.json          .codex/skills/impeccable/scripts/hook.mjs
  root .codex/hooks.json (build sync)   .codex/skills/impeccable/scripts/hook.mjs
  CLI .agents (codex) project install   .agents/skills/impeccable/scripts/hook.mjs
  CLI .agents (codex) global install    <home>/.agents/skills/.../hook.mjs (abs)
  .claude / .cursor                     unchanged

Tests: extended hook-build (codex-dir -> .codex/skills, agents-dir -> .agents/
skills) and skills-cli (bundle ships .codex/skills, install rewrites to .agents/
skills). Regenerated tracked .codex/hooks.json via build:release.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:09:33 -07:00
Paul BakausandClaude Fable 5 eda81f0937 Release prep: skill v4.0.1
Bump plugin + marketplace to 4.0.1 and sync the regenerated provider
output: the guarded hook commands from issue #399 (a missing hook file
exits 0 instead of crashing every turn of a user-level install), the
canon standing exit, the visualize flow, the two shipped subagents, and
the interactive-spine fixes from today's live testing. Detector count
validates at 59 with undersized-ui-text.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 12:23:07 -07:00
Paul Bakaus 06d21dea7d Add first-class Grok Build harness support
Emit .grok skills, agents, and PostToolUse/Stop hooks; wire the CLI
installer and downloads; fix the plugin install path to #plugin; and
document Grok in HARNESSES.md and README.

AI assistance: written with Grok Build.
2026-07-21 18:02:58 -07:00
Paul BakausandClaude Fable 5 c3aba1e343 hooks: two-tier design hook — immediate per-edit rules + full-set Stop deep pass
Eval evidence showed the per-edit PostToolUse stream fires overwhelmingly
on copy-level rules (em-dash-overuse ~97x/session) and measurably makes
models more conservative, while a full-detector pass at completion is what
actually fixes contrast/padding/glow. Split the hook accordingly:

- Per-edit (PostToolUse) now surfaces only IMMEDIATE_TIER_RULES: broken
  output (broken-image, text-overflow, clipped-overflow-container,
  body-text-viewport-edge), objective contrast/legibility failures
  (low-contrast, gray-on-color, tiny-text), single-property mechanical
  slop (gradient-text, dark-glow), and design-system drift (the four
  design-system-* rules, which compound if left uncorrected). Everything
  else defers. Override with hook.perEditRules: "all" in
  .impeccable/config.json. Tiering is off for Cursor/Copilot harnesses,
  which have no Stop pass wired, so nothing gets silently dropped there.

- Stop deep pass (runStopHook): runs the FULL rule set over every UI file
  touched this session (tracked via the existing hook.cache.json session
  state; deferred-only edits now mark the file touched), dedupes against
  everything already surfaced per-edit, honors ignore-rule/file/value and
  inline disables, reuses the [impeccable@1] envelope, and no-ops fast
  when no UI files were touched. Emits hookSpecificOutput
  { hookEventName: "Stop", additionalContext } per the Claude Code SDK
  Stop contract (conversation continues so the model can act on it).
  Second Stop fire is silent - deep-pass findings are remembered.

- Wiring: Stop entries (timeout 30) in plugin/hooks/hooks.json, the
  .claude settings + .codex hooks manifests (transformers + hook-admin
  repair path). Claude Code and Codex both dispatch a native Stop event;
  Cursor's stop hook is inconsistently dispatched (pre-write gate stays)
  and Copilot's agentStop/sessionEnd don't inject model context, so
  neither gets a Stop entry - documented in reference/hooks.md.

- Tests: tiering split/override/harness gating, Stop dedupe + silent
  no-touched-files + ignore machinery + kill switches; existing per-edit
  tests moved to immediate-tier rule ids. 181 tests green; smoke-tested
  the built dist skill end to end (glow surfaced per-edit, em-dash only
  at Stop, second Stop silent).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 16:46:00 -07:00
Paul BakausandGitHub da99645a58 Add OpenAI plugin submission bundle (#363)
* Add OpenAI plugin submission bundle

Build a Codex-native OpenAI plugin with bundled hooks, public listing metadata, submission guidance, privacy coverage, and regression tests.

AI assistance: OpenAI Codex prepared and validated these changes under maintainer direction.

* Fix provider script command rendering

Replace heuristic rewrites across executable scripts with one explicit provider marker, render pinned shortcuts per target harness, and remove the personal email from the public publisher manifest.

Addresses automated review feedback on PR #363.

AI assistance: OpenAI Codex prepared and validated these changes under maintainer direction.
2026-07-09 17:09:13 -07:00
44c27a72af Fix Codex plugin hook load failure; bump skill to 3.9.1 (#333)
Codex loads bundled plugin lifecycle hooks from `hooks/hooks.json` using a
strict schema that accepts only the top-level `hooks` field. The
plugin-packaged manifest carried a top-level `description`, so Codex rejected
the whole manifest with `unknown field description, expected hooks` and the
post-edit design detector never registered (issue #330).

Drop `description` from `buildClaudePluginHooksManifest()` and regenerate
`plugin/hooks/hooks.json`. The Claude Code plugin path is unaffected (it only
reads the `hooks` object). Add a regression assertion for the plugin artifact
and bump the skill version to 3.9.1.


Claude-Session: https://claude.ai/code/session_013GTTHY6uHwESUyAgjUEm7x

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-01 11:03:55 -07:00
Paul Bakaus 1a3f5d78bd Fix Codex hook manifest schema 2026-06-30 23:56:06 -07:00
41ff946121 Add GitHub Copilot hook support (CLI + cloud agent) (#279)
* Add GitHub Copilot hook support (CLI + cloud agent)

Wire the Impeccable design detector into GitHub Copilot's hook system so
direct file edits get the same post-edit design feedback the Claude Code,
Codex, and Cursor harnesses already receive.

GitHub Copilot's contract differs from the existing harnesses (verified
against Copilot CLI 1.0.63):
- Repo-level manifest at `.github/hooks/impeccable.json` (read by both the
  CLI, once committed to the default branch, and the cloud/app agent).
- Flat `postToolUse` entries with `bash`/`timeoutSec` and a full-match
  `matcher` regex; the file-editing tools are `edit` and `create`.
- The stdin event uses camelCase `toolName`/`toolArgs`, where `toolArgs` is
  a JSON *string* carrying the touched file under `path`.
- Context is injected via a top-level `additionalContext` string.

Changes:
- hooks.js: buildGitHubHooksManifest() + route `github` in hooksJsonFor().
- providers.js: emitHooks/hooksManifestRel for the github provider.
- hook-lib.mjs: detect the github harness, normalize the camelCase event
  (parse the JSON-string toolArgs -> tool_input.file_path), and emit the
  `additionalContext` payload shape.
- hook-admin.mjs / skills.mjs: install + idempotent-repair the
  `.github/hooks/impeccable.json` manifest (bash-aware marker stripping).
- hooks.md: document GitHub Copilot as a supported harness.
- Tests for the builder, routing, event normalization, and end-to-end run.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Cover Copilot apply_patch edits in the hook (live-verified)

The first cut matched only `edit|create`, the tool names `copilot -p` uses.
A live trace against Copilot CLI 1.0.63 in an interactive session showed it
edits files via `apply_patch`, whose toolArgs is a raw OpenAI-format patch
string (`*** Begin Patch` / `*** Add File:`), not JSON. With the narrow
matcher the hook command never ran.

- hooks.js / hook-admin.mjs: matcher -> `edit|create|apply_patch`.
- hook-lib.mjs: normalizeGitHubEvent now routes apply_patch's raw patch
  string into tool_input.command (reusing the existing parseApplyPatchPaths /
  resolveTargetFiles plumbing) and only JSON-parses toolArgs for the
  edit/create/view tools. tool_name is normalized to apply_patch so the patch
  path is extracted even if a future build relabels the tool.
- Tests: apply_patch matcher assertions, event normalization, and an
  end-to-end runHook covering the interactive/cloud path.

Verified live: a trusted interactive `apply_patch` edit fires the hook and
returns the expected `additionalContext` design reminder.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Address review feedback + add changelog entry

- hook-lib.mjs (Bugbot, low): looksLikeApplyPatch no longer misroutes an
  edit/create event whose edited *content* contains apply_patch markers. A
  real apply_patch payload is a raw string that does not parse as JSON; an
  edit payload is a JSON object, so only non-JSON-object strings are treated
  as apply_patch. Edit events keep extracting `path`. Adds a regression test.
- skills.mjs (Bugbot, medium): document why `.github` is intentionally
  excluded from hookScriptPathForProvider. Its hook manifest is committed and
  shared (read by the Copilot cloud agent and teammates), so the command must
  stay portable via `$(git rev-parse ...)`; rewriting it to a machine-local
  absolute path would break those. GitHub skills are project-scoped, so the
  project-relative path resolves.
- changelog: add an Upcoming (v3.x placeholder) entry for the Copilot hook.
  Version is not bumped yet (batching with other changes).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 02:24:18 -07:00
9c0012d4e1 feat(hooks): package design hook in plugin, install to settings.local.json (#243)
* feat(hooks): package design hook in plugin, install to settings.local.json

Three related changes to how the Impeccable design hook is distributed,
plus an unrelated build fix discovered along the way.

Package the hook in the Claude Code plugin
- The marketplace / `/plugin install` path previously shipped the skill and
  agents but no hook, so those users never got the design detector. The build
  now emits `plugin/hooks/hooks.json` (auto-discovered at the plugin root),
  resolving the script via `${CLAUDE_PLUGIN_ROOT}` so it works wherever Claude
  Code unpacks the plugin instead of assuming a `.claude/skills/` layout.

CLI installs the hook into settings.local.json, not shared settings.json
- `npx impeccable skills install/update` now writes the Claude hook to the
  gitignored `.claude/settings.local.json` (a machine-local install side
  effect) rather than the team-shared `settings.json`, which could otherwise
  be committed and break for teammates without the skill installed.
- Graceful handling (leave-it-never-duplicate): if our hook already lives in
  the shared `settings.json` (a legacy install or a deliberate user move), it
  is honored in place and never duplicated into the local override, which
  would otherwise run the detector twice per edit.
- The skill's `/impeccable hooks on|off` toggle is unaffected: it only writes
  `.impeccable/hook.json`, never the settings files.

Fix universal.zip build failure under archiver v8
- `archiver` was bumped to v8 (now ESM, factory function removed) but
  `scripts/lib/zip.js` still used the old `archiver('zip', ...)` API, so every
  build silently failed to produce `dist/universal.zip` (the skill-release
  artifact). Switched to `new ZipArchive({...})`.

Also folds in a pre-existing local rename of the hook status message
("Scanning design" -> "Checking UI changes") and its regenerated provider
output.

Tests: new coverage for the plugin-packaged hook manifest and the
shared-settings honor-in-place path; existing CLI assertions moved to
settings.local.json. Full suite green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cli): detect hook by marker, not file existence (Bugbot)

hookInstalledForProvider treated any existing settings.local.json (or
hooks.json) as proof the hook was installed. Those files commonly hold
unrelated local settings, so the already-installed `skills install` path
would skip repairing a genuinely missing hook that `update` would add.

Detect the Impeccable marker in the file instead of mere existence. Adds a
test for the exact case: a settings.local.json with only permissions still
triggers hook repair and preserves the unrelated content.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(build): fail loud on a broken release zip + cover the zip writer

Close the gap that let the archiver v8 break ship a 0-byte universal.zip
with a green test suite:

- createProviderZip no longer swallows failures. It throws on a missing
  source, an archive with zero entries, or a 0-byte output, and build() now
  exits non-zero on any such rejection. A build that can't produce its release
  artifact fails instead of deploying an empty bundle.
- New tests/zip.test.mjs exercises the real zip writer and round-trips through
  extract-zip (the unpacker the CLI uses): a valid bundle unpacks to the skill
  tree, and the empty/missing-source cases throw. Wired into the core suite so
  it runs in `bun run test`.

Why this matters: the prior CLI e2e tests stub the bundle as a local
directory, so they never built, downloaded, or unzipped a real archive. The
zip writer had no coverage and failed soft, so Dependabot's archiver 7->8
major bump merged green and the deploy shipped an unusable bundle.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cli): scope hook marker scan to the hooks subtree + prune local dupes (Bugbot)

Two follow-ups from Bugbot:

- fileHasImpeccableHookMarker scanned the whole settings file as raw text, so
  an unrelated string (e.g. a permissions allow entry that mentions the hook
  path) could falsely read as an installed hook and block install/repair or
  the shared-settings skip. Now it parses the JSON and scans only the `hooks`
  subtree.
- When the hook is honored in the shared settings.json, copyProviderHooks
  skipped the local write but left a stale hook in settings.local.json from an
  earlier machine-local install, so Claude Code loaded both and ran the
  detector twice per edit. It now prunes the local copy (preserving unrelated
  local settings, dropping the file if only our scaffolding remained).

Adds tests for both: a permissions string mentioning the hook path still
triggers repair, and a shared hook prunes the stale local duplicate while
keeping unrelated permissions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 23:54:46 -07:00
672517f76e Add automatic design hook install and exceptions (#170)
* 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>
2026-06-13 21:19:19 -07:00