Skip to content

research: investigate multi-agent hook support (Cursor, Windsurf, Cline, Codex) #58

@dean0x

Description

@dean0x

Research: Multi-Agent Hook Support

Parent: #19 (Phase B)
Category: B5 — Polish + Deferred

Context

GRANITE shipped support for 9 AI coding agents in a single 48-hour sprint (PR #704). Their approach: separate hook scripts per agent, unified rewrite_command() engine in Rust. This broadens their addressable market significantly.

However, their hook-per-agent approach has inherent fragility — each agent uses a different JSON protocol, different field naming, different permission models. This is where our MCP server approach (#60) provides a simpler alternative for MCP-compatible agents.

This research ticket catalogs each agent's integration mechanism to inform both our hook implementation (#43, #44) and our MCP server (#60).

Current Agent Landscape (March 2026)

Agent Users (est.) Hook Mechanism MCP Support Priority
Claude Code ~500K PreToolUse bash hook Yes (native) P0 — Ship first
Cursor ~2M hooks.json (VS Code extension API) Yes P1
GitHub Copilot CLI ~1M Custom hook format Partial P1
Google Gemini CLI ~200K BeforeTool hook (native Rust) Unknown P2
OpenAI Codex CLI ~300K AGENTS.md instruction file Unknown P2
Windsurf ~500K .windsurfrules instruction file Yes P2
Cline/Roo Code ~400K .clinerules instruction file Yes P2
OpenCode ~50K TypeScript plugin Unknown P3
OpenClaw ~10K Plugin marketplace Unknown P3

Known Hook JSON Formats

Claude Code (PreToolUse) — P0

// Input:
{"tool_name": "Bash", "tool_input": {"command": "git status"}}
// Output:
{"hookSpecificOutput": {"hookEventName": "PreToolUse", "updatedInput": {"command": "skim git status"}}}

Critical: GRANITE issue #506 discovered Claude Code silently ignores updatedInput in some cases. GRANITE works around this by having the hook script execute the rewritten command directly and return the result. Before implementing our hook, we MUST verify current updatedInput behavior with a minimal test hook.

Security: Our hook must NEVER set permissionDecision. See #44 for details.

Copilot CLI

// Input (note camelCase + JSON string):
{"toolName": "bash", "toolArgs": "{\"command\": \"git status\"}"}
// Output (denial with suggestion):
{"permissionDecision": "deny", "permissionDecisionReason": "use skim git status instead"}

Note: Copilot uses a denial + suggestion pattern rather than input rewriting. The agent decides whether to follow the suggestion.

Gemini CLI

// Input:
{"tool_name": "run_shell_command", "tool_input": {"command": "git status"}}
// Output:
{"decision": "allow", "hookSpecificOutput": {"tool_input": {"command": "skim git status"}}}

Cursor / Windsurf / Cline / OpenCode

These agents use different integration patterns:

  • Cursor: hooks.json in VS Code extension settings — similar to Claude Code but different JSON schema
  • Windsurf: .windsurfrules file — instruction-based, not programmatic hooks
  • Cline/Roo Code: .clinerules file — instruction-based
  • OpenCode: TypeScript plugin system — programmatic but different runtime

For instruction-based agents (Windsurf, Cline, Codex), the integration is NOT a hook but an instruction that says "use skim instead of cat for reading code files." This is fundamentally simpler but less reliable.

Key Differences to Account For

Dimension Claude Code Copilot Gemini Instruction-based
Field naming snake_case camelCase snake_case N/A
Input nesting tool_input.command toolArgs (JSON string) tool_input.command N/A
Permission model permissionDecision (optional) permissionDecision (required) decision N/A
Rewrite method updatedInput deny + suggest hookSpecificOutput instruction
Hook language bash/shell JSON config Rust (native) markdown
MCP alternative Yes Yes Unknown Yes (Cursor, Windsurf, Cline)

Strategic Recommendation

Two-track approach:

  1. Hook track (agents with programmatic hooks): Claude Code → Copilot → Gemini. One hook script with agent auto-detection based on JSON field names. See feat: add 'skim rewrite' subcommand for PreToolUse hook command transformation #43, feat: add 'skim init' for one-command Claude Code hook installation #44.

  2. MCP track (agents with MCP support): Claude Code, Cursor, Windsurf, Cline. Single MCP server implementation serves all. See feat: add MCP server mode for native agent integration #60.

  3. Instruction track (agents with instruction files only): Codex (AGENTS.md), Windsurf (.windsurfrules), Cline (.clinerules). skim init --agent <name> generates the appropriate instruction file.

The MCP track is the long-term winner — it requires zero per-agent maintenance and works with any future MCP-compatible agent.

Deliverable

Research document with:

  1. Verified JSON schema for each agent's hook protocol
  2. Whether updatedInput is actually honored by Claude Code (blocking for feat: add 'skim rewrite' subcommand for PreToolUse hook command transformation #43)
  3. Whether permissionDecision can be omitted (security requirement from feat: add 'skim init' for one-command Claude Code hook installation #44)
  4. MCP support status for each agent
  5. Recommendation: which agents get hooks, which get MCP, which get instructions

Acceptance Criteria

  • Research document committed to .docs/research/multi-agent-hooks.md
  • Each agent's hook protocol tested with a minimal proof-of-concept
  • updatedInput behavior verified on current Claude Code version
  • Recommendation for skim init --agent <name> flag values

Size: S

Depends on: B4 (after core Phase B is shipped)

Blocks: nothing

Metadata

Metadata

Assignees

No one assigned

    Labels

    phase-bPhase B: Command Output OptimizationpolishPolish & deferred work

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions