From f10e6b02df811a6fcaeb7c316714bf1b316d85d7 Mon Sep 17 00:00:00 2001 From: Michel Osswald Date: Sun, 14 Jun 2026 09:08:35 +0200 Subject: [PATCH] fix(clawpatch): address daily finding --- internal/guard/cli/cli.go | 19 +++++++++-- internal/guard/cli/cli_test.go | 59 ++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 3 deletions(-) diff --git a/internal/guard/cli/cli.go b/internal/guard/cli/cli.go index 341c37e..0bd7940 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) { + mode, err := fileModeOrDefault(path, 0o600) + if os.IsNotExist(err) { return nil } else if err != nil { return err @@ -450,16 +451,28 @@ func backupFile(path, label string) error { if err != nil { return err } - return os.WriteFile(backupPath, input, 0o644) + return os.WriteFile(backupPath, input, mode) } func writeJSONFile(path string, value any) error { + mode, err := fileModeOrDefault(path, 0o600) + if err != nil && !os.IsNotExist(err) { + return err + } bytes, err := json.MarshalIndent(value, "", " ") if err != nil { return err } bytes = append(bytes, '\n') - return os.WriteFile(path, bytes, 0o644) + return os.WriteFile(path, bytes, mode) +} + +func fileModeOrDefault(path string, fallback os.FileMode) (os.FileMode, error) { + info, err := os.Stat(path) + if err != nil { + return fallback, err + } + return info.Mode().Perm(), nil } 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..ff6c51b 100644 --- a/internal/guard/cli/cli_test.go +++ b/internal/guard/cli/cli_test.go @@ -194,6 +194,40 @@ func TestUninstallClaudeHooksPreservesNonGuardHookInMixedGroup(t *testing.T) { if command != "/usr/local/bin/custom-hook" { t.Fatalf("preserved command = %v, want custom hook", command) } + assertFileMode(t, settingsPath, 0o600) + assertBackupMode(t, settingsPath, 0o600) +} + +func TestInstallClaudeHooksPreservesClaudeSettingsPermissions(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) + } + settings := `{ + "hooks": { + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + {"type": "command", "command": "/usr/local/bin/custom-hook"} + ] + } + ] + } +}` + if err := os.WriteFile(settingsPath, []byte(settings), 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) + assertBackupMode(t, settingsPath, 0o600) } func TestPrintHookStatusReportsGuardAndHostedConflict(t *testing.T) { @@ -240,6 +274,31 @@ func TestPrintHookStatusReportsGuardAndHostedConflict(t *testing.T) { } } +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 assertBackupMode(t *testing.T, path string, want os.FileMode) { + t.Helper() + + matches, err := filepath.Glob(path + ".kontext-guard-backup-*") + if err != nil { + t.Fatal(err) + } + if len(matches) != 1 { + t.Fatalf("backup count = %d, want 1", len(matches)) + } + assertFileMode(t, matches[0], want) +} + func TestValidateLocalJudgeURLRejectsHostedURL(t *testing.T) { if err := validateLocalJudgeURL("https://api.example.com/v1"); err == nil { t.Fatal("validateLocalJudgeURL() error = nil, want hosted URL rejection")