Skip to content

Commit 65d805c

Browse files
authored
Merge pull request #78 from syou6162/additional_field
feat: Claude Code hooks仕様への追従 - 既存イベントの不足フィールド追加
2 parents bd83ad9 + 61574f6 commit 65d805c

File tree

10 files changed

+789
-44
lines changed

10 files changed

+789
-44
lines changed

CLAUDE.md

Lines changed: 82 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,7 @@ UserPromptSubmit:
330330
- type: output
331331
message: "Prompt validation message"
332332
decision: "block" # optional: "block" only; omit to allow prompt
333+
reason: "Detailed reason for blocking" # optional: reason for the decision
333334
```
334335

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

@@ -387,11 +390,17 @@ UserPromptSubmit:
387390
- type: output
388391
message: "Dangerous command detected"
389392
decision: "block"
393+
reason: "Destructive operations require manual confirmation"
390394
```
391395

392396
### PreToolUse JSON Output
393397

394-
PreToolUse hooks support JSON output format for Claude Code integration. Actions can return structured output with 3-stage permission control:
398+
PreToolUse hooks support JSON output format for Claude Code integration. Actions can return structured output with 3-stage permission control.
399+
400+
**Input Fields** (in addition to BaseInput and tool_name/tool_input):
401+
- `tool_use_id` (string): Unique identifier for this tool call, used to correlate PreToolUse and PostToolUse events
402+
- `agent_id` (string, optional): Subagent identifier, only present when called from within a subagent
403+
- `agent_type` (string, optional): Subagent type name (e.g., "Explore", "Bash", "Plan") or `--agent` flag value
395404

396405
**Output Action** (type: `output`):
397406
```yaml
@@ -480,7 +489,10 @@ PreToolUse:
480489

481490
### Stop JSON Output
482491

483-
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:
492+
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.
493+
494+
**Input Fields** (in addition to BaseInput and stop_hook_active):
495+
- `last_assistant_message` (string): The full text of Claude's final response before stopping. Useful for validating task completion.
484496

485497
**Output Action** (type: `output`):
486498
```yaml
@@ -549,7 +561,20 @@ Stop:
549561

550562
### SubagentStop JSON Output
551563

552-
SubagentStop hooks support JSON output format for Claude Code integration. Actions can return structured output with decision control to block or allow subagent stopping:
564+
SubagentStop hooks support JSON output format for Claude Code integration. Actions can return structured output with decision control to block or allow subagent stopping.
565+
566+
**Input Fields**:
567+
- `agent_id` (string): Unique identifier of the stopping subagent.
568+
- `agent_type` (string): Type name of the subagent (e.g., `"Explore"`, `"Bash"`, `"Plan"`).
569+
- `agent_transcript_path` (string): Path to the subagent's own transcript file (separate from the main session's `transcript_path`).
570+
- `last_assistant_message` (string): The final response text from the subagent.
571+
572+
**Matcher Support**:
573+
SubagentStop hooks support a `matcher` field to filter by agent type:
574+
- Matches against the `agent_type` field from SubagentStop input
575+
- Supports partial string matching (e.g., `"Exp"` matches `"Explore"`)
576+
- Supports pipe-separated OR logic (e.g., `"Explore|Plan|Bash"`)
577+
- Empty/omitted: Matches all agent types
553578

554579
**Output Action** (type: `output`):
555580
```yaml
@@ -608,7 +633,12 @@ SubagentStop:
608633

609634
### PostToolUse JSON Output
610635

611-
PostToolUse hooks support JSON output format for Claude Code integration. Actions can return structured output with decision control and additional context:
636+
PostToolUse hooks support JSON output format for Claude Code integration. Actions can return structured output with decision control and additional context.
637+
638+
**Input Fields** (in addition to BaseInput, tool_name/tool_input/tool_response):
639+
- `tool_use_id` (string): Unique identifier for this tool call (same as PreToolUse)
640+
- `agent_id` (string, optional): Subagent identifier
641+
- `agent_type` (string, optional): Subagent type name
612642

613643
**Output Action** (type: `output`):
614644
```yaml
@@ -869,7 +899,13 @@ PreCompact:
869899

870900
### SessionEnd JSON Output
871901

