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
46 changes: 46 additions & 0 deletions claude/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,18 @@ func Ask(reason string) PreToolUseOutput {
}
}

// AllowWithContext permits the tool and adds context for Claude.
func AllowWithContext(reason, context string) PreToolUseOutput {
return PreToolUseOutput{
HookSpecificOutput: &PreToolUseHookOutput{
HookEventName: "PreToolUse",
PermissionDecision: "allow",
PermissionDecisionReason: reason,
AdditionalContext: context,
},
}
}

// PassThrough returns an empty output, letting the normal permission flow proceed.
func PassThrough() PreToolUseOutput {
return PreToolUseOutput{}
Expand Down Expand Up @@ -124,6 +136,20 @@ func AllowPermissionWithInput(updatedInput map[string]any) PermissionRequestOutp
}
}

// AllowPermissionWithPermissions grants the permission and applies permission rule updates
// (equivalent to the user selecting an "always allow" option).
func AllowPermissionWithPermissions(updatedPermissions any) PermissionRequestOutput {
return PermissionRequestOutput{
HookSpecificOutput: &PermissionRequestHookOutput{
HookEventName: "PermissionRequest",
Decision: &PermissionRequestDecision{
Behavior: "allow",
UpdatedPermissions: updatedPermissions,
},
},
}
}

// DenyPermission rejects the permission request.
func DenyPermission(message string) PermissionRequestOutput {
return PermissionRequestOutput{
Expand Down Expand Up @@ -178,6 +204,16 @@ func PostToolContext(context string) PostToolUseOutput {
}
}

// PostToolReplaceMCPOutput replaces an MCP tool's output with the provided value.
func PostToolReplaceMCPOutput(output any) PostToolUseOutput {
return PostToolUseOutput{
HookSpecificOutput: &PostToolUseHookOutput{
HookEventName: "PostToolUse",
UpdatedMCPToolOutput: output,
},
}
}

// =============================================================================
// UserPromptSubmit Helpers
// =============================================================================
Expand Down Expand Up @@ -242,6 +278,16 @@ func NotificationOK() NotificationOutput {
return NotificationOutput{}
}

// NotificationContext adds context to the conversation from a notification hook.
func NotificationContext(context string) NotificationOutput {
return NotificationOutput{
HookSpecificOutput: &NotificationHookOutput{
HookEventName: "Notification",
AdditionalContext: context,
},
}
}

// =============================================================================
// PreCompact Helpers
// =============================================================================
Expand Down
61 changes: 60 additions & 1 deletion claude/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,22 @@ func TestAsk(t *testing.T) {
}
}

func TestAllowWithContext(t *testing.T) {
output := AllowWithContext("Tool is safe", "Running in production")
if output.HookSpecificOutput == nil {
t.Fatal("HookSpecificOutput should not be nil")
}
if output.HookSpecificOutput.PermissionDecision != "allow" {
t.Errorf("PermissionDecision = %q, want %q", output.HookSpecificOutput.PermissionDecision, "allow")
}
if output.HookSpecificOutput.PermissionDecisionReason != "Tool is safe" {
t.Errorf("PermissionDecisionReason = %q, want %q", output.HookSpecificOutput.PermissionDecisionReason, "Tool is safe")
}
if output.HookSpecificOutput.AdditionalContext != "Running in production" {
t.Errorf("AdditionalContext = %q, want %q", output.HookSpecificOutput.AdditionalContext, "Running in production")
}
}

