Two Cursor Bugbot Medium findings on the merged monorepo context PR:
- Excluded packages still listed: discoverTargetCandidates added every glob
match but never applied negated workspace patterns, so an excluded package
(e.g. "!packages/internal") showed up as a selectable target even though
resolveWorkspaceProjectRoot sends it back to the repo root. Now filtered
with the same isExcludedByWorkspacePattern check the resolver uses.
- Empty app list blocks root: resolveTargetSelection returned
TARGET_SELECTION_REQUIRED whenever projectRoot === repoRoot, even with zero
discoverable child apps (e.g. `workspaces: ["."]`), leaving an unanswerable
prompt. It now returns null (use the repo root as the project) when there
are no candidates.
Also documents two Greptile P2 clarity notes (the four contextSourceStatus
labels incl. the dual meaning of 'fallback', and the deliberate
isMonorepoRoot-before-hasGitBoundary ordering in findMonorepoRoot).
Adds regression tests for both behaviors.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Context files (PRODUCT.md / DESIGN.md) resolve child-first then fall back to the repo root, and /impeccable live lets the user pick a child app in a monorepo. Single-app behavior is unchanged. Closes#202. Co-Authored-By: abdulwahabone
Unknown/mistyped CLI subcommands now print 'Unknown command' and exit non-zero instead of silently routing to the detector. Closes#266. Version bump and changelog entry deferred (batching). Co-Authored-By: abdulwahabone
Audit of every user-facing surface that enumerates supported harnesses
found GitHub Copilot missing or buried in several. Bring it to parity with
Claude Code, Codex, Cursor, and Gemini.
Missing -> added:
- site/content/reference/hooks.md: the public /docs/hooks page (tagline,
the post-edit list, and the manifest table) now covers GitHub Copilot,
including the `.github/hooks/impeccable.json` surface and the
default-branch/trust note. (Only skill/reference/hooks.md was updated in
the feature PR; this is the website doc.)
- README.md Design hook section + the manifest surface list.
- site/content/tutorials/getting-started.md hook note.
- site/pages/faq.astro tool-specific setup list and the docs-links list.
- PRODUCT.md audience line and README.npm.md suite description.
Prominence + naming:
- README "Supported Tools" and the homepage hero logo row: move GitHub
Copilot up to third (after Claude Code) instead of trailing.
- site/pages/designing: list GitHub Copilot earlier, full name.
- README "Supported Tools": the harness link now points at GitHub Copilot
(github.com/features/copilot) instead of the unrelated VS Code entry.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* 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>
* Guard plugin/skill version drift in the build (issue #274)
The Claude Code marketplace installs from the committed ./plugin subtree,
so a version disagreement between the hand-edited manifests and the
generated subtree ships stale content under a wrong version. This is the
class of bug reported in #274: a version bump that doesn't regenerate
./plugin (e.g. PR #252, where root plugin.json was 3.7.0 while
plugin/.claude-plugin/plugin.json was still 3.6.0) merges a drift window
onto main, and marketplace/Cowork installs pull the stale subtree.
Add a build-time validator that treats root .claude-plugin/plugin.json
as the source of truth and fails the build if any of these disagree:
- .claude-plugin/marketplace.json plugins[0].version (hand-edited; the
post-merge sync workflow never bumps versions, so it can't repair a
mismatch here)
- plugin/.claude-plugin/plugin.json version (generated subtree)
- plugin/skills/impeccable/SKILL.md frontmatter version (bundled skill)
It only fires on an inconsistent bump; PRs that don't touch versions keep
every file in agreement and stay silent. The pure comparison lives in
scripts/lib/validate-plugin-versions.js with direct unit coverage; build.js
owns the logging and the non-zero exit. Documents the regenerate-on-bump
step in CLAUDE.md's Versioning section.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Harden version-drift collector against malformed/incomplete manifests
Address Greptile review on #278:
- Wrap every file read/parse in a sentinel helper (extractFromFile) so a
half-edited manifest — the exact state during a version bump — yields a
clean "could not parse (...)" diagnostic naming the file instead of a raw
JSON.parse stack trace out of build().
- Report a present-but-malformed root plugin.json, or one missing its
`version` field, as an explicit error. Previously `undefined` version
short-circuited the build wrapper's `source == null` guard and passed
silently. collectPluginVersions now returns an `errors` array; build.js
fails on errors + mismatches combined, and only the genuinely-absent root
manifest is a no-op skip.
Adds 4 unit tests: malformed checked manifest, malformed root, missing
version field, and the absent-root no-errors case.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Make SKILL.md frontmatter version read CRLF-tolerant
Address Cursor Bugbot review on #278: readSkillFrontmatterVersion only
matched `\n` delimiters, while the shared parseFrontmatter in
scripts/lib/utils.js accepts `\r?\n`. A bundled SKILL.md saved with CRLF
line endings would parse to a null version and trip a false mismatch
against root plugin.json even when the version line is correct.
Match the shared parser's `\r?\n` tolerance and drop the `$` anchor on
the version line (it would not match before a `\r`). Adds CRLF coverage
for both readSkillFrontmatterVersion and collectPluginVersions.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Re-trigger CI (no file change)
CI did not fire for 5cda9f6b; force a fresh run on the current tree.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Fix React hydration mismatch from live scroll-lock on SSR roots
The live overlay's startScrollLock disabled the browser's scroll
anchoring by setting `overflow-anchor: none` as an inline style on
`<html>` and `<body>`. On frameworks that server-render those roots
(notably Next.js App Router), that client-only inline style desyncs from
the server HTML, so React 19 logs "a tree hydrated but some attributes
of the server rendered HTML didn't match" on the next Fast-Refresh
re-render. It surfaced as a flaky failure of the nextjs-app-router
live-e2e fixture's expectConsoleClean probe.
Inject the suppression as a `<style>` rule keyed by a stable id instead
of mutating inline styles on hydrated host elements. Same computed
effect, but React no longer sees a client-only attribute on `<html>` /
`<body>`. The rule is recreated on every startScrollLock and removed on
teardown, so reload survival (driven by the persisted scroll key) is
unchanged.
Adds a regression guard pinning the new shape (no inline overflowAnchor
mutation on html/body; injected <style> created and removed by id).
Verified end-to-end: the nextjs-app-router live-e2e fixture now passes
the expectConsoleClean probe deterministically.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Relax regression-guard regex spans to {0,400}
Address Greptile review: the {0,200}/{0,220}/{0,160} character-span
limits between the injected-style constructs were tight enough that an
innocent refactor or added comment inside startScrollLock could silently
break the shape-check. Widen each segment to {0,400}; the guard still
passes on the fix and still fails when the inline html/body overflowAnchor
mutation is reintroduced.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Replace npx hints in live scripts with bundled-script paths
The live-mode poll/wrap scripts are invoked by the agent via
`node {{scripts_path}}/live-*.mjs`, never through the `npx impeccable`
CLI. Their help text and runtime error hints still pointed at
`npx impeccable poll|live|wrap`, which is misleading and, for the
error paths, not directly runnable.
- Docstrings/comments (never executed): switch to the
`node <scripts_path>/...` convention already used by live-server.mjs.
- Runtime-printed error/usage strings: resolve the script's own dir via
import.meta.url and print a real, copy-pasteable absolute path instead
of a placeholder.
Verified by triggering the error paths from the synced bundle and by
running the live-mode E2E (vite8-react-modal) through the full cycle.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Quote script paths in runtime hints to handle spaces
Paths containing spaces (e.g. /Users/john doe/...) would otherwise
produce a non-runnable command. Addresses Greptile review feedback.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The toolbar badge counted flagged elements (state.findings.length) while
the popup and DevTools panel counted total anti-pattern findings, so the
same scan showed two numbers (e.g. 21 vs 34 on the design-system page).
Since the surfaces are labeled "anti-patterns", count total findings in
the badge too so all three agree.
Closes#262
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix: surface scan failures in extension popup for local files
Scanning a local file:// page with "Allow access to file URLs" off left
the popup stuck on "Scanning..." because the blocked content-script
injection returned silently. ensureContentScriptInjected() now returns the
real error, and sendScanToTab() sends a scan-failed message that the popup
renders as a small line, with a permission hint shown only for file:// tabs.
Fixes#258
Co-authored-by: Cursor <cursoragent@cursor.com>
* Improve: report the actual error when a non-file scan fails
The generic "This page can't be scanned." gave no reason. Non-file failures
now read "Couldn't scan this page: <error>" so the user sees what Chrome
reported instead of a dead end.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix: scope popup broadcasts to the active tab
The popup acted on every findings-updated / scan-failed / overlays broadcast
regardless of which tab it targeted, so a background or DevTools-driven
rescan on another tab could reset the button or show a spurious error. Cache
the active tab id and ignore broadcasts for other tabs.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
The bundled detector's cli/main.mjs imports ../../lib/impeccable-config.mjs,
which in the source CLI resolves to cli/lib/impeccable-config.mjs. The skill
build only copies cli/engine/** into scripts/detector/**, leaving that
dependency behind, so from the bundled scripts/detector/cli/main.mjs the same
import resolved to scripts/lib/impeccable-config.mjs and failed with
"Cannot find module .../lib/impeccable-config.mjs". /impeccable critique (and
any detector-backed command) crashed on startup for every provider since #252.
Teach the detector bundler to copy out-of-bundle engine dependencies into the
skill's scripts/lib/, and add a build test that walks every bundled script and
asserts each relative import resolves to another bundled file, so a future
out-of-bundle dependency fails the test instead of the user.
Skill v3.7.1 (patch). CLI unchanged — the engine resolves fine in the CLI's
own tree; only the skill bundling was wrong.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On Node v24.16.0 / v26.1.0+, `impeccable install` printed "Downloading
impeccable skills...", exited 0, and installed nothing. A Node streams
regression (nodejs/node#63487) made pause()/resume() no-ops on destroyed
streams, stalling extract-zip's yauzl/fd-slicer read stack partway through;
its promise never settled and the process exited clean with no error.
Swap extract-zip for fflate across both extraction call sites
(downloadAndExtractBundle, copyOrExtractLocalBundle) via a new extractZip
helper. fflate decompresses from an in-memory buffer and never touches the
fs stream path, so it is immune on every Node version. It is pure JS with
zero dependencies, so the Windows fix from #198 (no `unzip` binary) holds.
Unlike extract-zip, fflate is actively maintained.
Because extractZip writes entries itself, it guards against zip-slip (`../`
entries escaping the target dir). Tests add a many-file regression guard
(fails on partial extraction) and a zip-slip rejection test.
Verified end-to-end: the real 1,194-file universal bundle extracts and
installs completely.
Fixes#250.
Co-authored-by: Claude <noreply@anthropic.com>
* feat(cli): interactive hook consent + unified .impeccable/config.json
Make the design-hook install a conscious choice and unify scattered config
into one file.
Interactive consent
- On an interactive `skills install`/`update`, the CLI explains what the hook
does and offers to install it (default yes), then records the per-developer
decision in the gitignored `.impeccable/config.local.json`, so it never
re-asks. A recorded decision or an already-installed hook short-circuits;
`-y`/non-TTY keeps the historical install-by-default behavior; `--no-hooks`
is a one-off skip that records nothing. The trigger keys on "is the hook
installed?" + "is there a recorded decision?", not a brittle version check.
Unified config
- `.impeccable/config.json` (shared) and `.impeccable/config.local.json`
(gitignored) now hold all Impeccable settings: hook settings under a `hook`
key, plus top-level `updateCheck`. `/impeccable hooks` writes the `hook`
subtree, preserving siblings. The hook runtime reads `hook.quiet` and
`hook.auditLog`; context boot reads `updateCheck`. The legacy
`IMPECCABLE_HOOK_DISABLED|QUIET|LOG` and `IMPECCABLE_NO_UPDATE_CHECK` env vars
still work and override config; docs now lead with config and treat env vars
as a legacy note.
- No backward compat for the pre-unification `hook.json`/`hook.local.json`
(the hook shipped an hour ago; nothing in the wild uses it). This repo's own
hook config is migrated to `.impeccable/config.json`.
The CLI and skill scripts are separate trees, so a small CLI-side config module
(cli/lib/impeccable-config.mjs) duplicates the config-path and .git/info/exclude
handling; comments flag the duplication.
Tests: new cli config unit test; skills-cli consent tests (declined skips,
accepted installs, --no-hooks records nothing); hook.test.mjs back-compat
removed and quiet/auditLog-from-config + gitexclude coverage added. Full suite
green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(hooks): preserve sibling config fields + resolve audit log from event cwd (Bugbot)
Two Bugbot findings:
- High: `/impeccable hooks` edits replaced the whole `hook` object with the
merge-helper output, dropping fields those helpers don't manage — so an
`ignore-value --local` could wipe the recorded install consent and make the
CLI re-prompt. writeConfig now merges over the existing hook object, keeping
consent/quiet/auditLog.
- Medium: config-based audit logging resolved hook.auditLog from process.cwd(),
which can differ from the hook event's project root (and Cursor's pre-edit
hook passed no cwd). The hook now stamps the resolved project root on the
audit entry, and writeAuditLog reads config from entry.cwd when present.
Tests: a /impeccable hooks edit preserves consent + quiet; writeAuditLog
resolves config auditLog from entry.cwd, not the fallback cwd.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(hooks): resolve a relative auditLog path against the project root (Bugbot)
A relative hook.auditLog was read from the project root but written relative to
the hook process cwd, so when those differ the log went to the wrong place.
writeAuditLog now resolves a relative target (from env or config) against the
same project root it reads config from. Absolute and ~/ paths are unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Fix hook consent recovery and smoke config
* Fix hook consent explainer for Cursor
* Fix empty hook target consent
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* 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>
* 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>
* Fix: tear down annotation overlay when Escape exits live pick mode.
The configure prompt auto-focuses and bypasses the global Escape handler, so its local path must hide the annot overlay; togglePick off now does the same as a safety net.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Improve live mode steer pill typing affordance.
Show a visible caret and placeholder when focused, expand on pointerdown, and drop the muddy border so the graphite surface carries the affordance alone.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Improve live mode configure bar layout and pill styling.
Align pills and input on a shared text track, refine muted pill chrome with a quiet action border, and center the row with symmetric inset so spacing reads evenly in the 36px bar.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Add x1 to live mode variant count picker.
The configure bar count pill now cycles 1→2→3→4→1 so users can request a single variant.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Polish live mode configure bar, edit badge, and action picker.
Refine selection pill layout and tooltips, shrink edit copy to an icon aligned with the outline, right-align the action picker, and sync demo styles and regression coverage.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix live mode element nav when configure input is focused.
Passthrough empty arrow keys from the configure and steer prompts so handleKeyDown can move between pickable elements without breaking autofocus typing.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Remove accidental live.js inject from Base.astro.
Strip the localhost helper script tag left over from local live mode iteration so the PR ships only intentional UI changes.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix review findings: pick-cursor state sync, anchor recovery, e2e selectors.
Code review of this branch surfaced ten confirmed bugs plus three smaller
ones; this commit fixes all of them.
- Route every interaction-state transition through a new setLiveState()
helper that re-syncs the pick-mode crosshair, fixing four confirmed
cursor bugs: never appearing on pick toggle (sync ran before the state
change), sticking through the configure phase, surviving teardown
page-wide, and the style mounting inside the adapter's shadow root
where it can't match the host document (now document.head).
- Anchor recovery: a matching id is decisive again (hashed class names
and component tags broke recovery), empty-text elements can no longer
match the fuzzy text passes (".includes('')" hole plus shortest-text
preference), and the dead 2-class-subset fallback is removed.
- Selection pill: drop the hover-only "armed" guard so keyboard
activation works; the pill arms on focus as well as hover.
- Configure chrome: remove the configure-bar tooltip on teardown, align
restorePickerBarChrome padding with initBar (5px), share the
configure-input stylesheet with the insert row, and sync the
ui-core.mjs surface inventory with live-browser.js.
- Site demos: delete the stale duplicate .live-demo-ctx-selection rule
that killed the teal pill on dark pages, and keep the configure-phase
demo bar on the overlay's dark surface in light mode so the near-white
prompt text stays readable.
- E2E/contract tests: match the icon-only submit button by aria-label
("Generate variants") instead of the removed "Go" text, and update
source-contract pins for setLiveState and buildConfigureSubmitButton.
Verified: bun run test green, live-mode E2E 23/23 across all fixtures.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Wire insert voice button into syncVoiceUi listening state.
Voice on the insert configure row runs through the same 'configure' mode,
but syncVoiceUi only stamped data-listening/aria state on the replace
row's #impeccable-live-configure-voice, so the insert button never pulsed
while listening. Target whichever of the two row buttons is mounted, the
same either-row pattern syncConfigureInputChrome uses.
Addresses Bugbot review comment on PR #242.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Reinject from source when the session wrapper lands during anchor recovery.
The anchor-recovery observer stood down as soon as the session's variant
wrapper appeared in the DOM, without running injectVariantsFromSource.
A wrapper can land incomplete (wrap HMR landed, variant insert did not),
which is exactly the case injectVariantsFromSource's existing-wrapper
replace path handles - so recovery ended with the bar stuck and no
variants. Route both the anchor-found and wrapper-landed cases through
injectVariantsFromSource, which owns wrapper replacement, recovery-flag
clearing, and variant display.
Addresses Bugbot review comment on PR #242.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Restore inline edit drafts before configure chrome teardown disables editing.
teardownConfigureChrome called disableInlineEdit() ahead of hideBar(),
wiping inlineEditRows and the impeccableOriginalText metadata that
hideBar()'s EDITING-state restoreInlineEditDrafts() needs - so turning
Pick off mid "Edit copy" left edited DOM text in place, neither saved
nor canceled. Let hideBar() own the sequence: it restores drafts first,
then disables inline edit.
Addresses Bugbot review comment on PR #242.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>