872-
SessionEnd hooks support JSON output format for Claude Code integration. Actions can return structured output for session cleanup:
902+
SessionEnd hooks support JSON output format for Claude Code integration. Actions can return structured output for session cleanup.
903+
904+
**Matcher Support**:
905+
SessionEnd hooks support a `matcher` field to filter by session end reason (exact match):
906+
- Matches against the `reason` field from SessionEnd input
907+
- Supports pipe-separated OR logic (e.g., `"clear|logout"`)
908+
- Empty/omitted: Matches all reasons
873909

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

1104+
### PermissionRequest Extensions
1105+
1106+
PermissionRequest hooks receive additional input fields and support output extensions:
1107+
1108+
**Input Fields:**
1109+
- **`tool_use_id`** (string): Unique identifier for the tool call triggering the permission dialog.
1110+
- **`permission_suggestions`** (array, optional): Permission rule suggestions provided by Claude Code's safety checks. Available as a template variable for advanced use cases.
1111+
1112+
**`updatedPermissions` Output:**
1113+
PermissionRequest hooks can return `updatedPermissions` to dynamically modify Claude Code's permission configuration when allowing a request:
1114+
1115+
**Command Action JSON:**
1116+
```json
1117+
{
1118+
"continue": true,
1119+
"hookSpecificOutput": {
1120+
"hookEventName": "PermissionRequest",
1121+
"decision": {
1122+
"behavior": "allow",
1123+
"updatedPermissions": [
1124+
{
1125+
"type": "addRules",
1126+
"behavior": "allow",
1127+
"destination": "session"
1128+
}
1129+
]
1130+
}
1131+
}
1132+
}
1133+
```
1134+
1135+
Available `updatedPermissions` entry types:
1136+
- `addRules`: Add permission rules
1137+
- `replaceRules`: Replace all rules of given behavior
1138+
- `removeRules`: Remove matching rules
1139+
- `setMode`: Change permission mode (`default`, `acceptEdits`, `dontAsk`, `bypassPermissions`, `plan`)
1140+
- `addDirectories`: Add working directories
1141+
- `removeDirectories`: Remove working directories
1142+
1143+
Available `destination` values: `session` (in-memory only), `localSettings`, `projectSettings`, `userSettings`
1144+
10681145
## Common Workflows
10691146

10701147
### Adding a New Hook Type

