hookshot

package module
v0.0.0-...-81c893e Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 30, 2026 License: MIT Imports: 9 Imported by: 0

README

hookshot

A Go library for building hooks for AI coding agents like Cursor, Claude Code, Windsurf Cascade, and Factory Droid.

Hooks are a key component of Agentic Coding Security Management (ACSM) — they let you observe, control, and secure AI agent behavior in your development environment.

Installation

go get github.com/CorridorSecurity/hookshot

Quick Start

package main

import (
    "strings"
    "github.com/CorridorSecurity/hookshot"
)

func main() {
    // Stop hooks - prevent agent from stopping prematurely
    hookshot.OnStop(func(ctx hookshot.StopContext) hookshot.StopDecision {
        if ctx.ShouldSkip() {
            return hookshot.AllowStop()
        }
        return hookshot.PreventStop("Please verify the changes")
    })

    // Execution hooks - control shell commands and MCP tools
    hookshot.OnBeforeExecution(func(ctx hookshot.ExecutionContext) hookshot.ExecutionDecision {
        if ctx.Type == hookshot.ExecutionShell {
            if strings.Contains(ctx.Command, "rm -rf /") {
                return hookshot.DenyExecution("Dangerous command blocked")
            }
        }
        return hookshot.AllowExecution()
    })

    // File edit hooks - react to file changes
    hookshot.OnAfterFileEdit(func(ctx hookshot.FileEditContext) hookshot.FileEditDecision {
        return hookshot.FileEditOK()
    })

    // Prompt hooks - validate user prompts
    hookshot.OnPromptSubmit(func(ctx hookshot.PromptContext) hookshot.PromptDecision {
        return hookshot.AllowPromptDecision()
    })

    hookshot.RunCommand()
}

Run with: ./my-hooks claude-stop or ./my-hooks cursor-stop

Building

# Current platform
go build -o my-hooks .

# All platforms
hookshot build -all -output ./dist

Installing Hooks

# Install to Claude Code and Cursor config files
hookshot install --binary /path/to/my-hooks

Configuration

Claude Code (~/.claude/settings.json)
{
  "hooks": {
    "Stop": [{ "hooks": [{ "type": "command", "command": "/path/to/my-hooks claude-stop" }] }],
    "PreToolUse": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "/path/to/my-hooks claude-pre-tool-use" }] }]
  }
}
Cursor (~/.cursor/hooks.json)
{
  "version": 1,
  "hooks": {
    "stop": [{ "command": "/path/to/my-hooks cursor-stop" }],
    "beforeShellExecution": [{ "command": "/path/to/my-hooks cursor-before-shell" }]
  }
}

Unified Handlers

Write once, run on both platforms:

Handler Claude Code Cursor
OnStop Stop stop
OnBeforeExecution PreToolUse beforeShellExecution, beforeMCPExecution
OnAfterFileEdit PostToolUse afterFileEdit
OnPromptSubmit UserPromptSubmit beforeSubmitPrompt

Platform-Specific Handlers

For platform-specific features, use Register:

// Claude Code only: SessionStart
hookshot.Register("claude-session-start", func() {
    hookshot.Run(func(input claude.SessionStartInput) claude.SessionStartOutput {
        return claude.SessionStartContext("Project uses Go 1.21+")
    })
})

// Cursor only: Tab completion
hookshot.Register("cursor-before-tab-read", func() {
    hookshot.Run(func(input cursor.BeforeTabFileReadInput) cursor.BeforeTabFileReadOutput {
        return cursor.AllowTabRead()
    })
})

// Windsurf Cascade: Pre-run command
hookshot.Register("cascade-pre-run-command", func() {
    hookshot.Run(func(input cascade.PreRunCommandInput) cascade.PreRunCommandOutput {
        return cascade.AllowCommand()
    })
})

// Factory Droid: Pre-tool use
hookshot.Register("droid-pre-tool-use", func() {
    hookshot.Run(func(input droid.PreToolUseInput) droid.PreToolUseOutput {
        return droid.PassThrough()
    })
})

Documentation

Full API documentation is available via godoc:

go doc github.com/CorridorSecurity/hookshot
go doc github.com/CorridorSecurity/hookshot/claude
go doc github.com/CorridorSecurity/hookshot/cursor
go doc github.com/CorridorSecurity/hookshot/cascade
go doc github.com/CorridorSecurity/hookshot/droid

Or view online at pkg.go.dev/github.com/CorridorSecurity/hookshot.

License

MIT

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:

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

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

func MustRegister(name string, handler Handler)

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

func ReadRawInput(v any) error

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

func Register(name string, handler Handler)

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

func Run[I any, O any](handler func(I) O)

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.

func RunE

func RunE[I any, O any](handler func(I) (O, error))

RunE is like Run but allows the handler to return an error. If the handler returns an error, stderr is written and the program exits with code 2 (blocking error that will be shown to the AI).

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 FileEdit

type FileEdit struct {
	OldString string
	NewString string
}

FileEdit represents a single edit operation.

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 Platform

type Platform string

Platform identifies which AI coding tool triggered the hook.

const (
	PlatformClaude  Platform = "claude"
	PlatformCursor  Platform = "cursor"
	PlatformDroid   Platform = "droid"
	PlatformCascade Platform = "cascade"
)

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL