fix: critical hooks bug - preserve all Claude Code settings

BREAKING BUG FIX: Hook installation was destroying existing Claude Code settings

- Fix critical data loss in --install-hook and --uninstall-hook commands
- Change ClaudeSettings to preserve ALL existing JSON settings via Data map
- Add automatic timestamped backup creation before any settings modification
- Implement surgical hook installation that adds to existing config structure
- Implement surgical hook removal that only removes shush hooks
- Prevent loss of user settings like includeCoAuthoredBy, themes, etc.
- Add comprehensive error handling for malformed settings files

This was a critical data loss bug that could destroy users' Claude Code configurations.
All existing settings are now preserved during hook operations.

Fixes: Users losing their Claude Code settings during hook installation
Added: Automatic backups with timestamp (settings.json.backup.TIMESTAMP)
Improved: Hook management now respects existing PostToolUse configurations
This commit is contained in:
carraes
2025-07-07 14:18:59 -03:00
parent 3aff5e644a
commit 9109964235
4 changed files with 297 additions and 65 deletions
+52 -7
View File
@@ -62,6 +62,10 @@ func InstallHooks(scope string) error {
return fmt.Errorf("failed to get settings path: %w", err)
}
if err := hooks.CreateBackup(path); err != nil {
return fmt.Errorf("failed to create backup: %w", err)
}
settings, err := hooks.LoadSettings(path)
if err != nil {
return fmt.Errorf("failed to load settings: %w", err)
@@ -109,6 +113,10 @@ func UninstallHooks(scope string) error {
return fmt.Errorf("shush hook not found for %s scope at %s", scopeName, path)
}
if err := hooks.CreateBackup(path); err != nil {
return fmt.Errorf("failed to create backup: %w", err)
}
if _, err := exec.LookPath("jq"); err == nil {
if err := uninstallWithJQ(path, scopeName); err == nil {
return nil
@@ -222,19 +230,56 @@ func listHooksForPath(path string) error {
return err
}
if len(settings.Hooks) == 0 {
hooksData, exists := settings.Data["hooks"]
if !exists {
return fmt.Errorf("no hooks")
}
hooksMap, ok := hooksData.(map[string]interface{})
if !ok {
return fmt.Errorf("invalid hooks format")
}
if len(hooksMap) == 0 {
return fmt.Errorf("no hooks")
}
hasShush := hooks.HasShushHook(settings)
for event, configs := range settings.Hooks {
for _, config := range configs {
for _, hook := range config.Hooks {
if hook.Command == "shush --changes-only" {
fmt.Printf(" ✓ shush --changes-only (%s: %s)\n", event, config.Matcher)
for event, eventData := range hooksMap {
configs, ok := eventData.([]interface{})
if !ok {
continue
}
for _, configData := range configs {
config, ok := configData.(map[string]interface{})
if !ok {
continue
}
matcher, _ := config["matcher"].(string)
hooksData, exists := config["hooks"]
if !exists {
continue
}
hooks, ok := hooksData.([]interface{})
if !ok {
continue
}
for _, hookData := range hooks {
hook, ok := hookData.(map[string]interface{})
if !ok {
continue
}
command, _ := hook["command"].(string)
if command == "shush --changes-only" {
fmt.Printf(" ✓ shush --changes-only (%s: %s)\n", event, matcher)
} else {
fmt.Printf(" ✓ %s (%s: %s)\n", hook.Command, event, config.Matcher)
fmt.Printf(" ✓ %s (%s: %s)\n", command, event, matcher)
}
}
}