Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
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
87 changes: 82 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,7 @@ UserPromptSubmit:
- type: output
message: "Prompt validation message"
decision: "block" # optional: "block" only; omit to allow prompt
reason: "Detailed reason for blocking" # optional: reason for the decision
```

**Command Action** (type: `command`):
Expand All @@ -338,6 +339,7 @@ Commands must output JSON with the following structure:
{
"continue": true,
"decision": "block",
"reason": "Detailed reason for blocking",
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": "Message to display"
Expand All @@ -352,6 +354,7 @@ Note: To allow the prompt, omit the `decision` field entirely.
When multiple actions execute:
- `continue`: Always `true` (cannot be changed for UserPromptSubmit)
- `decision`: Last value wins (early return on `"block"`)
- `reason`: Reset when decision changes; concatenated with newline within same decision
- `hookEventName`: Set once by first action
- `additionalContext` and `systemMessage`: Concatenated with newline separator

Expand Down Expand Up @@ -387,11 +390,17 @@ UserPromptSubmit:
- type: output
message: "Dangerous command detected"
decision: "block"
reason: "Destructive operations require manual confirmation"
```

### PreToolUse JSON Output

PreToolUse hooks support JSON output format for Claude Code integration. Actions can return structured output with 3-stage permission control:
PreToolUse hooks support JSON output format for Claude Code integration. Actions can return structured output with 3-stage permission control.

**Input Fields** (in addition to BaseInput and tool_name/tool_input):
- `tool_use_id` (string): Unique identifier for this tool call, used to correlate PreToolUse and PostToolUse events
- `agent_id` (string, optional): Subagent identifier, only present when called from within a subagent
- `agent_type` (string, optional): Subagent type name (e.g., "Explore", "Bash", "Plan") or `--agent` flag value

**Output Action** (type: `output`):
```yaml
Expand Down Expand Up @@ -480,7 +489,10 @@ PreToolUse:

### Stop JSON Output

Stop hooks support JSON output format for Claude Code integration. Actions can return structured output with decision control to block or allow Claude's stopping:
Stop hooks support JSON output format for Claude Code integration. Actions can return structured output with decision control to block or allow Claude's stopping.

**Input Fields** (in addition to BaseInput and stop_hook_active):
- `last_assistant_message` (string): The full text of Claude's final response before stopping. Useful for validating task completion.

**Output Action** (type: `output`):
```yaml
Expand Down Expand Up @@ -549,7 +561,20 @@ Stop:

### SubagentStop JSON Output

SubagentStop hooks support JSON output format for Claude Code integration. Actions can return structured output with decision control to block or allow subagent stopping:
SubagentStop hooks support JSON output format for Claude Code integration. Actions can return structured output with decision control to block or allow subagent stopping.

**Input Fields**:
- `agent_id` (string): Unique identifier of the stopping subagent.
- `agent_type` (string): Type name of the subagent (e.g., `"Explore"`, `"Bash"`, `"Plan"`).
- `agent_transcript_path` (string): Path to the subagent's own transcript file (separate from the main session's `transcript_path`).
- `last_assistant_message` (string): The final response text from the subagent.

**Matcher Support**:
SubagentStop hooks support a `matcher` field to filter by agent type:
- Matches against the `agent_type` field from SubagentStop input
- Supports partial string matching (e.g., `"Exp"` matches `"Explore"`)
- Supports pipe-separated OR logic (e.g., `"Explore|Plan|Bash"`)
- Empty/omitted: Matches all agent types

**Output Action** (type: `output`):
```yaml
Expand Down Expand Up @@ -608,7 +633,12 @@ SubagentStop:

### PostToolUse JSON Output

PostToolUse hooks support JSON output format for Claude Code integration. Actions can return structured output with decision control and additional context:
PostToolUse hooks support JSON output format for Claude Code integration. Actions can return structured output with decision control and additional context.

**Input Fields** (in addition to BaseInput, tool_name/tool_input/tool_response):
- `tool_use_id` (string): Unique identifier for this tool call (same as PreToolUse)
- `agent_id` (string, optional): Subagent identifier
- `agent_type` (string, optional): Subagent type name

**Output Action** (type: `output`):
```yaml
Expand Down Expand Up @@ -869,7 +899,13 @@ PreCompact:

