Documentation
¶
Overview ¶
Package hookshot provides a framework for building hooks for AI coding agents like Cursor and Claude Code.
Quick Start ¶
Use the unified handlers to write cross-platform hooks:
package main
import "github.com/CorridorSecurity/hookshot"
func main() {
hookshot.OnStop(func(ctx hookshot.StopContext) hookshot.StopDecision {
if ctx.ShouldSkip() {
return hookshot.AllowStop()
}
return hookshot.PreventStop("Please verify changes")
})
hookshot.RunCommand()
}
Platform-Specific Handlers ¶
For platform-specific features, use Register with Run:
func main() {
// Unified handlers
hookshot.OnStop(handleStop)
// Platform-specific: Cursor Tab completion (no Claude equivalent)
hookshot.Register("cursor-before-tab-read", func() {
hookshot.Run(func(input cursor.BeforeTabFileReadInput) cursor.BeforeTabFileReadOutput {
return cursor.AllowTabRead()
})
})
hookshot.RunCommand()
}
Run with: ./my-hooks claude-stop or ./my-hooks cursor-before-tab-read
Unified API ¶
For cross-platform handlers, use the unified API which works across both Claude Code and Cursor with a single handler:
hookshot.OnStop(func(ctx hookshot.StopContext) hookshot.StopDecision {
if ctx.ShouldSkip() {
return hookshot.AllowStop()
}
return hookshot.PreventStop("Please verify changes")
})
hookshot.OnBeforeExecution(func(ctx hookshot.ExecutionContext) hookshot.ExecutionDecision {
if ctx.Type == hookshot.ExecutionShell && strings.Contains(ctx.Command, "rm -rf") {
return hookshot.DenyExecution("Dangerous command")
}
return hookshot.AllowExecution()
})
Unified handlers:
- OnStop: Stop hooks for both platforms
- OnBeforeExecution: Shell/MCP execution for both platforms
- OnAfterFileEdit: File edit hooks for both platforms
- OnPromptSubmit: Prompt submission for both platforms
- [OnSessionStart]: Session start (Claude Code only)
Error Handling ¶
Use RunE when your handler can fail:
hookshot.RunE(func(input claude.PreToolUseInput) (claude.PreToolUseOutput, error) {
if err := validate(input); err != nil {
return claude.PreToolUseOutput{}, err // Exits with code 2
}
return claude.Allow("Validated"), nil
})
Building and Installing ¶
Use the hookshot CLI for building and installing:
go install github.com/CorridorSecurity/hookshot/cmd/hookshot@latest hookshot build -all -output ./dist hookshot install --binary ./dist/darwin-arm64/my-hooks
Configuration ¶
Configure hooks in Claude Code (~/.claude/settings.json):
{
"hooks": {
"Stop": [{
"hooks": [{ "type": "command", "command": "/path/to/my-hooks claude-stop" }]
}]
}
}
Configure hooks in Cursor (~/.cursor/hooks.json):
{
"version": 1,
"hooks": {
"stop": [{ "command": "/path/to/my-hooks cursor-stop" }]
}
}
Packages ¶
The hookshot module consists of:
- hookshot (this package): Core Run/Register/RunCommand functions
- hookshot/claude: Types and helpers for Claude Code hooks
- hookshot/cursor: Types and helpers for Cursor hooks
- hookshot/build: Cross-platform build tool
- hookshot/internal: Internal JSON I/O (not for external use)
See the sub-packages for platform-specific documentation.
Package hookshot provides a framework for building hooks for AI coding agents like Cursor and Claude Code.
Hookshot handles the boilerplate of reading JSON from stdin and writing JSON to stdout, letting you focus on your hook logic. All hooks must use the multi-hook pattern with Register/RunCommand.
Basic usage with unified handlers:
package main
import "github.com/CorridorSecurity/hookshot"
func main() {
hookshot.OnStop(func(ctx hookshot.StopContext) hookshot.StopDecision {
if ctx.ShouldSkip() {
return hookshot.AllowStop()
}
return hookshot.PreventStop("Please verify changes")
})
hookshot.RunCommand()
}
Platform-specific handlers:
hookshot.Register("cursor-before-tab-read", func() {
hookshot.Run(func(input cursor.BeforeTabFileReadInput) cursor.BeforeTabFileReadOutput {
return cursor.AllowTabRead()
})
})
Index ¶
- func ClearHandlers()
- func MustRegister(name string, handler Handler)
- func OnAfterFileEdit(handler FileEditHandler)
- func OnBeforeExecution(handler ExecutionHandler)
- func OnPromptSubmit(handler PromptHandler)
- func OnStop(handler StopHandler)
- func ReadRawInput(v any) error
- func Register(name string, handler Handler)
- func Run[I any, O any](handler func(I) O)
- func RunCommand()
- func RunE[I any, O any](handler func(I) (O, error))
- type ExecutionContext
- type ExecutionDecision
- type ExecutionHandler
- type ExecutionType
- type FileEdit
- type FileEditContext
- type FileEditDecision
- type FileEditHandler
- type Handler
- type Platform
- type PromptContext
- type PromptDecision
- type PromptHandler
- type StopContext
- type StopDecision
- type StopHandler
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func ClearHandlers ¶
func ClearHandlers()
ClearHandlers removes all registered handlers. Useful for testing.
func MustRegister ¶
MustRegister is like Register but panics if a handler with the same name already exists.
func OnAfterFileEdit ¶
func OnAfterFileEdit(handler FileEditHandler)
OnAfterFileEdit registers a unified handler for post-file-edit events. It automatically registers handlers for:
- "claude-after-file-edit" (PostToolUse for Write/Edit)
- "cursor-after-file-edit"
- "droid-after-file-edit" (PostToolUse for Write/Edit)
- "cascade-post-write-code"
func OnBeforeExecution ¶
func OnBeforeExecution(handler ExecutionHandler)
OnBeforeExecution registers a unified handler for pre-execution events. It automatically registers handlers for:
- "claude-pre-tool-use" (filters to Bash and mcp__* tools)
- "cursor-before-shell"
- "cursor-before-mcp"
- "droid-pre-tool-use" (filters to Bash and mcp__* tools)
- "cascade-pre-run-command"
- "cascade-pre-mcp-tool-use"
func OnPromptSubmit ¶
func OnPromptSubmit(handler PromptHandler)
OnPromptSubmit registers a unified handler for prompt submission events. It automatically registers handlers for:
- "claude-user-prompt-submit"
- "cursor-before-submit-prompt"
- "droid-user-prompt-submit"
- "cascade-pre-user-prompt"
func OnStop ¶
func OnStop(handler StopHandler)
OnStop registers a unified handler for stop events on all platforms. It automatically registers handlers for "claude-stop", "cursor-stop", "droid-stop", and "cascade-post-cascade-response".
func ReadRawInput ¶
ReadRawInput reads the raw JSON input from stdin into the provided struct. Use this when you need access to platform-specific fields not exposed in the unified contexts.
func Register ¶
Register adds a named handler for use with RunCommand. Use this to build a single binary that handles multiple hook types.
Example:
func main() {
hookshot.Register("claude-stop", handleClaudeStop)
hookshot.Register("cursor-stop", handleCursorStop)
hookshot.RunCommand()
}
func Run ¶
Run executes a hook handler function. It reads JSON input from stdin, passes it to the handler, and writes the output to stdout. If stdin cannot be read or contains invalid JSON, the program exits with code 0 (graceful failure - hooks should not block on parse errors).
func RunCommand ¶
func RunCommand()
RunCommand executes the handler matching the command name. The command is determined from os.Args[1] if provided, otherwise from the binary name. If no matching handler is found, usage information is printed and the program exits with code 1.
Types ¶
type ExecutionContext ¶
type ExecutionContext struct {
Platform Platform
Type ExecutionType
// For shell execution (Cursor beforeShellExecution, Claude Code Bash tool)
// Also used for local MCP servers on Cursor (command-based MCP servers)
// NOTE: Only populated for Cursor and Cascade, not Claude Code or Droid
Command string
Cwd string // Working directory
// For MCP execution
ToolName string // MCP tool name (e.g., "mcp__server__tool")
ToolInput json.RawMessage // Tool input parameters as JSON
ServerURL string // MCP server URL (Cursor/Cascade only, for URL-based servers)
// Raw input for advanced use cases
RawClaudeCode *claude.PreToolUseInput
RawCursor any // *cursor.BeforeShellExecutionInput or *cursor.BeforeMCPExecutionInput
RawDroid *droid.PreToolUseInput
RawCascade any // *cascade.PreRunCommandInput or *cascade.PreMCPToolUseInput
}
ExecutionContext provides a unified view of pre-execution events.
func (ExecutionContext) IsMCP ¶
func (c ExecutionContext) IsMCP() bool
IsMCP returns true if this is an MCP tool execution.
type ExecutionDecision ¶
type ExecutionDecision struct {
// Allow determines whether execution should proceed.
Allow bool
// Reason explains the decision.
// For Allow=true, shown to user (if not empty).
// For Allow=false, shown to agent.
Reason string
// Ask prompts the user to confirm (only if Allow is false and Ask is true).
Ask bool
}
ExecutionDecision represents the unified decision for execution hooks.
func AllowExecution ¶
func AllowExecution() ExecutionDecision
AllowExecution returns a decision that permits execution.
func AllowExecutionWithReason ¶
func AllowExecutionWithReason(reason string) ExecutionDecision
AllowExecutionWithReason permits execution with a reason shown to the user.
func AskExecution ¶
func AskExecution(reason string) ExecutionDecision
AskExecution prompts the user to confirm execution.
func DenyExecution ¶
func DenyExecution(reason string) ExecutionDecision
DenyExecution blocks execution with a reason shown to the agent.
type ExecutionHandler ¶
type ExecutionHandler func(ExecutionContext) ExecutionDecision
ExecutionHandler is the function signature for unified execution handlers.
type ExecutionType ¶
type ExecutionType string
ExecutionType identifies what kind of execution is being attempted.
const ( ExecutionShell ExecutionType = "shell" ExecutionMCP ExecutionType = "mcp" ExecutionTool ExecutionType = "tool" // Claude Code non-MCP tools (Read, Write, etc.) )
type FileEditContext ¶
type FileEditContext struct {
Platform Platform
SessionID string // Claude Code: session_id, Cursor: conversation_id, Cascade: trajectory_id
FilePath string
Edits []FileEdit
Cwd string
// Raw input for advanced use cases
RawClaudeCode *claude.PostToolUseInput
RawCursor *cursor.AfterFileEditInput
RawDroid *droid.PostToolUseInput
RawCascade *cascade.PostWriteCodeInput
}
FileEditContext provides a unified view of file edit events.
type FileEditDecision ¶
type FileEditDecision struct {
// Block sends feedback to the agent (Claude Code only).
Block bool
// Reason is shown to the agent when Block is true.
Reason string
// Context is additional context added for the agent (Claude Code only).
Context string
}
FileEditDecision represents the unified decision for file edit hooks.
func FileEditAddContext ¶
func FileEditAddContext(context string) FileEditDecision
FileEditAddContext adds context for the agent to consider.
func FileEditBlock ¶
func FileEditBlock(reason string) FileEditDecision
FileEditBlock sends feedback to the agent about the edit.
func FileEditOK ¶
func FileEditOK() FileEditDecision
FileEditOK returns a decision that allows normal flow.
type FileEditHandler ¶
type FileEditHandler func(FileEditContext) FileEditDecision
FileEditHandler is the function signature for unified file edit handlers.
type Handler ¶
type Handler func()
Handler is a function that can be registered for multi-hook binaries.
type PromptContext ¶
type PromptContext struct {
Platform Platform
SessionID string // Claude Code: session_id, Cursor: conversation_id, Cascade: trajectory_id
Prompt string
// Raw input for advanced use cases
RawClaudeCode *claude.UserPromptSubmitInput
RawCursor *cursor.BeforeSubmitPromptInput
RawDroid *droid.UserPromptSubmitInput
RawCascade *cascade.PreUserPromptInput
}
PromptContext provides a unified view of prompt submission events.
type PromptDecision ¶
type PromptDecision struct {
// Allow determines whether the prompt should be processed.
Allow bool
// Reason is shown to the user when Allow is false.
Reason string
// Context is additional context added for the agent (Claude Code only).
Context string
}
PromptDecision represents the unified decision for prompt hooks.
func AddPromptContext ¶
func AddPromptContext(context string) PromptDecision
AddPromptContext allows the prompt and adds context.
func AllowPromptDecision ¶
func AllowPromptDecision() PromptDecision
AllowPromptDecision returns a decision that allows the prompt.
func BlockPromptDecision ¶
func BlockPromptDecision(reason string) PromptDecision
BlockPromptDecision blocks the prompt with a reason.
type PromptHandler ¶
type PromptHandler func(PromptContext) PromptDecision
PromptHandler is the function signature for unified prompt handlers.
type StopContext ¶
type StopContext struct {
Platform Platform
SessionID string // Claude Code: session_id, Cursor: conversation_id
Cwd string // Working directory (Claude Code only, empty for Cursor)
// Claude Code-specific fields
StopHookActive bool // True if already continuing from a previous stop hook
// Cursor-specific fields
Status string // "completed", "aborted", or "error"
LoopCount int // Number of previous auto follow-ups (max 5)
}
StopContext provides a unified view of stop events from both platforms.
func (StopContext) ShouldSkip ¶
func (c StopContext) ShouldSkip() bool
ShouldSkip returns true if the stop hook should be skipped to prevent loops. For Claude Code and Droid, this checks StopHookActive. For Cursor, this checks LoopCount >= 3. For Cascade, there is no loop prevention mechanism (returns false).
type StopDecision ¶
type StopDecision struct {
// Continue determines whether the agent should stop.
// true = allow stopping, false = prevent stopping (continue working)
Continue bool
// Message is shown to the agent when Continue is false.
// For Claude Code, this becomes the "reason" field.
// For Cursor, this becomes the "followup_message" field.
Message string
}
StopDecision represents the unified decision for stop hooks.
func AllowStop ¶
func AllowStop() StopDecision
AllowStop returns a decision that allows the agent to stop.
func PreventStop ¶
func PreventStop(message string) StopDecision
PreventStop returns a decision that prevents stopping with a message.
type StopHandler ¶
type StopHandler func(StopContext) StopDecision
StopHandler is the function signature for unified stop handlers.
Directories
¶
| Path | Synopsis |
|---|---|
|
Build tool for cross-compiling hookshot binaries.
|
Build tool for cross-compiling hookshot binaries. |
|
Package cascade provides types and helpers for Windsurf Cascade hooks.
|
Package cascade provides types and helpers for Windsurf Cascade hooks. |
|
Package claude provides types and helpers for Claude Code hooks.
|
Package claude provides types and helpers for Claude Code hooks. |
|
cmd
|
|
|
hookshot
command
hookshot CLI for building and installing hooks.
|
hookshot CLI for building and installing hooks. |
|
Package cursor provides types and helpers for Cursor hooks.
|
Package cursor provides types and helpers for Cursor hooks. |
|
Package droid provides types and helpers for Factory Droid hooks.
|
Package droid provides types and helpers for Factory Droid hooks. |
|
examples
|
|
|
multi-hook
command
Example multi-hook binary demonstrating hookshot's patterns.
|
Example multi-hook binary demonstrating hookshot's patterns. |