From 3d484587e372d394ee04dc032df002dd63f7d475 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sat, 13 Dec 2025 09:42:52 +1100 Subject: [PATCH] fix(setup): auto-allowlist bd commands in Claude Code (#511) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add bd to Claude Code allowedTools when running `bd setup claude` so that all bd commands (create, update, close, etc.) can run without requiring per-command approval. Changes: - Add addAllowedTool() and removeAllowedTool() helper functions - InstallClaude() now adds "Bash(bd:*)" to allowedTools - RemoveClaude() cleans up the allowedTools entry - Add tests for new functionality Users who have already run `bd setup claude` can run it again to add the missing allowedTools entry while keeping their existing hooks. Fixes #511 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- cmd/bd/setup/claude.go | 51 ++++++++++++++ cmd/bd/setup/claude_test.go | 132 ++++++++++++++++++++++++++++++++++++ 2 files changed, 183 insertions(+) diff --git a/cmd/bd/setup/claude.go b/cmd/bd/setup/claude.go index 4a3f3526..9353828d 100644 --- a/cmd/bd/setup/claude.go +++ b/cmd/bd/setup/claude.go @@ -65,6 +65,11 @@ func InstallClaude(project bool, stealth bool) { fmt.Println("✓ Registered PreCompact hook") } + // Add bd to allowedTools so commands don't require per-command approval + if addAllowedTool(settings, "Bash(bd:*)") { + fmt.Println("✓ Added bd to allowedTools (no per-command approval needed)") + } + // Write back to file data, err = json.MarshalIndent(settings, "", " ") if err != nil { @@ -149,6 +154,9 @@ func RemoveClaude(project bool) { removeHookCommand(hooks, "SessionStart", "bd prime --stealth") removeHookCommand(hooks, "PreCompact", "bd prime --stealth") + // Remove bd from allowedTools + removeAllowedTool(settings, "Bash(bd:*)") + // Write back data, err = json.MarshalIndent(settings, "", " ") if err != nil { @@ -164,6 +172,49 @@ func RemoveClaude(project bool) { fmt.Println("✓ Claude hooks removed") } +// addAllowedTool adds a tool pattern to allowedTools if not already present +// Returns true if tool was added, false if already exists +func addAllowedTool(settings map[string]interface{}, tool string) bool { + // Get or create allowedTools array + allowedTools, ok := settings["allowedTools"].([]interface{}) + if !ok { + allowedTools = []interface{}{} + } + + // Check if tool already in list + for _, t := range allowedTools { + if t == tool { + fmt.Printf("✓ Tool already in allowedTools: %s\n", tool) + return false + } + } + + // Add tool to array + allowedTools = append(allowedTools, tool) + settings["allowedTools"] = allowedTools + return true +} + +// removeAllowedTool removes a tool pattern from allowedTools +func removeAllowedTool(settings map[string]interface{}, tool string) { + allowedTools, ok := settings["allowedTools"].([]interface{}) + if !ok { + return + } + + // Filter out the tool + var filtered []interface{} + for _, t := range allowedTools { + if t != tool { + filtered = append(filtered, t) + } else { + fmt.Printf("✓ Removed %s from allowedTools\n", tool) + } + } + + settings["allowedTools"] = filtered +} + // addHookCommand adds a hook command to an event if not already present // Returns true if hook was added, false if already exists func addHookCommand(hooks map[string]interface{}, event, command string) bool { diff --git a/cmd/bd/setup/claude_test.go b/cmd/bd/setup/claude_test.go index 4abb4ea3..36b1ee50 100644 --- a/cmd/bd/setup/claude_test.go +++ b/cmd/bd/setup/claude_test.go @@ -406,3 +406,135 @@ func TestIdempotencyWithStealth(t *testing.T) { t.Errorf("Expected 'bd prime --stealth', got %v", cmdMap["command"]) } } + +func TestAddAllowedTool(t *testing.T) { + tests := []struct { + name string + existingSettings map[string]interface{} + tool string + wantAdded bool + wantLen int + }{ + { + name: "add tool to empty settings", + existingSettings: make(map[string]interface{}), + tool: "Bash(bd:*)", + wantAdded: true, + wantLen: 1, + }, + { + name: "add tool to existing allowedTools", + existingSettings: map[string]interface{}{ + "allowedTools": []interface{}{"Bash(git:*)"}, + }, + tool: "Bash(bd:*)", + wantAdded: true, + wantLen: 2, + }, + { + name: "tool already exists", + existingSettings: map[string]interface{}{ + "allowedTools": []interface{}{"Bash(bd:*)"}, + }, + tool: "Bash(bd:*)", + wantAdded: false, + wantLen: 1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := addAllowedTool(tt.existingSettings, tt.tool) + if got != tt.wantAdded { + t.Errorf("addAllowedTool() = %v, want %v", got, tt.wantAdded) + } + + allowedTools, ok := tt.existingSettings["allowedTools"].([]interface{}) + if !ok { + t.Fatal("allowedTools not found") + } + + if len(allowedTools) != tt.wantLen { + t.Errorf("Expected %d tools, got %d", tt.wantLen, len(allowedTools)) + } + + // Verify tool exists in list + found := false + for _, tool := range allowedTools { + if tool == tt.tool { + found = true + break + } + } + if !found { + t.Errorf("Tool %q not found in allowedTools", tt.tool) + } + }) + } +} + +func TestRemoveAllowedTool(t *testing.T) { + tests := []struct { + name string + existingSettings map[string]interface{} + tool string + wantLen int + }{ + { + name: "remove only tool", + existingSettings: map[string]interface{}{ + "allowedTools": []interface{}{"Bash(bd:*)"}, + }, + tool: "Bash(bd:*)", + wantLen: 0, + }, + { + name: "remove one of multiple tools", + existingSettings: map[string]interface{}{ + "allowedTools": []interface{}{"Bash(git:*)", "Bash(bd:*)", "Bash(npm:*)"}, + }, + tool: "Bash(bd:*)", + wantLen: 2, + }, + { + name: "remove non-existent tool", + existingSettings: map[string]interface{}{ + "allowedTools": []interface{}{"Bash(git:*)"}, + }, + tool: "Bash(bd:*)", + wantLen: 1, + }, + { + name: "remove from empty settings", + existingSettings: make(map[string]interface{}), + tool: "Bash(bd:*)", + wantLen: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + removeAllowedTool(tt.existingSettings, tt.tool) + + allowedTools, ok := tt.existingSettings["allowedTools"].([]interface{}) + if !ok { + // If allowedTools doesn't exist, treat as empty + if tt.wantLen != 0 { + t.Errorf("Expected %d tools, got 0 (allowedTools not found)", tt.wantLen) + } + return + } + + if len(allowedTools) != tt.wantLen { + t.Errorf("Expected %d remaining tools, got %d", tt.wantLen, len(allowedTools)) + } + + // Verify tool is actually gone + for _, tool := range allowedTools { + if tool == tt.tool { + t.Errorf("Tool %q still present after removal", tt.tool) + } + } + }) + } +}