on main help sign in
feat(hooks): add name field to hooks
021e377Bruno Krugel
internal/config/config.go+11
@@ -545,6 +545,8 @@ func (t ToolGrep) GetTimeout() time.Duration {
545// is owned by hooks.Runner so a JSON round-trip, merge, or reload can't
545// is owned by hooks.Runner so a JSON round-trip, merge, or reload can't
546// silently drop compiled state.
546// silently drop compiled state.
547type HookConfig struct {
547type HookConfig struct {
548+ // Friendly display name shown in the TUI. Falls back to Command when empty.
549+ Name string `json:"name,omitempty" jsonschema:"description=Friendly display name shown in the TUI for this hook"`
548 // Regex pattern tested against the tool name. Empty means match all.
550 // Regex pattern tested against the tool name. Empty means match all.
549 Matcher string `json:"matcher,omitempty" jsonschema:"description=Regex pattern tested against the tool name. Empty means match all tools."`
551 Matcher string `json:"matcher,omitempty" jsonschema:"description=Regex pattern tested against the tool name. Empty means match all tools."`
550 // Shell command to execute.
552 // Shell command to execute.
@@ -553,6 +555,15 @@ type HookConfig struct {
553 Timeout int `json:"timeout,omitempty" jsonschema:"description=Timeout in seconds for the hook command,default=30"`
555 Timeout int `json:"timeout,omitempty" jsonschema:"description=Timeout in seconds for the hook command,default=30"`
554}
556}
555
557
558+// DisplayName returns the hook name for display purposes. It returns Name
559+// when set, otherwise falls back to Command.
560+func (h *HookConfig) DisplayName() string {
561+ if h.Name != "" {
562+ return h.Name
563+ }
564+ return h.Command
565+}
566+
556// TimeoutDuration returns the hook timeout as a time.Duration, defaulting
567// TimeoutDuration returns the hook timeout as a time.Duration, defaulting
557// to 30s.
568// to 30s.
558func (h *HookConfig) TimeoutDuration() time.Duration {
569func (h *HookConfig) TimeoutDuration() time.Duration {
internal/hooks/hooks_test.go+31
@@ -476,6 +476,37 @@ func TestValidateHooksNormalizesEventNames(t *testing.T) {
476 }
476 }
477}
477}
478
478
479+func TestRunnerHookNameUsesDisplayName(t *testing.T) {
480+ t.Parallel()
481+
482+ t.Run("name field is used when set", func(t *testing.T) {
483+ t.Parallel()
484+ hookCfg := config.HookConfig{
485+ Name: "my-hook",
486+ Command: `echo '{"decision":"allow"}'`,
487+ }
488+ r := NewRunner([]config.HookConfig{hookCfg}, t.TempDir(), t.TempDir())
489+ result, err := r.Run(context.Background(), EventPreToolUse, "sess", "bash", `{}`)
490+ require.NoError(t, err)
491+ require.Equal(t, DecisionAllow, result.Decision)
492+ require.Len(t, result.Hooks, 1)
493+ require.Equal(t, "my-hook", result.Hooks[0].Name)
494+ })
495+
496+ t.Run("command is used when name is empty", func(t *testing.T) {
497+ t.Parallel()
498+ hookCfg := config.HookConfig{
499+ Command: `echo '{"decision":"allow"}'`,
500+ }
501+ r := NewRunner([]config.HookConfig{hookCfg}, t.TempDir(), t.TempDir())
502+ result, err := r.Run(context.Background(), EventPreToolUse, "sess", "bash", `{}`)
503+ require.NoError(t, err)
504+ require.Equal(t, DecisionAllow, result.Decision)
505+ require.Len(t, result.Hooks, 1)
506+ require.Equal(t, `echo '{"decision":"allow"}'`, result.Hooks[0].Name)
507+ })
508+}
509+
479func TestRunnerParallelExecution(t *testing.T) {
510func TestRunnerParallelExecution(t *testing.T) {
480 t.Parallel()
511 t.Parallel()
481 // Two hooks: one allows, one denies. Deny should win.
512 // Two hooks: one allows, one denies. Deny should win.
internal/hooks/runner.go+1−1
@@ -123,7 +123,7 @@ func (r *Runner) Run(ctx context.Context, eventName, sessionID, toolName, toolIn
123 agg.Hooks = make([]HookInfo, len(deduped))
123 agg.Hooks = make([]HookInfo, len(deduped))
124 for i, h := range deduped {
124 for i, h := range deduped {
125 agg.Hooks[i] = HookInfo{
125 agg.Hooks[i] = HookInfo{
126− Name: h.Command,
126+ Name: h.DisplayName(),
127 Matcher: h.Matcher,
127 Matcher: h.Matcher,
128 Decision: results[i].Decision.String(),
128 Decision: results[i].Decision.String(),
129 Halt: results[i].Halt,
129 Halt: results[i].Halt,
schema.json+4
@@ -106,6 +106,10 @@
106 },
106 },
107 "HookConfig": {
107 "HookConfig": {
108 "properties": {
108 "properties": {
109+ "name": {
110+ "type": "string",
111+ "description": "Friendly display name shown in the TUI for this hook"
112+ },
109 "matcher": {
113 "matcher": {
110 "type": "string",
114 "type": "string",
111 "description": "Regex pattern tested against the tool name. Empty means match all tools."
115 "description": "Regex pattern tested against the tool name. Empty means match all tools."