diff --git a/claude/helpers.go b/claude/helpers.go index 91214d2..aa1af74 100644 --- a/claude/helpers.go +++ b/claude/helpers.go @@ -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{} @@ -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{ @@ -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 // ============================================================================= @@ -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 // ============================================================================= diff --git a/claude/helpers_test.go b/claude/helpers_test.go index d9667e8..70a3cc6 100644 --- a/claude/helpers_test.go +++ b/claude/helpers_test.go @@ -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 { @@ -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 { @@ -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 // ============================================================================= @@ -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) { diff --git a/claude/types.go b/claude/types.go index 80aa289..4fd8c34 100644 --- a/claude/types.go +++ b/claude/types.go @@ -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"` } @@ -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 // ============================================================================= @@ -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 `. May be empty. + AgentType string `json:"agent_type,omitempty"` } // SessionStartOutput can add context to the session. @@ -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"` } @@ -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. @@ -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 } // ============================================================================= @@ -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 } // ============================================================================= @@ -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"` } // =============================================================================