-
Notifications
You must be signed in to change notification settings - Fork 1
Description
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.jsonin VS Code extension settings — similar to Claude Code but different JSON schema - Windsurf:
.windsurfrulesfile — instruction-based, not programmatic hooks - Cline/Roo Code:
.clinerulesfile — 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:
-
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.
-
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.
-
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:
- Verified JSON schema for each agent's hook protocol
- Whether
updatedInputis actually honored by Claude Code (blocking for feat: add 'skim rewrite' subcommand for PreToolUse hook command transformation #43) - Whether
permissionDecisioncan be omitted (security requirement from feat: add 'skim init' for one-command Claude Code hook installation #44) - MCP support status for each agent
- 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
updatedInputbehavior verified on current Claude Code version- Recommendation for
skim init --agent <name>flag values