diff --git a/cmd/bd/setup.go b/cmd/bd/setup.go index baf69f61..36b2caf1 100644 --- a/cmd/bd/setup.go +++ b/cmd/bd/setup.go @@ -9,6 +9,7 @@ var ( setupProject bool setupCheck bool setupRemove bool + setupStealth bool ) var setupCmd = &cobra.Command{ @@ -86,7 +87,7 @@ agents from forgetting bd workflow after context compaction.`, return } - setup.InstallClaude(setupProject) + setup.InstallClaude(setupProject, setupStealth) }, } @@ -94,6 +95,7 @@ func init() { setupClaudeCmd.Flags().BoolVar(&setupProject, "project", false, "Install for this project only (not globally)") setupClaudeCmd.Flags().BoolVar(&setupCheck, "check", false, "Check if Claude integration is installed") setupClaudeCmd.Flags().BoolVar(&setupRemove, "remove", false, "Remove bd hooks from Claude settings") + setupClaudeCmd.Flags().BoolVar(&setupStealth, "stealth", false, "Use 'bd prime --stealth' (flush only, no git operations)") setupCursorCmd.Flags().BoolVar(&setupCheck, "check", false, "Check if Cursor integration is installed") setupCursorCmd.Flags().BoolVar(&setupRemove, "remove", false, "Remove bd rules from Cursor") diff --git a/cmd/bd/setup/claude.go b/cmd/bd/setup/claude.go index 80ade71e..4a3f3526 100644 --- a/cmd/bd/setup/claude.go +++ b/cmd/bd/setup/claude.go @@ -8,7 +8,7 @@ import ( ) // InstallClaude installs Claude Code hooks -func InstallClaude(project bool) { +func InstallClaude(project bool, stealth bool) { var settingsPath string if project { @@ -49,13 +49,19 @@ func InstallClaude(project bool) { settings["hooks"] = hooks } + // Determine which command to use + command := "bd prime" + if stealth { + command = "bd prime --stealth" + } + // Add SessionStart hook - if addHookCommand(hooks, "SessionStart", "bd prime") { + if addHookCommand(hooks, "SessionStart", command) { fmt.Println("✓ Registered SessionStart hook") } // Add PreCompact hook - if addHookCommand(hooks, "PreCompact", "bd prime") { + if addHookCommand(hooks, "PreCompact", command) { fmt.Println("✓ Registered PreCompact hook") } @@ -137,9 +143,11 @@ func RemoveClaude(project bool) { return } - // Remove bd prime hooks + // Remove bd prime hooks (both variants for backwards compatibility) removeHookCommand(hooks, "SessionStart", "bd prime") removeHookCommand(hooks, "PreCompact", "bd prime") + removeHookCommand(hooks, "SessionStart", "bd prime --stealth") + removeHookCommand(hooks, "PreCompact", "bd prime --stealth") // Write back data, err = json.MarshalIndent(settings, "", " ") @@ -284,7 +292,9 @@ func hasBeadsHooks(settingsPath string) bool { if !ok { continue } - if cmdMap["command"] == "bd prime" { + // Check for either variant + cmd := cmdMap["command"] + if cmd == "bd prime" || cmd == "bd prime --stealth" { return true } } diff --git a/cmd/bd/setup/claude_test.go b/cmd/bd/setup/claude_test.go index a9827bfb..4abb4ea3 100644 --- a/cmd/bd/setup/claude_test.go +++ b/cmd/bd/setup/claude_test.go @@ -22,6 +22,13 @@ func TestAddHookCommand(t *testing.T) { command: "bd prime", wantAdded: true, }, + { + name: "add stealth hook to empty hooks", + existingHooks: make(map[string]interface{}), + event: "SessionStart", + command: "bd prime --stealth", + wantAdded: true, + }, { name: "hook already exists", existingHooks: map[string]interface{}{ @@ -41,6 +48,25 @@ func TestAddHookCommand(t *testing.T) { command: "bd prime", wantAdded: false, }, + { + name: "stealth hook already exists", + existingHooks: map[string]interface{}{ + "SessionStart": []interface{}{ + map[string]interface{}{ + "matcher": "", + "hooks": []interface{}{ + map[string]interface{}{ + "type": "command", + "command": "bd prime --stealth", + }, + }, + }, + }, + }, + event: "SessionStart", + command: "bd prime --stealth", + wantAdded: false, + }, { name: "add second hook alongside existing", existingHooks: map[string]interface{}{ @@ -122,6 +148,25 @@ func TestRemoveHookCommand(t *testing.T) { command: "bd prime", wantRemaining: 0, }, + { + name: "remove stealth hook", + existingHooks: map[string]interface{}{ + "SessionStart": []interface{}{ + map[string]interface{}{ + "matcher": "", + "hooks": []interface{}{ + map[string]interface{}{ + "type": "command", + "command": "bd prime --stealth", + }, + }, + }, + }, + }, + event: "SessionStart", + command: "bd prime --stealth", + wantRemaining: 0, + }, { name: "remove one of multiple hooks", existingHooks: map[string]interface{}{ @@ -184,9 +229,9 @@ func TestHasBeadsHooks(t *testing.T) { tmpDir := t.TempDir() tests := []struct { - name string + name string settingsData map[string]interface{} - want bool + want bool }{ { name: "has bd prime hook", @@ -208,9 +253,66 @@ func TestHasBeadsHooks(t *testing.T) { want: true, }, { - name: "no hooks", + name: "has bd prime --stealth hook", + settingsData: map[string]interface{}{ + "hooks": map[string]interface{}{ + "SessionStart": []interface{}{ + map[string]interface{}{ + "matcher": "", + "hooks": []interface{}{ + map[string]interface{}{ + "type": "command", + "command": "bd prime --stealth", + }, + }, + }, + }, + }, + }, + want: true, + }, + { + name: "has bd prime in PreCompact", + settingsData: map[string]interface{}{ + "hooks": map[string]interface{}{ + "PreCompact": []interface{}{ + map[string]interface{}{ + "matcher": "", + "hooks": []interface{}{ + map[string]interface{}{ + "type": "command", + "command": "bd prime", + }, + }, + }, + }, + }, + }, + want: true, + }, + { + name: "has bd prime --stealth in PreCompact", + settingsData: map[string]interface{}{ + "hooks": map[string]interface{}{ + "PreCompact": []interface{}{ + map[string]interface{}{ + "matcher": "", + "hooks": []interface{}{ + map[string]interface{}{ + "type": "command", + "command": "bd prime --stealth", + }, + }, + }, + }, + }, + }, + want: true, + }, + { + name: "no hooks", settingsData: map[string]interface{}{}, - want: false, + want: false, }, { name: "has other hooks but not bd prime", @@ -242,7 +344,7 @@ func TestHasBeadsHooks(t *testing.T) { t.Fatalf("Failed to marshal test data: %v", err) } - if err := os.WriteFile(settingsPath, data, 0644); err != nil { + if err := os.WriteFile(settingsPath, data, 0o644); err != nil { t.Fatalf("Failed to write test file: %v", err) } @@ -276,3 +378,31 @@ func TestIdempotency(t *testing.T) { t.Errorf("Expected 1 hook, got %d", len(eventHooks)) } } + +// Test that running addHookCommand twice with stealth doesn't duplicate hooks +func TestIdempotencyWithStealth(t *testing.T) { + hooks := make(map[string]any) + + if !addHookCommand(hooks, "SessionStart", "bd prime --stealth") { + t.Error("First call should have added the stealth hook") + } + + // Second add (should detect existing) + if addHookCommand(hooks, "SessionStart", "bd prime --stealth") { + t.Error("Second call should have detected existing stealth hook") + } + + // Verify only one hook exists + eventHooks := hooks["SessionStart"].([]any) + if len(eventHooks) != 1 { + t.Errorf("Expected 1 hook, got %d", len(eventHooks)) + } + + // and that it's the correct one + hookMap := eventHooks[0].(map[string]any) + commands := hookMap["hooks"].([]any) + cmdMap := commands[0].(map[string]any) + if cmdMap["command"] != "bd prime --stealth" { + t.Errorf("Expected 'bd prime --stealth', got %v", cmdMap["command"]) + } +}