Skip to content

Commit fb0c6a8

Browse files
committed
fix: Stop/SubagentStop hookのエラー時fail-safeをblockからallowに変更
公式Claude Code hooks仕様に合わせた修正: - エラー時(コマンド失敗、JSON不正、バリデーション失敗)のdecision: "block" → decision: "" (allow stop)に変更 - エラー情報はsystemMessageとstderrに記録して観測性を維持 - ntfyなど通知コマンドが失敗してもClaudeが停止できるようになる 参照: 公式仕様ではhookエラーはnon-blocking errorとして扱われる types.go:353 `// "block" only; omit field to allow stop`
1 parent 5477192 commit fb0c6a8

File tree

1 file changed

+23
-31
lines changed

1 file changed

+23
-31
lines changed

executor.go

Lines changed: 23 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,7 @@ func (e *ActionExecutor) ExecuteStopAction(action Action, input *StopInput, rawJ
273273
stdout, stderr, exitCode, err := e.runner.RunCommandWithOutput(cmd, action.UseStdin, rawJSON)
274274

275275
// Command failed with non-zero exit code
276+
// Per official spec: hook errors are non-blocking → allow stop
276277
if exitCode != 0 {
277278
errMsg := fmt.Sprintf("Command failed with exit code %d: %s", exitCode, stderr)
278279
if strings.TrimSpace(stderr) == "" && err != nil {
@@ -281,8 +282,7 @@ func (e *ActionExecutor) ExecuteStopAction(action Action, input *StopInput, rawJ
281282
fmt.Fprintf(os.Stderr, "Warning: %s\n", errMsg)
282283
return &ActionOutput{
283284
Continue: true,
284-
Decision: "block",
285-
Reason: errMsg,
285+
Decision: "", // Allow stop on error
286286
SystemMessage: errMsg,
287287
}, nil
288288
}
@@ -302,8 +302,7 @@ func (e *ActionExecutor) ExecuteStopAction(action Action, input *StopInput, rawJ
302302
fmt.Fprintf(os.Stderr, "Warning: %s\n", errMsg)
303303
return &ActionOutput{
304304
Continue: true,
305-
Decision: "block",
306-
Reason: errMsg,
305+
Decision: "", // Allow stop on error
307306
SystemMessage: errMsg,
308307
}, nil
309308
}
@@ -315,8 +314,7 @@ func (e *ActionExecutor) ExecuteStopAction(action Action, input *StopInput, rawJ
315314
fmt.Fprintf(os.Stderr, "Warning: %s\n", errMsg)
316315
return &ActionOutput{
317316
Continue: true,
318-
Decision: "block",
319-
Reason: errMsg,
317+
Decision: "", // Allow stop on error
320318
SystemMessage: errMsg,
321319
}, nil
322320
}
@@ -327,8 +325,7 @@ func (e *ActionExecutor) ExecuteStopAction(action Action, input *StopInput, rawJ
327325
fmt.Fprintf(os.Stderr, "Warning: %s\n", errMsg)
328326
return &ActionOutput{
329327
Continue: true,
330-
Decision: "block",
331-
Reason: errMsg,
328+
Decision: "", // Allow stop on error
332329
SystemMessage: errMsg,
333330
}, nil
334331
}
@@ -349,14 +346,14 @@ func (e *ActionExecutor) ExecuteStopAction(action Action, input *StopInput, rawJ
349346
case "output":
350347
processedMessage := unifiedTemplateReplace(action.Message, rawJSON)
351348

352-
// Empty message check → fail-safe (decision: block)
349+
// Empty message check → allow stop (error in systemMessage)
350+
// Per official spec: hook errors are non-blocking → allow stop
353351
if strings.TrimSpace(processedMessage) == "" {
354352
errMsg := "Empty message in Stop action"
355353
fmt.Fprintf(os.Stderr, "Warning: %s\n", errMsg)
356354
return &ActionOutput{
357355
Continue: true,
358-
Decision: "block",
359-
Reason: errMsg,
356+
Decision: "", // Allow stop on error
360357
SystemMessage: errMsg,
361358
}, nil
362359
}
@@ -375,8 +372,7 @@ func (e *ActionExecutor) ExecuteStopAction(action Action, input *StopInput, rawJ
375372
fmt.Fprintf(os.Stderr, "Warning: %s\n", errMsg)
376373
return &ActionOutput{
377374
Continue: true,
378-
Decision: "block",
379-
Reason: processedMessage,
375+
Decision: "", // Allow stop on error
380376
SystemMessage: errMsg,
381377
}, nil
382378
}
@@ -402,13 +398,13 @@ func (e *ActionExecutor) ExecuteStopAction(action Action, input *StopInput, rawJ
402398
}
403399

404400
// Final validation: decision "block" requires non-empty reason
401+
// Per official spec: hook errors are non-blocking → allow stop on validation error
405402
if decision == "block" && strings.TrimSpace(reason) == "" {
406403
errMsg := "Empty reason when decision is 'block' (reason is required for block)"
407404
fmt.Fprintf(os.Stderr, "Warning: %s\n", errMsg)
408405
return &ActionOutput{
409406
Continue: true,
410-
Decision: "block",
411-
Reason: errMsg,
407+
Decision: "", // Allow stop on error
412408
SystemMessage: errMsg,
413409
}, nil
414410
}
@@ -425,14 +421,15 @@ func (e *ActionExecutor) ExecuteStopAction(action Action, input *StopInput, rawJ
425421
}
426422

427423
// ExecuteSubagentStopAction executes an action for the SubagentStop event.
428-
// Command failures result in exit status 2 to block the subagent stop operation.
424+
// Per official Claude Code hooks spec: hook errors are non-blocking (allow subagent stop).
429425
func (e *ActionExecutor) ExecuteSubagentStopAction(action Action, input *SubagentStopInput, rawJSON any) (*ActionOutput, error) {
430426
switch action.Type {
431427
case "command":
432428
cmd := unifiedTemplateReplace(action.Command, rawJSON)
433429
stdout, stderr, exitCode, err := e.runner.RunCommandWithOutput(cmd, action.UseStdin, rawJSON)
434430

435431
// Command failed with non-zero exit code
432+
// Per official spec: hook errors are non-blocking → allow subagent stop
436433
if exitCode != 0 {
437434
errMsg := fmt.Sprintf("Command failed with exit code %d: %s", exitCode, stderr)
438435
if strings.TrimSpace(stderr) == "" && err != nil {
@@ -441,8 +438,7 @@ func (e *ActionExecutor) ExecuteSubagentStopAction(action Action, input *Subagen
441438
fmt.Fprintf(os.Stderr, "Warning: %s\n", errMsg)
442439
return &ActionOutput{
443440
Continue: true,
444-
Decision: "block",
445-
Reason: errMsg,
441+
Decision: "", // Allow subagent stop on error
446442
SystemMessage: errMsg,
447443
}, nil
448444
}
@@ -462,8 +458,7 @@ func (e *ActionExecutor) ExecuteSubagentStopAction(action Action, input *Subagen
462458
fmt.Fprintf(os.Stderr, "Warning: %s\n", errMsg)
463459
return &ActionOutput{
464460
Continue: true,
465-
Decision: "block",
466-
Reason: errMsg,
461+
Decision: "", // Allow subagent stop on error
467462
SystemMessage: errMsg,
468463
}, nil
469464
}
@@ -475,8 +470,7 @@ func (e *ActionExecutor) ExecuteSubagentStopAction(action Action, input *Subagen
475470
fmt.Fprintf(os.Stderr, "Warning: %s\n", errMsg)
476471
return &ActionOutput{
477472
Continue: true,
478-
Decision: "block",
479-
Reason: errMsg,
473+
Decision: "", // Allow subagent stop on error
480474
SystemMessage: errMsg,
481475
}, nil
482476
}
@@ -487,8 +481,7 @@ func (e *ActionExecutor) ExecuteSubagentStopAction(action Action, input *Subagen
487481
fmt.Fprintf(os.Stderr, "Warning: %s\n", errMsg)
488482
return &ActionOutput{
489483
Continue: true,
490-
Decision: "block",
491-
Reason: errMsg,
484+
Decision: "", // Allow subagent stop on error
492485
SystemMessage: errMsg,
493486
}, nil
494487
}
@@ -509,14 +502,14 @@ func (e *ActionExecutor) ExecuteSubagentStopAction(action Action, input *Subagen
509502
case "output":
510503
processedMessage := unifiedTemplateReplace(action.Message, rawJSON)
511504

512-
// Empty message check → fail-safe (decision: block)
505+
// Empty message check → allow subagent stop (error in systemMessage)
506+
// Per official spec: hook errors are non-blocking → allow subagent stop
513507
if strings.TrimSpace(processedMessage) == "" {
514508
errMsg := "Empty message in SubagentStop action"
515509
fmt.Fprintf(os.Stderr, "Warning: %s\n", errMsg)
516510
return &ActionOutput{
517511
Continue: true,
518-
Decision: "block",
519-
Reason: errMsg,
512+
Decision: "", // Allow subagent stop on error
520513
SystemMessage: errMsg,
521514
}, nil
522515
}
@@ -535,8 +528,7 @@ func (e *ActionExecutor) ExecuteSubagentStopAction(action Action, input *Subagen
535528
fmt.Fprintf(os.Stderr, "Warning: %s\n", errMsg)
536529
return &ActionOutput{
537530
Continue: true,
538-
Decision: "block",
539-
Reason: processedMessage,
531+
Decision: "", // Allow subagent stop on error
540532
SystemMessage: errMsg,
541533
}, nil
542534
}
@@ -562,13 +554,13 @@ func (e *ActionExecutor) ExecuteSubagentStopAction(action Action, input *Subagen
562554
}
563555

564556
// Final validation: decision "block" requires non-empty reason
557+
// Per official spec: hook errors are non-blocking → allow subagent stop on validation error
565558
if decision == "block" && strings.TrimSpace(reason) == "" {
566559
errMsg := "Empty reason when decision is 'block' (reason is required for block)"
567560
fmt.Fprintf(os.Stderr, "Warning: %s\n", errMsg)
568561
return &ActionOutput{
569562
Continue: true,
570-
Decision: "block",
571-
Reason: errMsg,
563+
Decision: "", // Allow subagent stop on error
572564
SystemMessage: errMsg,
573565
}, nil
574566
}

0 commit comments

Comments
 (0)