Skip to content
Open
Changes from all commits
Commits
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
19 changes: 14 additions & 5 deletions internal/guard/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ func PrintHookStatus(out io.Writer) {
hosted := false
guard := false
for _, raw := range hooks {
visitHookCommands(raw, func(command string) {
walkHookCommands(raw, func(command string) bool {
switch {
case isGuardHookCommand(command):
guard = true
Expand All @@ -299,6 +299,7 @@ func PrintHookStatus(out io.Writer) {
hosted = true
fmt.Fprintf(out, "Claude Code hosted hook: %s\n", command)
}
return false
})
}
if guard && hosted {
Expand All @@ -316,10 +317,10 @@ func PrintHookStatus(out io.Writer) {
fmt.Fprintln(out, "Claude Code hook mode: no Kontext hook detected")
}

func visitHookCommands(raw any, visit func(string)) {
func walkHookCommands(raw any, visit func(string) bool) bool {
groups, ok := raw.([]any)
if !ok {
return
return false
}
for _, group := range groups {
groupMap, ok := group.(map[string]any)
Expand All @@ -336,10 +337,13 @@ func visitHookCommands(raw any, visit func(string)) {
continue
}
if command, ok := hookMap["command"].(string); ok {
visit(command)
if visit(command) {
return true
}
}
}
}
return false
}

func runHooks(args []string, out io.Writer) error {
Expand Down Expand Up @@ -534,7 +538,12 @@ func isGuardHookObject(raw any) bool {
}

func isGuardHookEntry(entry any) bool {
return isGuardHookCommand(fmt.Sprintf("%v", entry))
if command, ok := entry.(string); ok {
return isGuardHookCommand(command)
}
return walkHookCommands([]any{entry}, func(command string) bool {
return isGuardHookCommand(command)
})
}
Comment on lines 540 to 547
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Keep flat hook removal This fallback is used when an entry is not a normal matcher group or does not have a nested hooks array. Before this change, a flat entry like {"type":"command","command":"/usr/local/bin/kontext hook --agent claude --mode observe"} was removed because the formatted entry contained the guard command. Now the map is wrapped and passed to walkHookCommands, which only checks nested group["hooks"], so uninstall can leave that Guard hook behind and install can append a duplicate canonical hook.

Suggested change
func isGuardHookEntry(entry any) bool {
return isGuardHookCommand(fmt.Sprintf("%v", entry))
if command, ok := entry.(string); ok {
return isGuardHookCommand(command)
}
return walkHookCommands([]any{entry}, func(command string) bool {
return isGuardHookCommand(command)
})
}
func isGuardHookEntry(entry any) bool {
if command, ok := entry.(string); ok {
return isGuardHookCommand(command)
}
if isGuardHookObject(entry) {
return true
}
return walkHookCommands([]any{entry}, func(command string) bool {
return isGuardHookCommand(command)
})
}


func isGuardHookCommand(command string) bool {
Expand Down
Loading