### SessionEnd JSON Output

SessionEnd hooks support JSON output format for Claude Code integration. Actions can return structured output for session cleanup:
SessionEnd hooks support JSON output format for Claude Code integration. Actions can return structured output for session cleanup.

**Matcher Support**:
SessionEnd hooks support a `matcher` field to filter by session end reason (exact match):
- Matches against the `reason` field from SessionEnd input
- Supports pipe-separated OR logic (e.g., `"clear|logout"`)
- Empty/omitted: Matches all reasons

**Output Action** (type: `output`):
```yaml
Expand Down Expand Up @@ -1065,6 +1101,47 @@ Available reason values:
- `bypass_permissions_disabled`: Session ended because bypass permissions were disabled
- `other`: Other reasons

### PermissionRequest Extensions

PermissionRequest hooks receive additional input fields and support output extensions:

**Input Fields:**
- **`tool_use_id`** (string): Unique identifier for the tool call triggering the permission dialog.
- **`permission_suggestions`** (array, optional): Permission rule suggestions provided by Claude Code's safety checks. Available as a template variable for advanced use cases.

**`updatedPermissions` Output:**
PermissionRequest hooks can return `updatedPermissions` to dynamically modify Claude Code's permission configuration when allowing a request:

**Command Action JSON:**
```json
{
"continue": true,
"hookSpecificOutput": {
"hookEventName": "PermissionRequest",
"decision": {
"behavior": "allow",
"updatedPermissions": [
{
"type": "addRules",
"behavior": "allow",
"destination": "session"
}
]
}
}
}
```

Available `updatedPermissions` entry types:
- `addRules`: Add permission rules
- `replaceRules`: Replace all rules of given behavior
- `removeRules`: Remove matching rules
- `setMode`: Change permission mode (`default`, `acceptEdits`, `dontAsk`, `bypassPermissions`, `plan`)
- `addDirectories`: Add working directories
- `removeDirectories`: Remove working directories

Available `destination` values: `session` (in-memory only), `localSettings`, `projectSettings`, `userSettings`

## Common Workflows

### Adding a New Hook Type
Expand Down
8 changes: 8 additions & 0 deletions executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -895,6 +895,7 @@ func (e *ActionExecutor) ExecuteUserPromptSubmitAction(action Action, input *Use
result := &ActionOutput{
Continue: true,
Decision: decision,
Reason: cmdOutput.Reason,
HookEventName: cmdOutput.HookSpecificOutput.HookEventName,
SystemMessage: cmdOutput.SystemMessage,
}
Expand Down Expand Up @@ -935,9 +936,15 @@ func (e *ActionExecutor) ExecuteUserPromptSubmitAction(action Action, input *Use
decision = *action.Decision
}

reason := ""
if action.Reason != nil {
reason = *action.Reason
}

return &ActionOutput{
Continue: true,
Decision: decision,
Reason: reason,
HookEventName: "UserPromptSubmit",
AdditionalContext: processedMessage,
}, nil
Expand Down Expand Up @@ -1165,6 +1172,7 @@ func checkUnsupportedFieldsUserPromptSubmit(stdout string) {
supportedFields := map[string]bool{
"continue": true,
"decision": true, // UserPromptSubmit specific
"reason": true, // Required when decision is "block"
"stopReason": true,
"suppressOutput": true,
"systemMessage": true,
Expand Down
19 changes: 10 additions & 9 deletions executor_tool_permission.go
Original file line number Diff line number Diff line change
Expand Up @@ -533,15 +533,16 @@ func (e *ActionExecutor) ExecutePermissionRequestAction(action Action, input *Pe

// Return output with all fields
return &ActionOutput{
Continue: continueValue,
Behavior: cmdOutput.HookSpecificOutput.Decision.Behavior,
Message: cmdOutput.HookSpecificOutput.Decision.Message,
Interrupt: cmdOutput.HookSpecificOutput.Decision.Interrupt,
UpdatedInput: cmdOutput.HookSpecificOutput.Decision.UpdatedInput,
HookEventName: cmdOutput.HookSpecificOutput.HookEventName,
SystemMessage: cmdOutput.SystemMessage,
StopReason: cmdOutput.StopReason,
SuppressOutput: cmdOutput.SuppressOutput,
Continue: continueValue,
Behavior: cmdOutput.HookSpecificOutput.Decision.Behavior,
Message: cmdOutput.HookSpecificOutput.Decision.Message,
Interrupt: cmdOutput.HookSpecificOutput.Decision.Interrupt,
UpdatedInput: cmdOutput.HookSpecificOutput.Decision.UpdatedInput,
UpdatedPermissions: cmdOutput.HookSpecificOutput.Decision.UpdatedPermissions,
HookEventName: cmdOutput.HookSpecificOutput.HookEventName,
SystemMessage: cmdOutput.SystemMessage,
StopReason: cmdOutput.StopReason,
SuppressOutput: cmdOutput.SuppressOutput,
}, nil

case "output":
Expand Down
12 changes: 12 additions & 0 deletions hooks_dryrun.go
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,12 @@ func dryRunSubagentStopHooks(config *Config, input *SubagentStopInput, rawJSON a

executed := false
for i, hook := range config.SubagentStop {
// matcherチェック
if !checkMatcher(hook.Matcher, input.AgentType) {
fmt.Printf("[Hook %d] Skipped (matcher %q does not match agent_type %q)\n", i+1, hook.Matcher, input.AgentType)
continue
}

// 条件チェック
shouldExecute := true
for _, condition := range hook.Conditions {
Expand Down Expand Up @@ -496,6 +502,12 @@ func dryRunSessionEndHooks(config *Config, input *SessionEndInput, rawJSON any)

executed := false
for i, hook := range config.SessionEnd {
// matcherチェック(reason値で絞り込み、完全一致)
if !checkNotificationMatcher(hook.Matcher, input.Reason) {
fmt.Printf("[Hook %d] Skipped (matcher %q does not match reason %q)\n", i+1, hook.Matcher, input.Reason)
continue
}

// 条件チェック
shouldExecute := true
for _, condition := range hook.Conditions {
Expand Down
24 changes: 23 additions & 1 deletion hooks_execute.go
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,11 @@ func executeSubagentStopHooks(config *Config, input *SubagentStopInput, rawJSON
var systemMessageBuilder strings.Builder

for i, hook := range config.SubagentStop {
// matcherチェック(agent_typeで絞り込み)
if !checkMatcher(hook.Matcher, input.AgentType) {
continue
}

// 条件チェック
shouldExecute := true
for _, condition := range hook.Conditions {
Expand Down Expand Up @@ -792,9 +797,21 @@ func executeUserPromptSubmitHooks(config *Config, input *UserPromptSubmitInput,
// Continue: always true (do not overwrite from actionOutput)
// finalOutput.Continue remains true

// Decision: overwrite (last one wins)
// Decision: 後勝ち。decision変更時はReasonリセット
prevDecision := finalOutput.Decision
finalOutput.Decision = actionOutput.Decision

// Reason: decision変更時はリセット、同一decision内では改行連結
if actionOutput.Decision != prevDecision {
finalOutput.Reason = actionOutput.Reason
} else if actionOutput.Reason != "" {
if finalOutput.Reason != "" {
finalOutput.Reason += "\n" + actionOutput.Reason
} else {
finalOutput.Reason = actionOutput.Reason
}
}

// HookEventName: set once and preserve
if hookEventName == "" && actionOutput.HookEventName != "" {
hookEventName = actionOutput.HookEventName
Expand Down Expand Up @@ -892,6 +909,11 @@ func executeSessionEndHooksJSON(config *Config, input *SessionEndInput, rawJSON
var systemMessageBuilder strings.Builder

for i, hook := range config.SessionEnd {
// matcherチェック(reason値で絞り込み、完全一致)
if !checkNotificationMatcher(hook.Matcher, input.Reason) {
continue
}

// 条件チェック
shouldExecute := true
for _, condition := range hook.Conditions {
Expand Down
Loading