From fb990264c15e79f4bfbfad5a4a2e5d4d6b9a4136 Mon Sep 17 00:00:00 2001 From: Brian Douglas Date: Wed, 18 Mar 2026 19:35:31 -0700 Subject: [PATCH 1/3] Replace --allowedTools with --dangerously-skip-permissions Revert the scoped permissions approach in favor of --dangerously-skip-permissions for sub-agent invocations. The allowedTools whitelist was causing rate limiting issues, so switch back to the simpler skip-permissions flag. Removes DefaultAllowedTools, AllowedTools config field, --allowed-tools CLI flag, and all related plumbing through the provider system. --- README.md | 2 +- cmd/run.go | 15 ++++----------- pkg/agent/agent.go | 7 +++---- pkg/agent/agent_test.go | 6 +++--- pkg/config/config.go | 20 ++++---------------- pkg/config/config_test.go | 12 ------------ pkg/provider/claude.go | 2 +- pkg/provider/provider.go | 7 +++---- pkg/worker/claude.go | 11 ++++------- pkg/worker/claude_test.go | 10 +++++----- 10 files changed, 28 insertions(+), 64 deletions(-) diff --git a/README.md b/README.md index 22f0f1d..4eab216 100644 --- a/README.md +++ b/README.md @@ -231,7 +231,7 @@ Sweeper dispatches automated Claude sub-agents. To stay within [Anthropic's usag - **Rate limiting**: agents are dispatched with a configurable delay between each (default 2s, `--rate-limit`) - **Concurrency cap**: hard maximum of 5 parallel agents regardless of flags -- **Scoped permissions**: sub-agents use `--allowedTools` with a narrow whitelist (Read, Write, Edit, Glob, Grep, limited Bash) instead of `--dangerously-skip-permissions` +- **Skip permissions**: sub-agents use `--dangerously-skip-permissions` for non-interactive automated operation - **Backoff**: exponential delay between retry rounds (5s, 10s, 20s, ... capped at 60s) - **Agent identification**: all prompts identify the sub-agent as an automated tool with human oversight diff --git a/cmd/run.go b/cmd/run.go index 9645f57..5d64099 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -22,7 +22,6 @@ func newRunCmd() *cobra.Command { var dryRun bool var maxRounds int var staleThreshold int - var allowedTools []string var useVM bool var vmName string var vmJcard string @@ -47,16 +46,11 @@ Examples: if clamped != concurrency { fmt.Printf("Concurrency clamped to %d (max %d)\n", clamped, config.MaxConcurrency) } - tools := append([]string{}, config.DefaultAllowedTools...) - if len(allowedTools) > 0 { - tools = append(tools, allowedTools...) - } cfg := config.Config{ - TargetDir: targetDir, - Concurrency: clamped, - RateLimit: rateLimit, - AllowedTools: tools, - TelemetryDir: ".sweeper/telemetry", + TargetDir: targetDir, + Concurrency: clamped, + RateLimit: rateLimit, + TelemetryDir: ".sweeper/telemetry", DryRun: dryRun, NoTapes: noTapes, MaxRounds: maxRounds, @@ -156,7 +150,6 @@ Examples: return nil }, } - cmd.Flags().StringSliceVar(&allowedTools, "allowed-tools", nil, "additional tools for sub-agents (e.g. 'Bash(npm:*),Bash(cargo:*)')") cmd.Flags().BoolVar(&dryRun, "dry-run", false, "show what would be fixed without making changes") cmd.Flags().IntVar(&maxRounds, "max-rounds", 1, "maximum retry rounds (1 = single pass)") cmd.Flags().IntVar(&staleThreshold, "stale-threshold", 2, "consecutive non-improving rounds before exploration mode") diff --git a/pkg/agent/agent.go b/pkg/agent/agent.go index 0899a4b..26a335b 100644 --- a/pkg/agent/agent.go +++ b/pkg/agent/agent.go @@ -77,14 +77,13 @@ func New(cfg config.Config, opts ...Option) *Agent { if p, err := provider.Get(provName); err == nil { a.providerKind = p.Kind a.executor = p.NewExec(provider.Config{ - Model: cfg.ProviderModel, - APIBase: cfg.ProviderAPI, - AllowedTools: cfg.AllowedTools, + Model: cfg.ProviderModel, + APIBase: cfg.ProviderAPI, }) } else { fmt.Printf("Warning: unknown provider %q, falling back to claude\n", provName) a.providerKind = provider.KindCLI - a.executor = worker.NewClaudeExecutor(cfg.AllowedTools) + a.executor = worker.NewClaudeExecutor() } for _, opt := range opts { diff --git a/pkg/agent/agent_test.go b/pkg/agent/agent_test.go index f72ad73..1c36fa9 100644 --- a/pkg/agent/agent_test.go +++ b/pkg/agent/agent_test.go @@ -903,7 +903,7 @@ func TestNewAgentFallbackOnUnknownProvider(t *testing.T) { Concurrency: 1, TelemetryDir: t.TempDir(), Provider: "nonexistent-provider-xyz", - AllowedTools: []string{"Read"}, + } a := New(cfg) // Should fall back to KindCLI and Claude executor without panicking @@ -921,7 +921,7 @@ func TestNewAgentWithProviderFromRegistry(t *testing.T) { Concurrency: 1, TelemetryDir: t.TempDir(), Provider: "claude", - AllowedTools: []string{"Read"}, + } a := New(cfg) if a.providerKind != provider.KindCLI { @@ -935,7 +935,7 @@ func TestNewAgentEmptyProviderDefaultsToClaude(t *testing.T) { Concurrency: 1, TelemetryDir: t.TempDir(), Provider: "", - AllowedTools: []string{"Read"}, + } a := New(cfg) if a.providerKind != provider.KindCLI { diff --git a/pkg/config/config.go b/pkg/config/config.go index f977113..12e28d1 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -2,21 +2,10 @@ package config import "time" -// DefaultAllowedTools is the baseline set of tools sweeper agents can use. -// Users can extend this via --allowed-tools without reverting to a blanket bypass. -var DefaultAllowedTools = []string{ - "Read", - "Write", - "Edit", - "Glob", - "Grep", -} - type Config struct { - TargetDir string - Concurrency int - RateLimit time.Duration // minimum delay between agent dispatches - AllowedTools []string // tools sub-agents are permitted to use + TargetDir string + Concurrency int + RateLimit time.Duration // minimum delay between agent dispatches TelemetryDir string DryRun bool NoTapes bool @@ -41,8 +30,7 @@ func Default() Config { TargetDir: ".", Concurrency: 2, RateLimit: 2 * time.Second, - AllowedTools: append([]string{}, DefaultAllowedTools...), - TelemetryDir: ".sweeper/telemetry", + TelemetryDir: ".sweeper/telemetry", DryRun: false, MaxRounds: 1, StaleThreshold: 2, diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 922672b..ae3080d 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -51,18 +51,6 @@ func TestClampConcurrency(t *testing.T) { } } -func TestDefaultAllowedTools(t *testing.T) { - cfg := Default() - if len(cfg.AllowedTools) != len(DefaultAllowedTools) { - t.Errorf("expected %d default allowed tools, got %d", len(DefaultAllowedTools), len(cfg.AllowedTools)) - } - // Verify it's a copy, not a shared slice - cfg.AllowedTools = append(cfg.AllowedTools, "Bash(npm:*)") - if len(Default().AllowedTools) != len(DefaultAllowedTools) { - t.Error("modifying config should not mutate DefaultAllowedTools") - } -} - func TestDefaultConfigHasVMDisabled(t *testing.T) { cfg := Default() if cfg.VM { diff --git a/pkg/provider/claude.go b/pkg/provider/claude.go index 0ca78a1..955fc65 100644 --- a/pkg/provider/claude.go +++ b/pkg/provider/claude.go @@ -7,7 +7,7 @@ func init() { Name: "claude", Kind: KindCLI, NewExec: func(cfg Config) worker.Executor { - return worker.NewClaudeExecutor(cfg.AllowedTools) + return worker.NewClaudeExecutor() }, }) } diff --git a/pkg/provider/provider.go b/pkg/provider/provider.go index 99565a5..2e1d192 100644 --- a/pkg/provider/provider.go +++ b/pkg/provider/provider.go @@ -13,10 +13,9 @@ const ( // Config holds provider-specific settings passed when constructing an executor. type Config struct { - Model string // model name (e.g. "qwen2.5-coder:7b") - APIBase string // base URL for API providers (e.g. "http://localhost:11434") - AllowedTools []string // tools for CLI harnesses - ExtraArgs []string // additional CLI arguments + Model string // model name (e.g. "qwen2.5-coder:7b") + APIBase string // base URL for API providers (e.g. "http://localhost:11434") + ExtraArgs []string // additional CLI arguments } // Provider describes a registered AI backend. diff --git a/pkg/worker/claude.go b/pkg/worker/claude.go index d4545ea..db425f0 100644 --- a/pkg/worker/claude.go +++ b/pkg/worker/claude.go @@ -3,15 +3,12 @@ package worker import ( "context" "os/exec" - "strings" "time" ) -// NewClaudeExecutor returns an Executor that invokes claude with the given -// allowed tools. This lets callers configure which tools sub-agents can use -// without reverting to --dangerously-skip-permissions. -func NewClaudeExecutor(allowedTools []string) Executor { - toolsArg := strings.Join(allowedTools, ",") +// NewClaudeExecutor returns an Executor that invokes claude with +// --dangerously-skip-permissions for non-interactive sub-agent use. +func NewClaudeExecutor() Executor { return func(ctx context.Context, task Task) Result { start := time.Now() prompt := task.Prompt @@ -20,7 +17,7 @@ func NewClaudeExecutor(allowedTools []string) Executor { } cmd := exec.CommandContext(ctx, "claude", "--print", - "--allowedTools", toolsArg, + "--dangerously-skip-permissions", prompt, ) cmd.Dir = task.Dir diff --git a/pkg/worker/claude_test.go b/pkg/worker/claude_test.go index 89a65df..2c4ce21 100644 --- a/pkg/worker/claude_test.go +++ b/pkg/worker/claude_test.go @@ -51,7 +51,7 @@ func TestClaudeExecutorUsesTaskPrompt(t *testing.T) { dir := t.TempDir() fakeClaude := filepath.Join(dir, "claude") // Script echoes the prompt argument (last arg) so we can verify it - // Args: --print --allowedTools + // Args: --print --dangerously-skip-permissions if err := os.WriteFile(fakeClaude, []byte("#!/bin/sh\necho \"$@\""), 0o755); err != nil { t.Fatal(err) } @@ -67,7 +67,7 @@ func TestClaudeExecutorUsesTaskPrompt(t *testing.T) { }, Prompt: customPrompt, } - result := NewClaudeExecutor([]string{"Read", "Edit"})(context.Background(), task) + result := NewClaudeExecutor()(context.Background(), task) if !result.Success { t.Fatalf("expected success, got error: %s", result.Error) } @@ -93,7 +93,7 @@ func TestClaudeExecutorFallsBackToBuildPrompt(t *testing.T) { }, // Prompt intentionally left empty } - result := NewClaudeExecutor([]string{"Read", "Edit"})(context.Background(), task) + result := NewClaudeExecutor()(context.Background(), task) if !result.Success { t.Fatalf("expected success, got error: %s", result.Error) } @@ -119,7 +119,7 @@ func TestClaudeExecutorSuccess(t *testing.T) { {File: "test.go", Line: 1, Message: "unused var", Linter: "revive"}, }, } - result := NewClaudeExecutor([]string{"Read", "Edit"})(context.Background(), task) + result := NewClaudeExecutor()(context.Background(), task) if !result.Success { t.Errorf("expected success, got error: %s", result.Error) } @@ -147,7 +147,7 @@ func TestClaudeExecutorError(t *testing.T) { {File: "test.go", Line: 1, Message: "unused var", Linter: "revive"}, }, } - result := NewClaudeExecutor([]string{"Read", "Edit"})(context.Background(), task) + result := NewClaudeExecutor()(context.Background(), task) if result.Success { t.Error("expected failure") } From 2c0a6f3e6a54185809ead0d64ee9c50fe407ec8a Mon Sep 17 00:00:00 2001 From: Brian Douglas Date: Wed, 18 Mar 2026 19:43:06 -0700 Subject: [PATCH 2/3] fix(ci): handle emoji variation selectors in PR title check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Emojis like ♻️ include a variation selector (U+FE0F) after the base codepoint, which broke the \p{So} single-character match. Make the variation selector optional with \x{FE0F}?. --- .github/workflows/pr.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 95cadd5..88465a5 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -24,7 +24,7 @@ jobs: - name: Check conventional commit title run: | title="${{ github.event.pull_request.title }}" - if ! echo "$title" | grep -qP '^(\p{So} )?(feat|fix|docs|chore|ci|refactor|test|perf|build|style|revert)(\(.+\))?!?: .+'; then + if ! echo "$title" | grep -qP '^(\p{So}\x{FE0F}? )?(feat|fix|docs|chore|ci|refactor|test|perf|build|style|revert)(\(.+\))?!?: .+'; then echo "::error::PR title must follow: [emoji] type: description" echo "Valid types: feat, fix, docs, chore, ci, refactor, test, perf, build, style, revert" echo "Examples: '✨ feat: Add new feature' or 'fix: Correct bug'" From 68b6b11b0db68178eaa8e62701e9e5634faf78b1 Mon Sep 17 00:00:00 2001 From: Brian Douglas Date: Wed, 18 Mar 2026 19:48:31 -0700 Subject: [PATCH 3/3] style: fix struct alignment and CI shell injection Normalize struct field alignment in Config and run.go after AllowedTools removal. Remove trailing blank lines in agent tests. Pass PR title via env var instead of direct interpolation to prevent shell injection. --- .github/workflows/pr.yaml | 5 +++-- cmd/run.go | 8 ++++---- pkg/agent/agent_test.go | 3 --- pkg/config/config.go | 8 ++++---- 4 files changed, 11 insertions(+), 13 deletions(-) diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 88465a5..9354484 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -22,9 +22,10 @@ jobs: uses: actions/checkout@v4 - name: Check conventional commit title + env: + PR_TITLE: ${{ github.event.pull_request.title }} run: | - title="${{ github.event.pull_request.title }}" - if ! echo "$title" | grep -qP '^(\p{So}\x{FE0F}? )?(feat|fix|docs|chore|ci|refactor|test|perf|build|style|revert)(\(.+\))?!?: .+'; then + if ! echo "$PR_TITLE" | grep -qP '^(\p{So}\x{FE0F}? )?(feat|fix|docs|chore|ci|refactor|test|perf|build|style|revert)(\(.+\))?!?: .+'; then echo "::error::PR title must follow: [emoji] type: description" echo "Valid types: feat, fix, docs, chore, ci, refactor, test, perf, build, style, revert" echo "Examples: '✨ feat: Add new feature' or 'fix: Correct bug'" diff --git a/cmd/run.go b/cmd/run.go index 5d64099..1210716 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -47,10 +47,10 @@ Examples: fmt.Printf("Concurrency clamped to %d (max %d)\n", clamped, config.MaxConcurrency) } cfg := config.Config{ - TargetDir: targetDir, - Concurrency: clamped, - RateLimit: rateLimit, - TelemetryDir: ".sweeper/telemetry", + TargetDir: targetDir, + Concurrency: clamped, + RateLimit: rateLimit, + TelemetryDir: ".sweeper/telemetry", DryRun: dryRun, NoTapes: noTapes, MaxRounds: maxRounds, diff --git a/pkg/agent/agent_test.go b/pkg/agent/agent_test.go index 1c36fa9..34b8ee5 100644 --- a/pkg/agent/agent_test.go +++ b/pkg/agent/agent_test.go @@ -903,7 +903,6 @@ func TestNewAgentFallbackOnUnknownProvider(t *testing.T) { Concurrency: 1, TelemetryDir: t.TempDir(), Provider: "nonexistent-provider-xyz", - } a := New(cfg) // Should fall back to KindCLI and Claude executor without panicking @@ -921,7 +920,6 @@ func TestNewAgentWithProviderFromRegistry(t *testing.T) { Concurrency: 1, TelemetryDir: t.TempDir(), Provider: "claude", - } a := New(cfg) if a.providerKind != provider.KindCLI { @@ -935,7 +933,6 @@ func TestNewAgentEmptyProviderDefaultsToClaude(t *testing.T) { Concurrency: 1, TelemetryDir: t.TempDir(), Provider: "", - } a := New(cfg) if a.providerKind != provider.KindCLI { diff --git a/pkg/config/config.go b/pkg/config/config.go index 12e28d1..3d7fbb2 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -3,9 +3,9 @@ package config import "time" type Config struct { - TargetDir string - Concurrency int - RateLimit time.Duration // minimum delay between agent dispatches + TargetDir string + Concurrency int + RateLimit time.Duration // minimum delay between agent dispatches TelemetryDir string DryRun bool NoTapes bool @@ -30,7 +30,7 @@ func Default() Config { TargetDir: ".", Concurrency: 2, RateLimit: 2 * time.Second, - TelemetryDir: ".sweeper/telemetry", + TelemetryDir: ".sweeper/telemetry", DryRun: false, MaxRounds: 1, StaleThreshold: 2,