# feat(review): grant the claude reviewer child a read-only tool allowlist

`90c8d54`·

peyton-alt·1w ago·2 files·+72 added/-2 removed

Headless claude -p auto-denies any tool call that would have prompted
interactively, and buildReviewCmd passed no permission config — so
finder subagents bounced off denials for git diff/log/show and detoured
around them (observed in a live review transcript: 'attempted tool
calls, hit approval/tool restrictions, and continued').

Pass --allowedTools with the read-only surface a review needs: Read/
Grep/Glob/Task/TodoWrite, enumerated read-only git subcommands (not
Bash(git:\*) — aliases/hooks execute arbitrary code and push/commit
match the blanket), and entire's read-only lookup commands referenced
by the checkpoint context. Never --dangerously-skip-permissions:
reviewers process untrusted diffs and must stay unable to write.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

## Sessions

f0c2e8deec13View transcript

[?\
# Handoff To Claude: `entire review` RedoClaude Code·17 steps](/content/gh/entireio/cli/session/93833a17-c2c6-4cb0-85b2-663c867b105f#timeline-f0c2e8deec13/index.html)

## Changes

2

- cmd/entire/cli/agent/claudecode
  
  - Mreviewer.go+27/-1
  
  - Mreviewer_test.go+45/-1

```
7 unmodified lines

8
9
10
11
12
13
14
19 unmodified lines

34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
40
62
63
64
65
66
67
68
69

7 unmodified lines

"io"
	"os"
	"os/exec"
	"strings"

"github.com/entireio/cli/cmd/entire/cli/review"
	reviewtypes "github.com/entireio/cli/cmd/entire/cli/review/types"
19 unmodified lines

}
}

// reviewToolAllowlist is the read-only tool surface a headless claude
// review child gets pre-approved. Print mode (-p) auto-denies any tool call
// that would have prompted interactively, so without an explicit allowlist
// every finder that runs `git diff`/`git log` bounces off a denial and
// detours (slower reviews, findings verified by workaround or not at all).
//
// Deliberately narrow: read-only git subcommands are enumerated instead of
// granting Bash(git:\*) — git can execute arbitrary code via aliases/hooks,
// and push/commit match the blanket pattern. Nothing write-capable (Edit,
// Write) and never --dangerously-skip-permissions: reviewers process
// untrusted input (the diff under review), so the child must stay unable to
// modify the repo. --allowedTools ADDS to the user's own permission config;
// it cannot revoke anything.
var reviewToolAllowlist = []string{
	"Read", "Grep", "Glob", "Task", "TodoWrite",
	"Bash(git diff:*)", "Bash(git log:*)", "Bash(git show:*)",
	"Bash(git status:*)", "Bash(git blame:*)", "Bash(git rev-parse:*)",
	"Bash(git merge-base:*)", "Bash(git ls-files:*)", "Bash(git branch:*)",
	"Bash(entire search:*)", "Bash(entire checkpoint explain:*)",
}

// buildReviewCmd builds the exec.Cmd for a claude review run.
// Exposed at package level for test inspection of argv and env.
func buildReviewCmd(ctx context.Context, cfg reviewtypes.RunConfig) *exec.Cmd {
	prompt := review.ComposeReviewPrompt(cfg)
	args := []string{"-p", prompt, "--output-format", "stream-json", "--verbose"}
	args := []string{
		"-p", prompt,
		"--output-format", "stream-json", "--verbose",
		"--allowedTools", strings.Join(reviewToolAllowlist, ","),
	}
	args = review.AppendModelFlag(args, cfg.Model)
	cmd := exec.CommandContext(ctx, "claude", args...)
	cmd.Env = review.AppendReviewEnv(os.Environ(), "claude-code", cfg, prompt)
}

Mcmd/entire/cli/agent/claudecode/reviewer.go+27/-1

```

95 unmodified lines

96
97
98
99
99
100
101
102
103
297 unmodified lines

401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446

95 unmodified lines

cmd := buildReviewCmd(context.Background(), cfg)

// Expect: claude -p <prompt> --output-format stream-json --verbose
	wantSuffix := []string{"--output-format", "stream-json", "--verbose"}
	//         --allowedTools <read-only allowlist>
	wantSuffix := []string{"--output-format", "stream-json", "--verbose", "--allowedTools", strings.Join(reviewToolAllowlist, ",")}
	if len(cmd.Args) != 3+len(wantSuffix) {
		
t.Fatalf("expected %d args, got %d: %v", 3+len(wantSuffix), len(cmd.Args), cmd.Args)
	}
297 unmodified lines

_ = ev
	}
}

// TestReviewer_ArgvIncludesReadOnlyAllowlist verifies the spawned claude
// child gets an explicit read-only tool allowlist. Headless -p mode
// auto-denies any tool call not pre-approved, so without this every finder
// that runs `git diff`/`git log` bounces off a denial and detours — slower
// reviews and findings that were never verified. The allowlist grants the
// read-only surface a review needs and nothing write-capable.
func TestReviewer_ArgvIncludesReadOnlyAllowlist(t *testing.T) {
	t.Parallel()
	cmd := buildReviewCmd(context.Background(), reviewtypes.RunConfig{Skills: []string{"/x"}})

allowed := ""
	for i, arg := range cmd.Args {
		if arg == "--allowedTools" && i+1 < len(cmd.Args) {
			allowed = cmd.Args[i+1]
		}
		if arg == "--dangerously-skip-permissions" {
			t.Fatal("reviewer child must never run with permissions disabled")
		}
	}
	if allowed == "" {
		t.Fatalf("--allowedTools missing from argv: %v", cmd.Args)
	}

members := make(map[string]bool)
	for _, m := range strings.Split(allowed, ",") {
		members[strings.TrimSpace(m)] = true
	}
	for _, want := range []string{
		"Read", "Grep", "Glob", "Task",
		"Bash(git diff:*)", "Bash(git log:*)", "Bash(git show:*)",
		"Bash(git status:*)", "Bash(git blame:*)", "Bash(git rev-parse:*)",
	} {
		if !members[want] {
			t.Errorf("allowlist missing %q; got %q", want, allowed)
		}
	}
	for _, banned := range []string{"Edit", "Write", "MultiEdit", "NotebookEdit", "Bash", "Bash(git:*)"} {
		if members[banned] {
			t.Errorf("allowlist must not grant write-capable or blanket tool %q; got %q", banned, allowed)
		}
	}
}
