diff --git a/internal/guard/cli/cli.go b/internal/guard/cli/cli.go index 341c37e..44adb44 100644 --- a/internal/guard/cli/cli.go +++ b/internal/guard/cli/cli.go @@ -440,7 +440,8 @@ func readClaudeSettings() (string, map[string]any, error) { } func backupFile(path, label string) error { - if _, err := os.Stat(path); os.IsNotExist(err) { + info, err := os.Stat(path) + if os.IsNotExist(err) { return nil } else if err != nil { return err @@ -450,7 +451,7 @@ func backupFile(path, label string) error { if err != nil { return err } - return os.WriteFile(backupPath, input, 0o644) + return os.WriteFile(backupPath, input, info.Mode().Perm()) } func writeJSONFile(path string, value any) error { @@ -459,7 +460,15 @@ func writeJSONFile(path string, value any) error { return err } bytes = append(bytes, '\n') - return os.WriteFile(path, bytes, 0o644) + return os.WriteFile(path, bytes, fileModeOrDefault(path, 0o600)) +} + +func fileModeOrDefault(path string, fallback os.FileMode) os.FileMode { + info, err := os.Stat(path) + if err != nil { + return fallback + } + return info.Mode().Perm() } func mergeHooks(raw any, hookCommand string) map[string]any { diff --git a/internal/guard/cli/cli_test.go b/internal/guard/cli/cli_test.go index 6ecc43d..1729b8a 100644 --- a/internal/guard/cli/cli_test.go +++ b/internal/guard/cli/cli_test.go @@ -196,6 +196,44 @@ func TestUninstallClaudeHooksPreservesNonGuardHookInMixedGroup(t *testing.T) { } } +func TestInstallClaudeHooksPreservesRestrictiveSettingsMode(t *testing.T) { + home := t.TempDir() + t.Setenv("HOME", home) + settingsPath := filepath.Join(home, ".claude", "settings.json") + if err := os.MkdirAll(filepath.Dir(settingsPath), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(settingsPath, []byte(`{"hooks":{}}`), 0o600); err != nil { + t.Fatal(err) + } + + var stdout bytes.Buffer + if err := installClaudeHooks(&stdout, "/tmp/kontext.sock"); err != nil { + t.Fatal(err) + } + + assertFileMode(t, settingsPath, 0o600) + backups, err := filepath.Glob(settingsPath + ".kontext-guard-backup-*") + if err != nil { + t.Fatal(err) + } + if len(backups) != 1 { + t.Fatalf("backup count = %d, want 1", len(backups)) + } + assertFileMode(t, backups[0], 0o600) +} + +func assertFileMode(t *testing.T, path string, want os.FileMode) { + t.Helper() + info, err := os.Stat(path) + if err != nil { + t.Fatal(err) + } + if got := info.Mode().Perm(); got != want { + t.Fatalf("%s mode = %o, want %o", path, got, want) + } +} + func TestPrintHookStatusReportsGuardAndHostedConflict(t *testing.T) { home := t.TempDir() t.Setenv("HOME", home)