Skip to content

feat: first-class agent-native runtime hooks for integrations#3704

Open
kanfil wants to merge 1 commit into
github:mainfrom
tikalk:feat/agent-runtime-hooks
Open

feat: first-class agent-native runtime hooks for integrations#3704
kanfil wants to merge 1 commit into
github:mainfrom
tikalk:feat/agent-runtime-hooks

Conversation

@kanfil

@kanfil kanfil commented Jul 23, 2026

Copy link
Copy Markdown

🎯 Problem & Motivation

While Spec Kit excels at scaffolding commands and prompt templates during specify init, there was previously no first-class mechanism for event-driven hook execution natively tied to AI coding agents in upstream.

This PR introduces an agent-native runtime hooks layer that bridges Spec Kit directly to agent lifecycle events (like SessionStart, PreToolUse, PostToolUse, Stop, etc.).


✨ Key Features Added

  1. The Hook Bridge (.specify/hooks/bridge.py):
    A lightweight, zero-dependency Python script scaffolded in projects during specify init. It maps registered Spec Kit commands to their underlying scripts and executes them on demand, passing any payload through stdin.

  2. Agent Hook Adapters:
    Individual, target-aware adapters for multiple agent environments that generate/merge native hook configurations seamlessly during setup() and teardown():

    • Claude Code: .claude/settings.json (nested JSON structure)
    • Cursor: .cursor/hooks.json (flat, camelCase structure)
    • Codex: .codex/config.toml (TOML format)
    • opencode: .opencode/plugin/speckit-hooks.ts + opencode.json (TypeScript plugin)
    • Gemini, Qwen, Devin, Tabnine: Native settings merge
  3. Layered Hook Resolution:
    Hooks are resolved cleanly using a 4-tier priority stack:

    • Layer 1 (CLI): Passing --hooks false during init disables hooks entirely.
    • Layer 2 (User Override): Users can create .specify/integration-hooks.yml to override hook behaviors.
    • Layer 3 (Extension-Declared): Extensions can declare runtime_hooks: in their extension.yml (completely decoupling core code from extension details!).
    • Layer 4 (Built-in Defaults): Native integration class defaults.

🛠️ Example Usage

An extension can declare runtime hooks in extension.yml:

extension:
  id: "team-ai-directives"
  name: "Team AI Directives"
  version: "1.0.0"

runtime_hooks:
  SessionStart:
    command: "speckit.team-directives.boot"
    description: "Load team rules and constitution on session startup"

When the user runs specify init --integration claude, the Claude adapter automatically detects this, generates the Hook Bridge, and injects the hook into .claude/settings.json. When Claude Code launches, it natively triggers the bridge, booting the team context seamlessly.


🧪 Verification & Dependencies

  • Dependencies: Uses pyyaml (already a spec-kit dependency) to parse extension-declared hooks. No new external dependencies.
  • Teardown: Run specify integration uninstall to cleanly strip all native hook config lines/plugins, leaving the user's files exactly as they were.
  • Unit Tests: Includes a suite of 56 passing unit tests verifying layered hook resolution, override parsing, extension collection, and adapter configurations.

@kanfil
kanfil requested a review from mnriem as a code owner July 23, 2026 22:15
@mnriem

mnriem commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Thanks for this — the agent-runtime wiring is a real gap and the canonical→native event mapping, marker-based surgical teardown, and layered resolution are the right instincts. Sharing some architectural feedback before this goes further; happy to pair on any of it.

1. Call the feature "events", and use snake_case names consistent with the existing event vocabulary. hooks is already load-bearing in Spec Kit: extensions declare hooks: in extension.yml, there's a documented Hook System + HookExecutor, and those fire on SDD-workflow points (before_specify, after_tasks) as soft/in-prompt prompts. This PR fires on agent-runtime points (SessionStart, PreToolUse) wired into native agent config — genuinely different, and reusing "hooks" will confuse users and docs. Suggest:

  • extension field events: (not runtime_hooks),
  • CLI flag --events,
  • canonical event names in the same xx_yyy style as the existing workflow events: session_start, pre_tool_use, post_tool_use, session_end, user_prompt_submit, stop,
  • dispatcher events.py, marker __speckit_event__.

Adapters translate those canonical snake_case names to each agent's native casing (Claude SessionStart, Copilot/Cursor sessionStart, Gemini BeforeTool, …), so extension authors never learn agent-specific names.

2. Drive adapters from the integration registry, not a parallel _ADAPTERS dict. AGENTS.md makes the integration registry the single source of truth; the hardcoded {"claude": ..., "cursor-agent": ...} map means new agents must be registered twice and keys can drift. Fold each adapter into its integration class as CANONICAL_TO_NATIVE + events_config_file + emit_events()/remove_events() the registry drives — the same pattern Copilot already uses to own .vscode/settings.json. This also collapses the 4 duplicated insertions in base.py into one path.

3. Split sourcing from writing so integrations/ never imports extensions/. Right now _hooks.py both writes native config and sources declarations by scanning .specify/extensions/*/extension.yml (from ..extensions import ValidationError). Those are two responsibilities:

  • Writing native config is legitimately integration-domain (like Copilot's settings.json) → keep it in the integration class.
  • Sourcing the event→command map (built-in defaults + extension events: + user override) should move out: have the init orchestration resolve it and inject it via setup(..., events=map). The integration owns the write but doesn't reach up into the extension system.

4. The dispatcher is agent-agnostic → make it core. One bridge.py serves every agent and re-implements command→script resolution that already lives in _commands.py/the registrar. Lift it to a core .specify/events.py that reuses the existing resolver instead of duplicating it.

5. Two capabilities the PR description advertises aren't actually wired yet — make sure the reworked versions are real and covered by tests. As filed: the disable flag reads parsed_options.get("hooks"), but no integration declares that option via options() and there's no top-level flag, so it can't be set the way the description implies; and validate_runtime_hooks() documents itself as "called from ExtensionManifest._validate()", but the manifest isn't touched in this PR (only 3 files changed), so malformed declarations are never validated at load. After the rework that means: --events should be a real, declared gate, and event validation should actually be invoked from the manifest validator — both with end-to-end tests.

6. Missing Copilot CLI adapter. Copilot CLI has first-class hooks (docs): native events (sessionStart, preToolUse, postToolUse, sessionEnd, userPromptSubmitted), per-entry bash/powershell variants, and a dedicated .github/hooks/*.json location. Writing our own .github/hooks/speckit.json (rather than merging a shared settings file) makes teardown a plain file-delete with no marker scanning, and the bash/powershell split maps cleanly onto Spec Kit's existing scripts: {sh, ps} frontmatter. Note Copilot also reads cross-tool .claude/settings.json, so a project with both integrations needs a de-dupe guard in the resolver.

Smaller correctness notes: the Codex adapter embeds $(git rev-parse --show-toplevel) inside a TOML command string (relies on the agent shell-expanding it); several native event names (Cursor beforeSubmitPrompt, Gemini BeforeTool/AfterAgent, Tabnine) read as best-guesses worth verifying per-agent or marking experimental; and silent writes into .claude/settings.json etc. during init should be surfaced in output.

Net: the writing mechanism belongs in the integration layer as you have it — the main structural change is splitting the one module in two: native-config writing stays per-integration (registry-driven), while event sourcing moves to an injected core resolver, with the dispatcher lifted to core. Add the terminology change and the Copilot adapter and I think this is in great shape. Glad to help implement or review.


Posted on behalf of @mnriem by GitHub Copilot (model: Claude Opus 4.8).

@mnriem mnriem left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please address my feedback above. Great proposal!

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.

2 participants