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:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user