executor.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -895,6 +895,7 @@ func (e *ActionExecutor) ExecuteUserPromptSubmitAction(action Action, input *Use
895895
result := &ActionOutput{
896896
Continue: true,
897897
Decision: decision,
898+
Reason: cmdOutput.Reason,
898899
HookEventName: cmdOutput.HookSpecificOutput.HookEventName,
899900
SystemMessage: cmdOutput.SystemMessage,
900901
}
@@ -935,9 +936,15 @@ func (e *ActionExecutor) ExecuteUserPromptSubmitAction(action Action, input *Use
935936
decision = *action.Decision
936937
}
937938

939+
reason := ""
940+
if action.Reason != nil {
941+
reason = *action.Reason
942+
}
943+
938944
return &ActionOutput{
939945
Continue: true,
940946
Decision: decision,
947+
Reason: reason,
941948
HookEventName: "UserPromptSubmit",
942949
AdditionalContext: processedMessage,
943950
}, nil
@@ -1165,6 +1172,7 @@ func checkUnsupportedFieldsUserPromptSubmit(stdout string) {
11651172
supportedFields := map[string]bool{
11661173
"continue": true,
11671174
"decision": true, // UserPromptSubmit specific
1175+
"reason": true, // Required when decision is "block"
11681176
"stopReason": true,
11691177
"suppressOutput": true,
11701178
"systemMessage": true,

executor_tool_permission.go

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -533,15 +533,16 @@ func (e *ActionExecutor) ExecutePermissionRequestAction(action Action, input *Pe
533533

534534
// Return output with all fields
535535
return &ActionOutput{
536-
Continue: continueValue,
537-
Behavior: cmdOutput.HookSpecificOutput.Decision.Behavior,
538-
Message: cmdOutput.HookSpecificOutput.Decision.Message,
539-
Interrupt: cmdOutput.HookSpecificOutput.Decision.Interrupt,
540-
UpdatedInput: cmdOutput.HookSpecificOutput.Decision.UpdatedInput,
541-
HookEventName: cmdOutput.HookSpecificOutput.HookEventName,
542-
SystemMessage: cmdOutput.SystemMessage,
543-
StopReason: cmdOutput.StopReason,
544-
SuppressOutput: cmdOutput.SuppressOutput,
536+
Continue: continueValue,
537+
Behavior: cmdOutput.HookSpecificOutput.Decision.Behavior,
538+
Message: cmdOutput.HookSpecificOutput.Decision.Message,
539+
Interrupt: cmdOutput.HookSpecificOutput.Decision.Interrupt,
540+
UpdatedInput: cmdOutput.HookSpecificOutput.Decision.UpdatedInput,
541+
UpdatedPermissions: cmdOutput.HookSpecificOutput.Decision.UpdatedPermissions,
542+
HookEventName: cmdOutput.HookSpecificOutput.HookEventName,
543+
SystemMessage: cmdOutput.SystemMessage,
544+
StopReason: cmdOutput.StopReason,
545+
SuppressOutput: cmdOutput.SuppressOutput,
545546
}, nil
546547

547548
case "output":

hooks_dryrun.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,12 @@ func dryRunSubagentStopHooks(config *Config, input *SubagentStopInput, rawJSON a
277277

278278
executed := false
279279
for i, hook := range config.SubagentStop {
280+
// matcherチェック
281+
if !checkMatcher(hook.Matcher, input.AgentType) {
282+
fmt.Printf("[Hook %d] Skipped (matcher %q does not match agent_type %q)\n", i+1, hook.Matcher, input.AgentType)
283+
continue
284+
}
285+
280286
// 条件チェック
281287
shouldExecute := true
282288
for _, condition := range hook.Conditions {
@@ -496,6 +502,12 @@ func dryRunSessionEndHooks(config *Config, input *SessionEndInput, rawJSON any)
496502

497503
executed := false
498504
for i, hook := range config.SessionEnd {
505+
// matcherチェック(reason値で絞り込み、完全一致)
506+
if !checkNotificationMatcher(hook.Matcher, input.Reason) {
507+
fmt.Printf("[Hook %d] Skipped (matcher %q does not match reason %q)\n", i+1, hook.Matcher, input.Reason)
508+
continue
509+
}
510+
499511
// 条件チェック
500512
shouldExecute := true
501513
for _, condition := range hook.Conditions {

hooks_execute.go

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,11 @@ func executeSubagentStopHooks(config *Config, input *SubagentStopInput, rawJSON
411411
var systemMessageBuilder strings.Builder
412412

413413
for i, hook := range config.SubagentStop {
414+
// matcherチェック(agent_typeで絞り込み)
415+
if !checkMatcher(hook.Matcher, input.AgentType) {
416+
continue
417+
}
418+
414419
// 条件チェック
415420
shouldExecute := true
416421
for _, condition := range hook.Conditions {
@@ -792,9 +797,21 @@ func executeUserPromptSubmitHooks(config *Config, input *UserPromptSubmitInput,
792797
// Continue: always true (do not overwrite from actionOutput)
793798
// finalOutput.Continue remains true
794799

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

804+
// Reason: decision変更時はリセット、同一decision内では改行連結
805+
if actionOutput.Decision != prevDecision {
806+
finalOutput.Reason = actionOutput.Reason
807+
} else if actionOutput.Reason != "" {
808+
if finalOutput.Reason != "" {
809+
finalOutput.Reason += "\n" + actionOutput.Reason
810+
} else {
811+
finalOutput.Reason = actionOutput.Reason
812+
}
813+
}
814+
798815
// HookEventName: set once and preserve
799816
if hookEventName == "" && actionOutput.HookEventName != "" {
800817
hookEventName = actionOutput.HookEventName
@@ -892,6 +909,11 @@ func executeSessionEndHooksJSON(config *Config, input *SessionEndInput, rawJSON
892909
var systemMessageBuilder strings.Builder
893910

894911
for i, hook := range config.SessionEnd {
912+
// matcherチェック(reason値で絞り込み、完全一致)
913+
if !checkNotificationMatcher(hook.Matcher, input.Reason) {
914+
continue
915+
}
916+
895917
// 条件チェック
896918
shouldExecute := true
897919
for _, condition := range hook.Conditions {

0 commit comments

Comments
 (0)