Hooks
CAPA can install and manage provider lifecycle hooks straight from capabilities.yaml. Hooks are declared once against a canonical event model and translated to each provider's native shape (Cursor's hooks.json, Claude Code's hooks, Codex hooks, etc.).
Configuration
Hooks are declared as a top-level hooks: array in the capabilities file:
hooks:
- id: audit-shell
description: Log shell commands to audit trail
on: beforeShell
command: 'echo "$(date) $TOOL_INPUT" >> ~/.capa/audit.log'
timeout: 5
- id: lint-staged
on: afterFileEdit
source:
type: github
def:
repo: acme/dev-toolkit::hooks/lint-staged.sh:v2.0.0 {
"hooks": [
{
"id": "audit-shell",
"description": "Log shell commands to audit trail",
"on": "beforeShell",
"command": "echo \"$(date) $TOOL_INPUT\" >> ~/.capa/audit.log",
"timeout": 5
},
{
"id": "lint-staged",
"on": "afterFileEdit",
"source": {
"type": "github",
"def": {
"repo": "acme/dev-toolkit::hooks/lint-staged.sh:v2.0.0"
}
}
}
]
} Canonical Events
CAPA defines 16 canonical hook events. These are mapped to each provider's native event system automatically:
| Event | Description |
|---|---|
sessionStart | Fires when a new agent session begins |
sessionEnd | Fires when an agent session ends |
userPromptSubmit | Fires when the user submits a prompt |
beforeTool | Fires before any tool invocation |
afterTool | Fires after a successful tool invocation |
afterToolFailure | Fires after a failed tool invocation |
beforeShell | Fires before a shell command executes |
afterShell | Fires after a shell command completes |
beforeFileRead | Fires before a file is read |
afterFileEdit | Fires after a file is edited |
beforeMcpCall | Fires before an MCP tool call |
afterMcpCall | Fires after an MCP tool call completes |
subagentStart | Fires when a sub-agent is spawned |
subagentStop | Fires when a sub-agent terminates |
preCompact | Fires before context compaction |
stop | Fires when the agent stops |
Note: not all providers support all events. CAPA warns (but never fails) if a provider doesn't support a requested event.
Hook Fields
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Stable identifier for tracking and lockfile |
description | string | No | Human-readable description |
on | string | Yes | Canonical event name or provider-scoped event (e.g. cursor:beforeShellExecution) |
type | string | No | command or prompt (default: command) |
command | string | Conditional | Shell command to run (for command hooks) |
prompt | string | Conditional | Prompt text (for prompt hooks, supported by Cursor) |
matcher | string | No | Provider-specific tool/glob filter |
timeout | number | No | Timeout in seconds |
failClosed | boolean | No | If true, hook failure blocks the operation |
sequential | boolean | No | If true, hooks run sequentially rather than in parallel |
providers | string[] | No | Restrict which providers receive this hook |
source | object | No | Alternate body source (see Source Types below) |
Source Types
Hook bodies can come from five source types:
inline (default)
The command or prompt is specified directly in the capabilities file.
remote
Fetched from a URL at install time. The body is materialized under ~/.capa/hooks/.
github
Fetched from a GitHub repository. Uses repo::path:version syntax. Materialized under ~/.capa/hooks/ and pinned by SHA-256 in capabilities.lock.
gitlab
Same as github but for GitLab repos.
local
References a file path on disk. The script is read in-place (never copied), so capa clean does not remove it.
hooks:
# inline (default) — command specified directly
- id: notify-start
on: sessionStart
command: 'echo "Session started at $(date)"'
# remote — fetched from URL at install time
- id: security-scan
on: beforeShell
source:
type: remote
def:
url: https://example.com/hooks/security-scan.sh
# github — fetched from GitHub repo
- id: lint-staged
on: afterFileEdit
source:
type: github
def:
repo: acme/dev-toolkit::hooks/lint-staged.sh:v2.0.0
# gitlab — fetched from GitLab repo
- id: deploy-check
on: beforeShell
source:
type: gitlab
def:
repo: team/infra::hooks/deploy-check.sh:main
# local — references a file on disk (never copied)
- id: custom-validator
on: afterFileEdit
source:
type: local
def:
path: ./scripts/validate.sh {
"hooks": [
{
"id": "notify-start",
"on": "sessionStart",
"command": "echo \"Session started at $(date)\""
},
{
"id": "security-scan",
"on": "beforeShell",
"source": {
"type": "remote",
"def": {
"url": "https://example.com/hooks/security-scan.sh"
}
}
},
{
"id": "lint-staged",
"on": "afterFileEdit",
"source": {
"type": "github",
"def": {
"repo": "acme/dev-toolkit::hooks/lint-staged.sh:v2.0.0"
}
}
},
{
"id": "deploy-check",
"on": "beforeShell",
"source": {
"type": "gitlab",
"def": {
"repo": "team/infra::hooks/deploy-check.sh:main"
}
}
},
{
"id": "custom-validator",
"on": "afterFileEdit",
"source": {
"type": "local",
"def": {
"path": "./scripts/validate.sh"
}
}
}
]
} Lockfile Pinning
For source-backed hooks (github, gitlab, remote), capabilities.lock stores:
- bodySha256: SHA-256 hash of the hook body
- resolvedRef: The resolved git ref
This ensures reproducible installs across machines.
# capabilities.lock (excerpt)
hooks:
- id: lint-staged
bodySha256: a1b2c3d4e5f6...
resolvedRef: abc123def456... Managed Hooks
CAPA only manages hooks it creates — tracked in the managed_hooks database table. User-authored hook entries in provider config files are never touched during install, prune, or clean operations.
Provider Support
Initially supported providers: Claude Code, Cursor, Codex, Gemini CLI. Each provider's hooks are serialized to its native format:
- Cursor:
.cursor/hooks.json - Claude Code: matcher-group arrays in the Claude Code hooks configuration
- Codex: Codex native hook format
- Gemini CLI: Gemini CLI native hook format
Prompt-type hooks are supported by Cursor and local sources.
Related Documentation
- Capabilities File: the full capabilities file reference.
- Lockfile: reproducible installs via locked commit SHAs.
- capa install: the install command that processes hooks.
- capa clean: removing installed hooks and other artifacts.