Skip to content

Commit 18ffbee

Browse files
yanmxaclaude
andcommitted
feat: enhance hooks with context injection, updated input propagation, and permission scoping
- Add hook context injection and updated input propagation to FilterToolCalls - Scope permission_mode to relevant hook events (omit for session lifecycle) - Add Description field to SubagentStart hook input - Add /exit command for clean session termination - Add applyUpdatedToolInput for hook-modified tool arguments Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Meng Yan <myan@redhat.com>
1 parent 51dd77c commit 18ffbee

8 files changed

Lines changed: 66 additions & 11 deletions

File tree

internal/agent/executor.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -303,8 +303,9 @@ func (e *Executor) fireSubagentStart(req AgentRequest, agentHookID string) {
303303
return
304304
}
305305
e.hooks.ExecuteAsync(hooks.SubagentStart, hooks.HookInput{
306-
AgentType: req.Agent,
307-
AgentID: agentHookID,
306+
AgentType: req.Agent,
307+
AgentID: agentHookID,
308+
Description: req.Description,
308309
})
309310
}
310311

internal/app/handler_approval.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package app
22

33
import (
44
"context"
5+
"encoding/json"
56
"strings"
67

78
tea "github.com/charmbracelet/bubbletea"
@@ -103,6 +104,10 @@ func (m *model) checkPermissionHook(req *permission.PermissionRequest) (blocked,
103104
if outcome.PermissionAllow {
104105
// Apply structured permission updates from hook
105106
m.applyPermissionUpdates(outcome.UpdatedPermissions)
107+
// Propagate updated input back to the pending tool call
108+
if outcome.UpdatedInput != nil {
109+
m.applyUpdatedToolInput(outcome.UpdatedInput)
110+
}
106111
return false, true, outcome.HookSource
107112
}
108113

@@ -299,6 +304,19 @@ func (m *model) persistAllowRule(req *permission.PermissionRequest) {
299304
config.Reload()
300305
}
301306

307+
// applyUpdatedToolInput marshals the hook-provided input and updates the current
308+
// pending tool call so the executor uses the modified arguments.
309+
func (m *model) applyUpdatedToolInput(updated map[string]any) {
310+
if m.tool.PendingCalls == nil || m.tool.CurrentIdx >= len(m.tool.PendingCalls) {
311+
return
312+
}
313+
data, err := json.Marshal(updated)
314+
if err != nil {
315+
return
316+
}
317+
m.tool.PendingCalls[m.tool.CurrentIdx].Input = string(data)
318+
}
319+
302320
// togglePermissionPreview toggles the expand state of permission prompt previews.
303321
func (m *model) togglePermissionPreview() {
304322
m.approval.TogglePreview()

internal/app/handler_command.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,15 @@ func ExecuteCommand(ctx context.Context, m *model, input string) (string, tea.Cm
8888
return "", nil, false
8989
}
9090

91+
// Handle /exit like CC does
92+
if cmd == "exit" {
93+
if m.conv.Stream.Cancel != nil {
94+
m.conv.Stream.Cancel()
95+
}
96+
m.fireSessionEnd("user_exit")
97+
return "", tea.Quit, true
98+
}
99+
91100
handlers := handlerRegistry()
92101
if handler, ok := handlers[cmd]; ok {
93102
result, followUp, err := handler(ctx, m, args)

internal/app/handler_tool.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,9 +162,17 @@ func (m *model) handleAllToolsCompleted() tea.Cmd {
162162

163163
// filterToolCallsWithHooks runs PreToolUse hooks and filters blocked tools.
164164
func (m *model) filterToolCallsWithHooks(toolCalls []message.ToolCall) []message.ToolCall {
165-
allowed, blocked, hookAllowed := m.loop.FilterToolCalls(context.Background(), toolCalls)
165+
allowed, blocked, hookAllowed, hookContext := m.loop.FilterToolCalls(context.Background(), toolCalls)
166166
m.tool.HookAllowed = hookAllowed
167167

168+
// Inject additional context from hooks into conversation
169+
if hookContext != "" {
170+
m.conv.Append(message.ChatMessage{
171+
Role: message.RoleUser,
172+
Content: hookContext,
173+
})
174+
}
175+
168176
// Add blocked results as chat messages
169177
for _, br := range blocked {
170178
m.conv.Append(message.ChatMessage{

internal/core/core.go

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,8 @@ func (l *Loop) Run(ctx context.Context, opts RunOptions) (*Result, error) {
228228
}
229229

230230
// --- 5. Filter through hooks ---
231-
allowed, blocked, _ := l.FilterToolCalls(ctx, decision.ToolCalls)
231+
allowed, blocked, _, hookContext := l.FilterToolCalls(ctx, decision.ToolCalls)
232+
_ = hookContext // context injection handled by incremental callers
232233
for _, br := range blocked {
233234
l.AddToolResult(br)
234235
}
@@ -394,12 +395,13 @@ func (l *Loop) AddToolResult(r message.ToolResult) {
394395
// --- Tool dispatch ---
395396

396397
// FilterToolCalls runs PreToolUse hooks, returning allowed tool calls, blocked results,
397-
// and a set of tool call IDs that hooks explicitly allowed (can skip permission prompts).
398+
// a set of tool call IDs that hooks explicitly allowed (can skip permission prompts),
399+
// and any additional context injected by hooks.
398400
func (l *Loop) FilterToolCalls(ctx context.Context, calls []message.ToolCall) (
399-
allowed []message.ToolCall, blocked []message.ToolResult, hookAllowed map[string]bool,
401+
allowed []message.ToolCall, blocked []message.ToolResult, hookAllowed map[string]bool, additionalContext string,
400402
) {
401403
if l.Hooks == nil {
402-
return calls, nil, nil
404+
return calls, nil, nil, ""
403405
}
404406

405407
hookAllowed = make(map[string]bool)
@@ -423,13 +425,21 @@ func (l *Loop) FilterToolCalls(ctx context.Context, calls []message.ToolCall) (
423425
}
424426
}
425427

428+
if outcome.AdditionalContext != "" {
429+
if additionalContext == "" {
430+
additionalContext = outcome.AdditionalContext
431+
} else {
432+
additionalContext += "\n" + outcome.AdditionalContext
433+
}
434+
}
435+
426436
if outcome.PermissionAllow {
427437
hookAllowed[tc.ID] = true
428438
}
429439

430440
allowed = append(allowed, tc)
431441
}
432-
return allowed, blocked, hookAllowed
442+
return allowed, blocked, hookAllowed, additionalContext
433443
}
434444

435445
// firePostToolHook fires PostToolUse or PostToolUseFailure hooks after tool execution.

internal/core/core_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,7 @@ func TestFilterToolCallsNoHooks(t *testing.T) {
356356
{ID: "t2", Name: "Write"},
357357
}
358358

359-
allowed, blocked, _ := loop.FilterToolCalls(context.Background(), calls)
359+
allowed, blocked, _, _ := loop.FilterToolCalls(context.Background(), calls)
360360
if len(allowed) != 2 {
361361
t.Errorf("expected 2 allowed, got %d", len(allowed))
362362
}

internal/hooks/engine.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,12 +170,20 @@ func (e *Engine) SetTranscriptPath(path string) {
170170
}
171171

172172
// populateInputFields fills common fields in hook input.
173+
// permission_mode is only set for events where it's contextually relevant,
174+
// matching Claude Code's behavior (session lifecycle events omit it).
173175
func (e *Engine) populateInputFields(input *HookInput, event EventType) {
174176
input.SessionID = e.sessionID
175177
input.TranscriptPath = e.transcriptPath
176178
input.Cwd = e.cwd
177-
input.PermissionMode = e.permissionMode
178179
input.HookEventName = string(event)
180+
181+
switch event {
182+
case SessionStart, SessionEnd, Notification, SubagentStart, PreCompact:
183+
// These events don't include permission_mode (matches CC behavior)
184+
default:
185+
input.PermissionMode = e.permissionMode
186+
}
179187
}
180188

181189
// extractCommands filters and returns command-type hooks.

internal/hooks/types.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ type HookInput struct {
2727
SessionID string `json:"session_id"`
2828
TranscriptPath string `json:"transcript_path"`
2929
Cwd string `json:"cwd"`
30-
PermissionMode string `json:"permission_mode"`
30+
PermissionMode string `json:"permission_mode,omitempty"`
3131
HookEventName string `json:"hook_event_name"`
3232

3333
// Tool events
@@ -52,6 +52,7 @@ type HookInput struct {
5252
// Agent events
5353
AgentID string `json:"agent_id,omitempty"`
5454
AgentType string `json:"agent_type,omitempty"`
55+
Description string `json:"description,omitempty"`
5556
AgentTranscriptPath string `json:"agent_transcript_path,omitempty"`
5657
StopHookActive bool `json:"stop_hook_active,omitempty"`
5758

0 commit comments

Comments
 (0)