Skip to content

feat(hooks): declarative provider lifecycle hooks - #72

Merged
Minitour merged 11 commits into
developfrom
feat/hooks-support
May 29, 2026
Merged

Minitour merged 11 commits into
developfrom
feat/hooks-support

Conversation

@Minitour

@Minitour Minitour commented May 24, 2026 •

Copy link
Copy Markdown
Member

Closes #70.

Summary

Adds a top-level hooks: section to capabilities.yaml plus per-provider integrations for the four agents that ship project-local hook support today (Claude Code, Cursor, Codex, Gemini CLI). Hook entries are translated through a canonical event model and written into the provider's existing config file with name: capa:<id> (or id: <hookId> for TOML) tags so capa only ever updates or removes its own entries — user-authored hooks are preserved verbatim. Providers without a hooks integration emit a warning and skip; install never fails.

Design (per #70)

hooks:
  - id: audit-shell
    description: Append shell commands to ~/.capa/audit.log
    on: beforeShell
    command: 'echo "$(date) $TOOL_INPUT" >> ~/.capa/audit.log'
    timeout: 5

  - id: block-rm-rf
    on: cursor:beforeShellExecution        # provider-scoped event
    matcher: 'rm -rf *'
    failClosed: true
    command: 'echo blocked && exit 1'

  - id: lint-staged
    on: afterFileEdit
    source:
      type: github
      def:
        repo: acme/dev-toolkit::hooks/lint-staged.sh:v2.0.0
Provider Config Shape Tag
Claude Code .claude/settings.json → hooks claude (matcher groups) name: capa:<id>
Cursor .cursor/hooks.json (standalone) cursor (v1 envelope) name: capa:<id>
Codex .codex/config.toml → [hooks] codex-toml id: <hookId>
Gemini CLI .gemini/settings.json → hooks gemini (claude-style) name: capa:<id>

Source-backed hook bodies (github / gitlab / remote / local) materialise to ~/.capa/hooks/<projectId>/<hookId> so the project directory never gets polluted with generated files.

Implementation highlights

  • Types & registry (src/types/hooks.ts, src/types/providers.ts, src/shared/providers/registry.ts) — Hook, HookSource, CanonicalHookEvent, HooksIntegration, ProviderEventMapping. Each integrated provider declares its storage layout (standalone / inline-config), shape, name-tag support, and canonical→provider event map.
  • Per-shape serialisers (src/shared/providers/hook-handlers.ts) — buildHookEntry, upsertHookEntry, removeHookEntryAt translate between the canonical Hook model and the four wire formats. findIndex checks ensure surgical updates only touch the same capa:<id> tag.
  • Install pipeline — new prune-orphan-hooks and install-hooks tasks slot in after install-subagents (see updated diagram in docs/README.md). installHooksTask resolves sources, materialises scripts under ~/.capa, upserts entries into provider configs, and tracks each in the new managed_hooks SQLite table. pruneOrphanHooksTask removes entries no longer in the capabilities file. capa clean gains a cleanHooks step that uses the same DB table to strip every entry capa owns and delete materialised scripts.
  • Lockfile (src/types/lockfile.ts, src/shared/lockfile.ts) — new hooks: section pins github / gitlab / remote sources via bodySha256, resolvedRef, and friends. LockfileBuilder gains upsertHook / findHook, and pruneToIds accepts an optional hookIds set.
  • Validation (src/shared/hooks-validate.ts) — runtime checks for canonical-or-provider-scoped events, exclusive command / prompt / source, etc. Issues become warnings, not install failures.
  • Schema (src/shared/capabilities.ts) — hooks is now a known top-level key validated by Zod.
  • Web UI — REST endpoint surfaces declared hooks plus their installed-providers metadata (config path + script path); new HooksList panel renders them similarly to RulesList. i18n strings added under web-ui/src/locales/en/projects.json.
  • Database (src/db/schema.ts, src/db/managed-hooks.ts) — managed_hooks(project_id, provider_id, hook_id, config_path, locator, script_path) with surgical upsert/remove/clear.
  • Docs
    • Top-level README.md — new bullet about declarative hooks.
    • docs/README.md — install/clean Mermaid diagrams updated, new managed-hooks abstraction explained, prune-orphan-hooks / install-hooks rows added to the task table.
    • docs/providers/README.md — new "Hooks integration" section with the per-provider config/shape table.
    • docs/providers/{claude-code,cursor,codex,gemini-cli}.md — new Hooks row in the Capa-integration table, event-mapping table, upstream doc citations (Claude docs/claude-code/hooks, Cursor cursor.com/docs/agent/hooks, Codex openai/codex docs/config.md#hooks, Gemini google-gemini/gemini-cli docs/cli/configuration.md#hooks), Last verified: 2026-05-24.
    • skills/capabilities-manager/SKILL.md + references/capabilities-schema.md + references/commands.md — hooks section in the capabilities schema (events, source grammar, per-provider table) and a note on capa clean's hook cleanup.

Test plan

  • bun test — 963 pass, 0 fail (62 files), including new tests:
    • src/shared/__tests__/hooks-validate.test.ts
    • src/shared/providers/__tests__/hook-handlers.test.ts
    • src/cli/utils/__tests__/hooks-installer.test.ts (install / prune / clean, preserves user-authored entries, warnings, lockfile updates)
    • src/db/__tests__/database.test.ts — ManagedHooksRepo upsert / getAll / remove / clear
    • src/shared/__tests__/lockfile.test.ts — LockfileBuilder hooks upsert / prune
    • src/shared/providers/__tests__/registry.test.ts — hooks integration data for Claude Code, Cursor, Codex, Gemini CLI
  • bunx tsc --noEmit — clean
  • bunx biome lint <new files> — clean
  • bun run build:web — clean

Manual verification

  • capa install on a fixture project that declares one canonical-event hook (beforeShell) and one provider-scoped hook (cursor:beforeShellExecution) — confirm .claude/settings.json, .cursor/hooks.json, .codex/config.toml, .gemini/settings.json updates leave user-authored entries untouched.
  • Edit the user-authored entry between installs; confirm capa still updates only its own tagged entry.
  • Remove a hook from the YAML; re-run capa install; confirm the orphan entry and any materialised script under ~/.capa/hooks/<projectId>/ are removed.
  • Run capa clean; confirm all capa:* hook entries are stripped, the managed_hooks rows are cleared, and ~/.capa/hooks/<projectId>/ is removed while user entries remain.
  • Visit the project page in the local web UI and confirm the Hooks panel lists each declared hook plus the providers that received it.

Made with Cursor

Minitour and others added 2 commits May 23, 2026 22:47
Adds a top-level `hooks:` section to capabilities.yaml, a canonical
event model (sessionStart, beforeShell, afterFileEdit, …), and per-
provider integrations for the four agents that ship project-local hook
support today: Claude Code, Cursor, Codex, Gemini CLI. Other providers
warn-and-skip without failing install.

- New types: Hook, HookSource, CanonicalHookEvent, HooksIntegration,
  ProviderEventMapping. ProviderIntegration gains an optional `hooks`
  field; registry entries declare storage layout, shape, name-tag
  support, and the canonical→provider event map.
- New hook-handlers module translates canonical hooks to provider-
  specific entries (Claude/Gemini matcher groups, Cursor cursor-v1
  envelope, Codex TOML tables) and surgically upserts/removes only the
  entries it tagged with `name: capa:<id>` / `id: <hookId>`.
- New install tasks `prune-orphan-hooks` and `install-hooks` run after
  sub-agents and before credential setup; new `cleanHooks` step in
  `capa clean` strips capa entries and removes materialised scripts.
- Source-backed hook bodies materialise to
  `~/.capa/hooks/<projectId>/<hookId>` (out of the project tree).
- New SQLite `managed_hooks` table tracks (projectId, providerId,
  hookId, configPath, locator, scriptPath) for surgical edits.
- New lockfile `hooks:` section pins github/gitlab/remote sources.
- Server `/api/projects/:id` now returns each declared hook with its
  installed-providers metadata; web UI gains a HooksList panel and i18n
  strings to surface them.
- Schema-side validation via `validateHooks` warns instead of failing.
- Tests: hooks-validate, hook-handlers (per shape), hooks-installer
  (install/prune/clean), managed-hooks repo, registry hooks blocks,
  lockfile hook entries.
- Docs: top-level README bullet, docs/README install/clean diagrams +
  managed-hooks abstraction, providers/README hooks matrix +
  per-provider sections (claude-code, cursor, codex, gemini-cli) with
  upstream documentation citations, capabilities-manager skill +
  schema reference.

Closes #70.

Co-authored-by: Cursor <cursoragent@cursor.com>
Comment thread src/cli/utils/hooks-installer.ts Fixed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds first-class, declarative lifecycle hooks to capa (new hooks: section in capabilities.yaml) and wires installation/pruning/cleaning across supported providers, with lockfile pinning and web UI visibility.

Changes:

  • Introduces canonical hook types/events + provider registry hooks integrations and per-shape config serializers.
  • Adds install/prune/clean pipeline support (including managed_hooks DB tracking + lockfile entries for remote sources).
  • Surfaces hooks in the server API and web UI (types, UI list panel, i18n, docs).

Reviewed changes

Copilot reviewed 42 out of 42 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
web-ui/src/types/api.ts Adds hook types (Hook, InstalledHook) to the web UI API model.
web-ui/src/pages/ProjectDetailPage.tsx Treats hooks as a capability so the detail page renders the capabilities section when hooks exist.
web-ui/src/locales/en/projects.json Adds i18n strings for the Hooks panel and empty/search states.
web-ui/src/features/projects/components/HooksList.tsx New UI component to list hooks and per-provider installation metadata.
web-ui/src/features/projects/components/CapabilitiesSection.tsx Wires hooks into the capabilities UI section and search.
src/types/providers.ts Adds hooks?: HooksIntegration plus hook storage/event mapping types.
src/types/lockfile.ts Adds LockHookEntry and a top-level hooks array on the lockfile schema.
src/types/hooks.ts New core hook model: canonical events, sources, provider-scoped events.
src/types/capabilities.ts Exposes hooks in the public capabilities types.
src/shared/providers/registry.ts Declares hooks integrations for claude-code/cursor/codex/gemini-cli.
src/shared/providers/hook-handlers.ts New pure serializers/upsert/remove logic per provider “shape” + locator model.
src/shared/providers/tests/registry.test.ts Tests that v1 providers have hooks integration wired correctly.
src/shared/providers/tests/hook-handlers.test.ts Tests hook entry building/upsert/remove behaviors and name-tag helpers.
src/shared/lockfile.ts Adds hook validation/loading + LockfileBuilder support for hooks.
src/shared/hooks-validate.ts New runtime validation for capabilities hooks[] entries.
src/shared/config.ts Adds getHookScriptDir() for ~/.capa/hooks/<projectId>/ materialization.
src/shared/capabilities.ts Extends known top-level capabilities keys/schema to include hooks.
src/shared/tests/lockfile.test.ts Tests lockfile hook upsert/prune behavior.
src/shared/tests/hooks-validate.test.ts Tests hook validation (canonical/provider-scoped, duplicates, sources).
src/shared/tests/capabilities.test.ts Ensures hooks default to empty arrays in normalization and round-trip tests.
src/server/index.ts Adds hooks to the project capabilities API payload with installed entries from DB.
src/db/schema.ts Adds managed_hooks table for surgically managed provider hook entries.
src/db/managed-hooks.ts New repository for managed hook row CRUD.
src/db/database.ts Exposes managed-hooks operations on CapaDatabase.
src/db/tests/database.test.ts Tests managed-hooks DB operations via CapaDatabase.
src/cli/utils/hooks-installer.ts New install/prune/clean implementation: resolve sources, materialize scripts, edit provider configs, track locators.
src/cli/utils/tests/hooks-installer.test.ts Tests install behavior, preserving user entries, warnings, prune/clean workflows, lockfile recording.
src/cli/commands/install-tasks/write-lockfile.ts Extends lockfile pruning to include hook IDs with remote/repo sources.
src/cli/commands/install-tasks/prune-orphan-hooks.ts New install task to remove orphan hook entries prior to install.
src/cli/commands/install-tasks/install-hooks.ts New install task to validate and install hooks, recording warnings not failures.
src/cli/commands/install-tasks/index.ts Inserts prune/install hooks tasks into the install pipeline after subagents.
src/cli/commands/clean.ts Adds a Clean hooks step to remove capa-managed hook entries/scripts on capa clean.
skills/capabilities-manager/SKILL.md Documents hooks in the “capabilities-manager” skill.
skills/capabilities-manager/references/commands.md Updates capa clean docs to mention hook cleanup.
skills/capabilities-manager/references/capabilities-schema.md Documents the new hooks schema, event model, sources, and provider targets.
README.md Adds top-level bullet describing declarative hooks feature.
docs/README.md Updates install/clean pipeline diagrams + task table for hooks.
docs/providers/README.md Adds a dedicated hooks integration section and provider config table.
docs/providers/gemini-cli.md Documents Gemini hooks integration and event mapping (needs alignment with registry).
docs/providers/cursor.md Documents Cursor hooks integration and event mapping (needs alignment with registry).
docs/providers/codex.md Documents Codex hooks integration and event mapping (needs alignment with registry).
docs/providers/claude-code.md Documents Claude hooks integration and canonical-to-provider event mapping.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/shared/providers/hook-handlers.ts Outdated
Comment thread src/cli/utils/hooks-installer.ts
Comment thread src/shared/hooks-validate.ts
Comment thread src/cli/utils/hooks-installer.ts
Comment thread src/cli/utils/hooks-installer.ts Outdated
Comment thread src/server/index.ts
Comment thread docs/providers/codex.md Outdated
Comment thread docs/providers/cursor.md Outdated
Comment thread docs/providers/gemini-cli.md Outdated
Comment thread src/cli/commands/install-tasks/write-lockfile.ts
- Changed the shebang from `#!/bin/sh` to `#!/usr/bin/env bash` for better compatibility.
- Introduced a new variable `CAPA_MODIFIED_PROFILE` to track modifications to the shell profile.
- Added functions to check if the install directory is already in the user's shell profile and to detect the appropriate shell profile based on the user's shell.
- Improved the `add_to_path` function to ensure the installation directory is added to the user's PATH in a shell-appropriate manner.
- Updated the installation process to always attempt to add the directory to the user's shell profile, regardless of its current presence in the session's PATH.

Additionally, made minor adjustments to the capabilities schema and hook management to ensure consistency and clarity in the documentation and code structure.
Comment thread src/cli/utils/hooks-installer.ts Dismissed
Minitour and others added 4 commits May 24, 2026 21:38
Security:
- validateHooks() now rejects unsafe hook ids (path separators, '..',
  control chars) since ids are used both as filenames under
  ~/.capa/hooks/<projectId>/ and as provider tags. materialiseHookScript
  re-validates as defence in depth and rejects any path that escapes the
  hook script dir.

Correctness:
- resolveMatcher() now combines matcherPrefix with a user-supplied
  matcher (regex alternation) instead of dropping the canonical
  tool-family scope when the user adds their own matcher.
- pruneOrphanHooks() only deletes the managed_hooks DB row after the
  on-disk entry was removed, so a transient unlink failure doesn't
  leave an orphan in the provider config forever.
- readJsonFile() returns null on parse-error / wrong-shape and the
  installer refuses to overwrite, instead of silently clobbering a
  corrupted user config with {}.
- Server pre-fetches managed_hooks once and groups by hookId so
  /api/projects/:id stays O(n) for projects with many hooks.

Provider mapping accuracy:
- Cursor registry now exposes the full set of canonical→provider
  mappings (afterShell, afterMcpCall, sessionStart, sessionEnd,
  beforeTool, afterTool, afterToolFailure, subagentStart, subagentStop,
  preCompact) to match cursor.com/docs/agent/hooks.
- Gemini registry replaces the made-up PreToolUse/PostToolUse names
  with the real BeforeTool/AfterTool, drops the non-existent Stop
  mapping, and uses Gemini's actual built-in tool names
  (run_shell_command, read_file, write_file|replace|edit_file, mcp_*).
- Codex registry replaces the made-up PreShellExec/PostShellExec with
  Codex's real Claude-style PreToolUse/PostToolUse + tool-name matchers
  (Bash, apply_patch, mcp__).
- Provider docs (cursor.md, gemini-cli.md, codex.md) updated to match
  the registries; gemini docs now cite docs/hooks/reference.md, codex
  docs cite developers.openai.com/codex/hooks.

Lockfile:
- write-lockfile validates hooks before deriving hookIdsForLock so
  invalid hook entries can't keep stale lock pins alive past install.

Tests:
- New hooks-validate cases for path-traversal/unsafe ids and a
  matching positive case.
- New hook-handlers cases for matcherPrefix + user matcher union.
- New hooks-installer case that verifies pruneOrphanHooks keeps the
  DB row when on-disk removal fails (so the next run can retry).

Co-authored-by: Cursor <cursoragent@cursor.com>
For `source: { type: local, path: ... }`, the script already lives in
the project, is version-controlled, and the user is responsible for it.
Copying it to ~/.capa/hooks/<projectId>/<hookId> on every install was
wasteful: it duplicated the file, required re-running `capa install`
after every edit, and forced capa to chmod +x a file it didn't own.

Now capa resolves the relative path against the capabilities-file
directory and writes that absolute path verbatim into the provider
entry's `command` field. No copy under ~/.capa, no chmod, edits take
effect immediately, and `managed_hooks.scriptPath` stays NULL so
prune/clean never touches the user's file.

Inline / remote / github / gitlab sources still materialise to
~/.capa/hooks/<projectId>/<hookId> as before — they have nowhere else
to live.

Refactors `ResolvedHookBody.materialised` (which was misleading for
local sources, where the body did come from outside the YAML but is
already on disk) into the clearer `needsMaterialisation` flag, with an
optional `localPath` set only for `source.type=local`.

Updates docs/README.md, docs/providers/README.md, and the
capabilities-manager schema reference to spell out the new behavior.
Adds a hooks-installer test that asserts the user's script path lands
in the provider config and `scriptPath` stays NULL in `managed_hooks`.

Co-authored-by: Cursor <cursoragent@cursor.com>
The audit-shell example used \$TOOL_INPUT which is doubly misleading:

  1. \${TOOL_INPUT} (with braces) collides with capa's project variable
     resolver and prompts for a value at install time.
  2. No provider exports tool input as an env var — every supported
     provider hands the hook a JSON payload on stdin instead.

Replace the placeholder with a date-only one-liner, add an explicit
warning about \${...} vs shell vars, and show the real pattern: a
local-source script that reads stdin and parses the relevant field
with jq.

Co-authored-by: Cursor <cursoragent@cursor.com>
…de shape

Codex now shares the matcher-grouped layout with Claude, transitioning from a TOML table structure to a nested array of tables format. This change allows for a `name = "capa:<hookId>"` field to be appended for surgical updates, while Codex's deserializer ignores unknown fields, ensuring compatibility with user-authored entries. Documentation and tests have been updated to reflect these changes, including adjustments to the provider registry and hook handlers to support the new structure.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 43 out of 43 changed files in this pull request and generated 4 comments.

Comment thread install.sh Outdated
Comment thread src/cli/utils/hooks-installer.ts
Comment thread src/shared/providers/hook-handlers.ts
Comment thread docs/providers/README.md Outdated
- resolveMatcher now wraps each side in a non-capturing group so
  `(?:prefix)|(?:userMatcher)` matches the documented format and
  composes safely when either side already contains `|` (e.g. the
  registry's `Edit|MultiEdit|Write` prefix). Avoids capture-group
  number shifts that could collide with user backreferences.
- resolveHookBody returns `needsMaterialisation: false` for prompt-type
  hooks; only command bodies actually get written to
  `~/.capa/hooks/<projectId>/<hookId>`.
- install.sh `profile_has_dir` uses `grep -F` for the `$HOME` and
  `${HOME}` forms instead of embedding the raw path into a `grep -E`
  pattern; previously a relative path like `.local/bin` was treated as
  a regex (the `.` metacharacter) and caused false matches that
  skipped the PATH update.
- docs/providers/README.md no longer claims TOML providers use an
  `id: <hook-id>` tag; Codex uses the same `name = "capa:<id>"` field
  as the JSON Claude-style shapes.

Tests:
- hook-handlers: assert the new `(?:…)|(?:…)` matcher format
- full suite still green (970 pass / 0 fail)

Co-authored-by: Cursor <cursoragent@cursor.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 43 out of 43 changed files in this pull request and generated 2 comments.

Comment thread src/shared/providers/hook-handlers.ts
Comment thread src/cli/utils/hooks-installer.ts Outdated
Minitour and others added 3 commits May 29, 2026 03:01
Address Copilot review feedback on PR #72:

- buildCursorEntry() now honors hook.type. Cursor supports prompt-based,
  LLM-evaluated hooks (`{ type: "prompt", prompt: ... }`) in addition to
  command hooks; previously every Cursor entry serialised as a `command`,
  making prompt-type hooks unusable on that provider.
- resolveHookBody() reads the file contents for prompt-type `local`
  sources instead of returning empty text. Command-type local sources are
  still referenced in place (the provider entry points at the user's
  script verbatim); prompt-type local sources have no script to execute,
  so the file *is* the prompt text and is inlined like inline/remote
  sources. Previously prompt + local yielded an empty run reference and
  the hook was silently skipped.

Tests:
- hook-handlers: cursor prompt-entry branch + assert command entries omit
  a `type` discriminator.
- hooks-installer: prompt-type local source inlines the file contents and
  leaves managed_hooks.scriptPath null.

Docs: cursor.md notes command + prompt hook support.

Co-authored-by: Cursor <cursoragent@cursor.com>
…ication

- Introduced an optional `entryType` field in the MCP integration interface to specify the transport type for providers that require it.
- Updated the `buildMcpEntry` function to include the `type` in the returned entry when applicable.
- Modified the MCP client manager and registry to support the new `entryType` field, ensuring existing configurations are preserved.
- Enhanced tests to validate the correct handling of the `entryType` field across various providers.

This change improves the flexibility and clarity of MCP server configurations.
- Updated the test for MCP client registration to include the `type` field in the expected configuration for the `capa-infra-agent`.
- This change ensures that the test accurately reflects the recent enhancements made to the MCP integration regarding transport type specification.
@Minitour
Minitour merged commit d824921 into develop May 29, 2026
10 of 11 checks passed
@Minitour
Minitour deleted the feat/hooks-support branch May 29, 2026 01:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants