Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,14 +99,14 @@ hookshot install --binary /path/to/my-hooks

## Unified Handlers

Write once, run on both platforms:
Write once, run on all four platforms:

| Handler | Claude Code | Cursor |
|---------|-------------|--------|
| `OnStop` | Stop | stop |
| `OnBeforeExecution` | PreToolUse | beforeShellExecution, beforeMCPExecution |
| `OnAfterFileEdit` | PostToolUse | afterFileEdit |
| `OnPromptSubmit` | UserPromptSubmit | beforeSubmitPrompt |
| Handler | Claude Code | Cursor | Windsurf Cascade | Factory Droid |
|---------|-------------|--------|------------------|---------------|
| `OnStop` | Stop | stop | post-cascade-response | Stop |
| `OnBeforeExecution` | PreToolUse | beforeShellExecution, beforeMCPExecution | pre-run-command, pre-mcp-tool-use | PreToolUse |
| `OnAfterFileEdit` | PostToolUse | afterFileEdit | post-write-code | PostToolUse |
| `OnPromptSubmit` | UserPromptSubmit | beforeSubmitPrompt | pre-user-prompt | UserPromptSubmit |

## Platform-Specific Handlers

Expand All @@ -127,10 +127,10 @@ hookshot.Register("cursor-before-tab-read", func() {
})
})

// Windsurf Cascade: Pre-run command
hookshot.Register("cascade-pre-run-command", func() {
hookshot.Run(func(input cascade.PreRunCommandInput) cascade.PreRunCommandOutput {
return cascade.AllowCommand()
// Windsurf Cascade: Pre-write-code (not covered by unified API)
hookshot.Register("cascade-pre-write-code", func() {
hookshot.RunE(func(input cascade.PreWriteCodeInput) (cascade.PreWriteCodeOutput, error) {
return cascade.AllowWrite(), nil
})
})

Expand Down
2 changes: 1 addition & 1 deletion cascade/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
// PreUserPrompt hooks validate or modify user prompts:
//
// hookshot.Run(func(input cascade.PreUserPromptInput) cascade.PreUserPromptOutput {
// if containsSecrets(input.ToolInfo.Prompt) {
// if containsSecrets(input.ToolInfo.UserPrompt) {
// return cascade.BlockPrompt("Please don't include secrets in prompts")
// }
// return cascade.AllowPrompt()
Expand Down
31 changes: 20 additions & 11 deletions cascade/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
// points in the agent loop. See Windsurf documentation for details.
package cascade

import "encoding/json"

// =============================================================================
// Common Types
// =============================================================================
Expand Down Expand Up @@ -72,10 +74,16 @@ type PostRunCommandOutput struct{}
// PreWriteCode
// =============================================================================

// CascadeEdit represents a single edit operation in Cascade write hooks.
type CascadeEdit struct {
OldString string `json:"old_string"`
NewString string `json:"new_string"`
}

// PreWriteCodeToolInfo contains details about the file write operation.
type PreWriteCodeToolInfo struct {
FilePath string `json:"file_path"`
Content string `json:"content"`
FilePath string `json:"file_path"`
Edits []CascadeEdit `json:"edits"`
}

// PreWriteCodeInput is received before writing a code file.
Expand All @@ -95,8 +103,8 @@ type PreWriteCodeOutput struct {

// PostWriteCodeToolInfo contains details about the written file.
type PostWriteCodeToolInfo struct {
FilePath string `json:"file_path"`
Content string `json:"content"`
FilePath string `json:"file_path"`
Edits []CascadeEdit `json:"edits"`
}

// PostWriteCodeInput is received after writing a code file.
Expand Down Expand Up @@ -153,9 +161,9 @@ type PostReadCodeOutput struct{}

// PreMCPToolUseToolInfo contains details about the MCP tool call.
type PreMCPToolUseToolInfo struct {
ToolName string `json:"tool_name"`
ToolInput string `json:"tool_input"` // JSON string of parameters
ServerURL string `json:"server_url,omitempty"`
MCPServerName string `json:"mcp_server_name"`
MCPToolName string `json:"mcp_tool_name"`
MCPToolArguments json.RawMessage `json:"mcp_tool_arguments"`
}

// PreMCPToolUseInput is received before an MCP tool executes.
Expand All @@ -175,9 +183,10 @@ type PreMCPToolUseOutput struct {

// PostMCPToolUseToolInfo contains details about the executed MCP tool.
type PostMCPToolUseToolInfo struct {
ToolName string `json:"tool_name"`
ToolInput string `json:"tool_input"`
ToolOutput string `json:"tool_output,omitempty"`
MCPServerName string `json:"mcp_server_name"`
MCPToolName string `json:"mcp_tool_name"`
MCPToolArguments json.RawMessage `json:"mcp_tool_arguments"`
MCPResult string `json:"mcp_result,omitempty"`
}

// PostMCPToolUseInput is received after an MCP tool executes.
Expand All @@ -195,7 +204,7 @@ type PostMCPToolUseOutput struct{}

// PreUserPromptToolInfo contains details about the user's prompt.
type PreUserPromptToolInfo struct {
Prompt string `json:"prompt"`
UserPrompt string `json:"user_prompt"`
}

// PreUserPromptInput is received when the user submits a prompt.
Expand Down
66 changes: 38 additions & 28 deletions docs/reference-cascade.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,17 +94,21 @@ func AskCommand(message string) PreRunCommandOutput

### Example

Cascade uses exit code 2 to block actions, so pre-hooks should use `RunE`. When the handler returns an error, the process exits with code 2 and Cascade blocks the action.

```go
hookshot.Register("cascade-pre-run-command", func() {
hookshot.Run(func(input cascade.PreRunCommandInput) cascade.PreRunCommandOutput {
hookshot.RunE(func(input cascade.PreRunCommandInput) (cascade.PreRunCommandOutput, error) {
if strings.Contains(input.ToolInfo.CommandLine, "rm -rf /") {
return cascade.DenyCommand("Dangerous command blocked")
return cascade.PreRunCommandOutput{}, fmt.Errorf("Dangerous command blocked")
}
return cascade.AllowCommand()
return cascade.AllowCommand(), nil
})
})
```

> **Note:** The unified handler `OnBeforeExecution` already handles this correctly — it uses `RunE` for Cascade pre-hooks automatically. You only need `Register` for Cascade-specific hooks not covered by the unified API.

---

## PostRunCommand
Expand Down Expand Up @@ -148,9 +152,14 @@ Called before writing a file.
### PreWriteCodeInput

```go
type CascadeEdit struct {
OldString string `json:"old_string"`
NewString string `json:"new_string"`
}

type PreWriteCodeToolInfo struct {
FilePath string `json:"file_path"`
Content string `json:"content"`
FilePath string `json:"file_path"`
Edits []CascadeEdit `json:"edits"`
}

type PreWriteCodeInput struct {
Expand Down Expand Up @@ -179,11 +188,11 @@ func AskWrite(message string) PreWriteCodeOutput

```go
hookshot.Register("cascade-pre-write-code", func() {
hookshot.Run(func(input cascade.PreWriteCodeInput) cascade.PreWriteCodeOutput {
hookshot.RunE(func(input cascade.PreWriteCodeInput) (cascade.PreWriteCodeOutput, error) {
if strings.HasSuffix(input.ToolInfo.FilePath, ".env") {
return cascade.DenyWrite("Cannot write to .env files")
return cascade.PreWriteCodeOutput{}, fmt.Errorf("Cannot write to .env files")
}
return cascade.AllowWrite()
return cascade.AllowWrite(), nil
})
})
```
Expand All @@ -198,8 +207,8 @@ Called after writing a file.

```go
type PostWriteCodeToolInfo struct {
FilePath string `json:"file_path"`
Content string `json:"content"`
FilePath string `json:"file_path"`
Edits []CascadeEdit `json:"edits"`
}

type PostWriteCodeInput struct {
Expand Down Expand Up @@ -259,11 +268,11 @@ func AskRead(message string) PreReadCodeOutput

```go
hookshot.Register("cascade-pre-read-code", func() {
hookshot.Run(func(input cascade.PreReadCodeInput) cascade.PreReadCodeOutput {
hookshot.RunE(func(input cascade.PreReadCodeInput) (cascade.PreReadCodeOutput, error) {
if strings.Contains(input.ToolInfo.FilePath, "secrets") {
return cascade.DenyRead("Cannot read secret files")
return cascade.PreReadCodeOutput{}, fmt.Errorf("Cannot read secret files")
}
return cascade.AllowRead()
return cascade.AllowRead(), nil
})
})
```
Expand Down Expand Up @@ -310,9 +319,9 @@ Called before an MCP tool executes.

```go
type PreMCPToolUseToolInfo struct {
ToolName string `json:"tool_name"`
ToolInput string `json:"tool_input"` // JSON string of parameters
ServerURL string `json:"server_url,omitempty"`
MCPServerName string `json:"mcp_server_name"`
MCPToolName string `json:"mcp_tool_name"`
MCPToolArguments json.RawMessage `json:"mcp_tool_arguments"`
}

type PreMCPToolUseInput struct {
Expand Down Expand Up @@ -341,11 +350,11 @@ func AskMCP(message string) PreMCPToolUseOutput

```go
hookshot.Register("cascade-pre-mcp-tool-use", func() {
hookshot.Run(func(input cascade.PreMCPToolUseInput) cascade.PreMCPToolUseOutput {
if strings.Contains(input.ToolInfo.ServerURL, "blocked.com") {
return cascade.DenyMCP("MCP server not allowed")
hookshot.RunE(func(input cascade.PreMCPToolUseInput) (cascade.PreMCPToolUseOutput, error) {
if input.ToolInfo.MCPServerName == "blocked-server" {
return cascade.PreMCPToolUseOutput{}, fmt.Errorf("MCP server not allowed")
}
return cascade.AllowMCP()
return cascade.AllowMCP(), nil
})
})
```
Expand All @@ -360,9 +369,10 @@ Called after an MCP tool executes.

```go
type PostMCPToolUseToolInfo struct {
ToolName string `json:"tool_name"`
ToolInput string `json:"tool_input"`
ToolOutput string `json:"tool_output,omitempty"`
MCPServerName string `json:"mcp_server_name"`
MCPToolName string `json:"mcp_tool_name"`
MCPToolArguments json.RawMessage `json:"mcp_tool_arguments"`
MCPResult string `json:"mcp_result,omitempty"`
}

type PostMCPToolUseInput struct {
Expand Down Expand Up @@ -393,7 +403,7 @@ Called when the user submits a prompt.

```go
type PreUserPromptToolInfo struct {
Prompt string `json:"prompt"`
UserPrompt string `json:"user_prompt"`
}

type PreUserPromptInput struct {
Expand Down Expand Up @@ -421,11 +431,11 @@ func BlockPrompt(message string) PreUserPromptOutput

```go
hookshot.Register("cascade-pre-user-prompt", func() {
hookshot.Run(func(input cascade.PreUserPromptInput) cascade.PreUserPromptOutput {
if strings.Contains(input.ToolInfo.Prompt, "api_key=") {
return cascade.BlockPrompt("Don't include API keys in prompts")
hookshot.RunE(func(input cascade.PreUserPromptInput) (cascade.PreUserPromptOutput, error) {
if strings.Contains(input.ToolInfo.UserPrompt, "api_key=") {
return cascade.PreUserPromptOutput{}, fmt.Errorf("Don't include API keys in prompts")
}
return cascade.AllowPrompt()
return cascade.AllowPrompt(), nil
})
})
```
Expand Down
Loading
Loading