Repository navigation
feat(hooks): declarative provider lifecycle hooks - #72
Merged
Merged
Conversation
release: v1.9.1
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>
Contributor
There was a problem hiding this comment.
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
hooksintegrations and per-shape config serializers. - Adds install/prune/clean pipeline support (including
managed_hooksDB 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.
- 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.
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.
- 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>
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.
4 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #70.
Summary
Adds a top-level
hooks:section tocapabilities.yamlplus 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 withname: capa:<id>(orid: <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)
.claude/settings.json→hooksname: capa:<id>.cursor/hooks.json(standalone)name: capa:<id>.codex/config.toml→[hooks]id: <hookId>.gemini/settings.json→hooksname: 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
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.src/shared/providers/hook-handlers.ts) —buildHookEntry,upsertHookEntry,removeHookEntryAttranslate between the canonical Hook model and the four wire formats.findIndexchecks ensure surgical updates only touch the samecapa:<id>tag.prune-orphan-hooksandinstall-hookstasks slot in afterinstall-subagents(see updated diagram indocs/README.md).installHooksTaskresolves sources, materialises scripts under~/.capa, upserts entries into provider configs, and tracks each in the newmanaged_hooksSQLite table.pruneOrphanHooksTaskremoves entries no longer in the capabilities file.capa cleangains acleanHooksstep that uses the same DB table to strip every entry capa owns and delete materialised scripts.src/types/lockfile.ts,src/shared/lockfile.ts) — newhooks:section pinsgithub/gitlab/remotesources viabodySha256,resolvedRef, and friends.LockfileBuildergainsupsertHook/findHook, andpruneToIdsaccepts an optionalhookIdsset.src/shared/hooks-validate.ts) — runtime checks for canonical-or-provider-scoped events, exclusivecommand/prompt/source, etc. Issues become warnings, not install failures.src/shared/capabilities.ts) —hooksis now a known top-level key validated by Zod.HooksListpanel renders them similarly toRulesList. i18n strings added underweb-ui/src/locales/en/projects.json.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.README.md— new bullet about declarative hooks.docs/README.md— install/clean Mermaid diagrams updated, newmanaged-hooksabstraction explained,prune-orphan-hooks/install-hooksrows 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 (Claudedocs/claude-code/hooks, Cursorcursor.com/docs/agent/hooks, Codexopenai/codex docs/config.md#hooks, Geminigoogle-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 oncapa clean's hook cleanup.Test plan
bun test— 963 pass, 0 fail (62 files), including new tests:src/shared/__tests__/hooks-validate.test.tssrc/shared/providers/__tests__/hook-handlers.test.tssrc/cli/utils/__tests__/hooks-installer.test.ts(install / prune / clean, preserves user-authored entries, warnings, lockfile updates)src/db/__tests__/database.test.ts—ManagedHooksRepoupsert/getAll/remove/clearsrc/shared/__tests__/lockfile.test.ts—LockfileBuilderhooks upsert / prunesrc/shared/providers/__tests__/registry.test.ts— hooks integration data for Claude Code, Cursor, Codex, Gemini CLIbunx tsc --noEmit— cleanbunx biome lint <new files>— cleanbun run build:web— cleanManual verification
capa installon 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.jsonupdates leave user-authored entries untouched.capa install; confirm the orphan entry and any materialised script under~/.capa/hooks/<projectId>/are removed.capa clean; confirm allcapa:*hook entries are stripped, themanaged_hooksrows are cleared, and~/.capa/hooks/<projectId>/is removed while user entries remain.Made with Cursor