Files
pbakaus_impeccable/skill/reference/hooks.md
T
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

7.8 KiB

/impeccable hooks

Manage the design detector hook for the current project.

The hook runs the impeccable design detector on direct file edits to design-relevant files (.tsx, .jsx, .html, .vue, .svelte, .astro, .css, .scss, .sass, .less, .ts, .js). Claude Code, Codex, and GitHub Copilot use a post-tool-use hook and push a short system reminder into the agent's context after the edit; findings get a correction prompt, pending issues get a re-nudge, and clean UI-ish files get a short ack unless quiet mode is on (hook.quiet in config). Plain .ts and .js files are still scanned, but stay quiet unless the detector finds something. Cursor uses preToolUse to block bad proposed writes before they land and stays silent when it allows a clean write.

This command toggles the hook per project by editing .impeccable/config.json (the unified Impeccable config; hook runtime settings live under its hook key, and shared detector ignores live under detector). Per-developer overrides, including the install consent decision (hook.consent) the CLI records, live in the gitignored .impeccable/config.local.json. Set hook.enabled: false to turn the hook off, hook.quiet: true to silence the clean/pending acks, or hook.auditLog to a file path for an NDJSON log. The legacy IMPECCABLE_HOOK_DISABLED, IMPECCABLE_HOOK_QUIET, and IMPECCABLE_HOOK_LOG env vars are still honored and override these config values when set.

Manual npx impeccable detect scans use the same project filter config by default: detector.ignoreRules, detector.ignoreFiles, detector.ignoreValues, and detector.designSystem.enabled. hook.enabled only controls automatic hook execution, not manual CLI scans. Use npx impeccable detect --no-config ... for a raw detector run that ignores project config/context. Use npx impeccable ignores ... for direct CLI CRUD on the same detector ignores.

Supported harnesses: Claude Code (.claude/settings.local.json in the project, which is gitignored so the hook stays machine-local; a hook you move into the shared settings.json is honored in place too), Codex (.codex/hooks.json in the project), Cursor (.cursor/hooks.json in the project), and GitHub Copilot (.github/hooks/impeccable.json in the project, a team-shared committed file that both the Copilot CLI and the cloud agent read). For the Copilot CLI, repo-level hooks fire once .github/hooks/impeccable.json is committed to the repository's default branch.

On Cursor, preToolUse checks proposed Write/Edit/Shell write content and denies only when the real detector finds an issue. The denial message is visible to the agent as the tool error, so the agent can reconsider before the bad write lands.

Routing

The first argument is the action. Defaults to status.

Action What it does
status Print current state, shared/local config paths, ignored rules / files / values, env override.
on Set enabled: true in .impeccable/config.json, record local hook consent as accepted, and install/repair provider hook manifests when the skill is installed.
off Set enabled: false in .impeccable/config.json.
ignore-rule <id> Append <id> to detector.ignoreRules; for overused-font, requires --all-values.
ignore-file <glob> Append <glob> to detector.ignoreFiles.
ignore-value <id> <value> [--shared] [--reason "..."] Append a rule/value suppression to shared .impeccable/config.json.
ignore-value <id> <value> --local [--reason "..."] Append a private rule/value suppression to .impeccable/config.local.json.
reset Delete the project config, dedup cache, and Cursor pending queue.

Flow

  1. Resolve the action from the user's argument. If no action was given, default to status.

  2. Invoke the admin script and pass the user's output through verbatim:

    node {{scripts_path}}/hook-admin.mjs <action> [args...]
    
  3. If <action> is off, follow up with a one-line note: "Done. New edits will not trigger the design hook in this project until you run {{command_prefix}}impeccable hooks on."

  4. If <action> is on, follow up with: "Done. The design hook will fire after the next Edit/Write/MultiEdit on a UI file."

  5. If <action> is ignore-value, ignore-file, or ignore-rule, just print the script output. The default scope is shared .impeccable/config.json; add --local only when the user explicitly asks for a private exception.

  6. If <action> is status, just print the script output. Do not add commentary unless the user asked a follow-up question.

Intentional findings

The hook itself never writes ignore config. Persist an exception only after the user explicitly confirms the flagged issue is intentional, and always go through hook-admin.mjs.

Prefer the narrowest exception:

  • If the finding line shows an exact ignore-value command, run that command. This writes shared .impeccable/config.json by default.
  • For value-specific findings such as overused-font and bounce-easing, use ignore-value when the user confirms the specific value. Do not use ignore-rule overused-font for a specific font.
  • If the finding has no value-specific command, such as side-tab, prefer ignore-file <path> for the current file.
  • Use ignore-rule <id> only when the user asks to suppress that whole rule across the project. For broad overused-font suppression, use ignore-rule overused-font --all-values only when the user asks to ignore overused fonts generally.
  • Do not add source comments such as impeccable: ignore; inline comments pollute code and are not a supported suppression mechanism.

Example value-specific exception:

node {{scripts_path}}/hook-admin.mjs ignore-value overused-font Inter --shared --reason "User confirmed Inter is intentional"

Example intentional motion exception:

node {{scripts_path}}/hook-admin.mjs ignore-value bounce-easing bounce-ball --shared --reason "User confirmed ball bounce animation is intentional"

Example whole-rule font exception:

node {{scripts_path}}/hook-admin.mjs ignore-rule overused-font --all-values --reason "User asked to ignore overused fonts generally"

Example file-scoped exception:

node {{scripts_path}}/hook-admin.mjs ignore-file "src/legacy/Card.tsx"

Constraints

  • Never modify .impeccable/config.json or .impeccable/config.local.json by hand from this command. Always go through hook-admin.mjs so writes stay validated and the file shape stays consistent.
  • Do not edit the hook scripts themselves (hook.mjs, hook-lib.mjs, hook-before-edit.mjs) from this flow. Those are skill plumbing.
  • Cursor can block a proposed write when the detector finds a real issue. Claude Code, Codex, and GitHub Copilot do not block the edit; they emit a post-edit reminder instead. Disabling stops both blocking and reminders.
  • The hook is bundled with the Impeccable skill and installed through project-local manifests: .claude/settings.local.json, .codex/hooks.json, .cursor/hooks.json, and .github/hooks/impeccable.json. On Codex, the user must approve the hook via /hooks the first time. On Cursor, confirm hooks are enabled under Settings -> Hooks. On GitHub Copilot, the CLI loads .github/hooks/impeccable.json once it is committed to the repository's default branch, and the cloud agent reads it from the repo directly.

Failure modes

  • If .impeccable/config.json or .impeccable/config.local.json is unreadable or malformed, the hook ignores that file and uses the remaining valid config/defaults. hook-admin.mjs status will show malformed files as ignored.
  • If the user asks to "disable the hook" globally, lead with {{command_prefix}}impeccable hooks off (persistent for this project; writes hook.enabled: false to config). The legacy IMPECCABLE_HOOK_DISABLED=1 env var also works as a one-shot override that follows the shell.