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