func TestPassThrough(t *testing.T) {
output := PassThrough()
if output.HookSpecificOutput != nil {
Expand Down Expand Up @@ -156,6 +172,20 @@ func TestDenyPermission(t *testing.T) {
}
}

func TestAllowPermissionWithPermissions(t *testing.T) {
perms := map[string]any{"type": "toolAlwaysAllow", "tool": "Bash"}
output := AllowPermissionWithPermissions(perms)
if output.HookSpecificOutput == nil || output.HookSpecificOutput.Decision == nil {
t.Fatal("HookSpecificOutput and Decision should not be nil")
}
if output.HookSpecificOutput.Decision.Behavior != "allow" {
t.Errorf("Behavior = %q, want %q", output.HookSpecificOutput.Decision.Behavior, "allow")
}
if output.HookSpecificOutput.Decision.UpdatedPermissions == nil {
t.Error("UpdatedPermissions should not be nil")
}
}

func TestDenyPermissionAndStop(t *testing.T) {
output := DenyPermissionAndStop("Critical error")
if output.HookSpecificOutput == nil || output.HookSpecificOutput.Decision == nil {
Expand Down Expand Up @@ -200,6 +230,20 @@ func TestPostToolContext(t *testing.T) {
}
}

func TestPostToolReplaceMCPOutput(t *testing.T) {
replacement := map[string]string{"result": "sanitized"}
output := PostToolReplaceMCPOutput(replacement)
if output.HookSpecificOutput == nil {
t.Fatal("HookSpecificOutput should not be nil")
}
if output.HookSpecificOutput.HookEventName != "PostToolUse" {
t.Errorf("HookEventName = %q, want %q", output.HookSpecificOutput.HookEventName, "PostToolUse")
}
if output.HookSpecificOutput.UpdatedMCPToolOutput == nil {
t.Error("UpdatedMCPToolOutput should not be nil")
}
}

// =============================================================================
// UserPromptSubmit Helpers Tests
// =============================================================================
Expand Down Expand Up @@ -263,7 +307,22 @@ func TestSessionEndOK(t *testing.T) {

func TestNotificationOK(t *testing.T) {
output := NotificationOK()
_ = output
if output.HookSpecificOutput != nil {
t.Error("HookSpecificOutput should be nil for NotificationOK")
}
}

func TestNotificationContext(t *testing.T) {
output := NotificationContext("User needs help")
if output.HookSpecificOutput == nil {
t.Fatal("HookSpecificOutput should not be nil")
}
if output.HookSpecificOutput.HookEventName != "Notification" {
t.Errorf("HookEventName = %q, want %q", output.HookSpecificOutput.HookEventName, "Notification")
}
if output.HookSpecificOutput.AdditionalContext != "User needs help" {
t.Errorf("AdditionalContext = %q, want %q", output.HookSpecificOutput.AdditionalContext, "User needs help")
}
}

func TestPreCompactOK(t *testing.T) {
Expand Down
60 changes: 47 additions & 13 deletions claude/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ type BaseInput struct {
SessionID string `json:"session_id"`
TranscriptPath string `json:"transcript_path"`
Cwd string `json:"cwd"`
PermissionMode string `json:"permission_mode"` // "default", "plan", "acceptEdits", "bypassPermissions"
PermissionMode string `json:"permission_mode"` // "default", "plan", "acceptEdits", "dontAsk", "bypassPermissions"
HookEventName string `json:"hook_event_name"`
}

Expand Down Expand Up @@ -57,9 +57,21 @@ type StopOutput struct {
}

// SubagentStopInput is received when a Claude Code subagent (Task tool) finishes.
type SubagentStopInput = StopInput
type SubagentStopInput struct {
BaseInput
// StopHookActive is true when the subagent is already continuing as a result of a stop hook.
StopHookActive bool `json:"stop_hook_active"`
// AgentID is the unique identifier for the subagent.
AgentID string `json:"agent_id"`
// AgentType is the agent type name (e.g. "Bash", "Explore", "Plan", or custom agent names).
// This is the value used for matcher filtering.
AgentType string `json:"agent_type"`
// AgentTranscriptPath is the path to the subagent's own transcript file.
AgentTranscriptPath string `json:"agent_transcript_path,omitempty"`
}

// SubagentStopOutput controls whether a subagent should stop or continue.
// Uses the same decision format as StopOutput.
type SubagentStopOutput = StopOutput

// =============================================================================
Expand All @@ -71,6 +83,10 @@ type SessionStartInput struct {
BaseInput
// Source indicates how the session started: "startup", "resume", "clear", "compact"
Source string `json:"source"`
// Model is the model identifier (e.g. "claude-sonnet-4-6").
Model string `json:"model"`
// AgentType is the agent name when started via `claude --agent <name>`. May be empty.
AgentType string `json:"agent_type,omitempty"`
}

// SessionStartOutput can add context to the session.
Expand All @@ -92,7 +108,7 @@ type SessionStartHookOutput struct {
// SessionEndInput is received when a Claude Code session ends.
type SessionEndInput struct {
BaseInput
// Reason indicates why the session ended: "clear", "logout", "prompt_input_exit", "other"
// Reason indicates why the session ended: "clear", "logout", "prompt_input_exit", "bypass_permissions_disabled", "other"
Reason string `json:"reason"`
}

Expand Down Expand Up @@ -125,18 +141,26 @@ type PreToolUseHookOutput struct {
PermissionDecision string `json:"permissionDecision,omitempty"` // "allow", "deny", "ask"
PermissionDecisionReason string `json:"permissionDecisionReason,omitempty"` // Shown to user (allow/ask) or Claude (deny)
UpdatedInput map[string]any `json:"updatedInput,omitempty"` // Modified tool input
AdditionalContext string `json:"additionalContext,omitempty"` // Added to Claude's context before tool executes
}

// =============================================================================
// PermissionRequest
// =============================================================================

// PermissionSuggestion represents an "always allow" option from the permission dialog.
type PermissionSuggestion struct {
Type string `json:"type"`
Tool string `json:"tool,omitempty"`
}

// PermissionRequestInput is received when the user is shown a permission dialog.
type PermissionRequestInput struct {
BaseInput
ToolName string `json:"tool_name"`
ToolInput json.RawMessage `json:"tool_input"`
ToolUseID string `json:"tool_use_id"`
ToolName string `json:"tool_name"`
ToolInput json.RawMessage `json:"tool_input"`
ToolUseID string `json:"tool_use_id"`
PermissionSuggestions []PermissionSuggestion `json:"permission_suggestions,omitempty"`
}

// PermissionRequestOutput controls the permission dialog response.
Expand All @@ -153,10 +177,11 @@ type PermissionRequestHookOutput struct {

// PermissionRequestDecision controls how the permission request is handled.
type PermissionRequestDecision struct {
Behavior string `json:"behavior"` // "allow" or "deny"
UpdatedInput map[string]any `json:"updatedInput,omitempty"` // For "allow": modified tool input
Message string `json:"message,omitempty"` // For "deny": shown to Claude
Interrupt bool `json:"interrupt,omitempty"` // For "deny": stop Claude if true
Behavior string `json:"behavior"` // "allow" or "deny"
UpdatedInput map[string]any `json:"updatedInput,omitempty"` // For "allow": modified tool input
UpdatedPermissions any `json:"updatedPermissions,omitempty"` // For "allow": permission rule updates (equivalent to "always allow")
Message string `json:"message,omitempty"` // For "deny": shown to Claude
Interrupt bool `json:"interrupt,omitempty"` // For "deny": stop Claude if true
}

// =============================================================================
Expand Down Expand Up @@ -184,8 +209,9 @@ type PostToolUseOutput struct {

// PostToolUseHookOutput contains post-tool-use-specific output fields.
type PostToolUseHookOutput struct {
HookEventName string `json:"hookEventName,omitempty"`
AdditionalContext string `json:"additionalContext,omitempty"` // Added to context for Claude
HookEventName string `json:"hookEventName,omitempty"`
AdditionalContext string `json:"additionalContext,omitempty"` // Added to context for Claude
UpdatedMCPToolOutput any `json:"updatedMCPToolOutput,omitempty"` // For MCP tools: replaces the tool's output
}

// =============================================================================
Expand Down Expand Up @@ -222,12 +248,20 @@ type UserPromptSubmitHookOutput struct {
type NotificationInput struct {
BaseInput
Message string `json:"message"`
Title string `json:"title,omitempty"`
NotificationType string `json:"notification_type"` // "permission_prompt", "idle_prompt", "auth_success", "elicitation_dialog"
}

// NotificationOutput has no decision control - hooks just run for side effects.
// NotificationOutput can add context to the conversation.
type NotificationOutput struct {
BaseOutput
HookSpecificOutput *NotificationHookOutput `json:"hookSpecificOutput,omitempty"`
}

// NotificationHookOutput contains notification-specific output fields.
type NotificationHookOutput struct {
HookEventName string `json:"hookEventName,omitempty"`
AdditionalContext string `json:"additionalContext,omitempty"`
}

// =============================================================================
Expand Down
Loading