Skip to content

Commit 8a70655

Browse files
davidslaterGitHub Ace
andauthored
fix(engine): grant Read tool and acceptEdits permission mode to Claude (#895)
The Claude spawn only granted Bash, Write, and Edit via --allowed-tools and never passed --permission-mode, so Claude defaulted to interactive permission mode. Detection artifacts live under /tmp/gh-aw/threat-detection/, outside Claude's working directory, so Read requests there were denied with no user available to grant consent. Sonnet-class models silently recover via `Bash cat`, masking the bug, but Haiku (the default `detection` model alias) exhausts its retries and never records a verdict. Add --permission-mode acceptEdits and grant Read, Read(/tmp/*), and Read(/tmp/gh-aw/*) alongside the existing tools, matching gh-aw's own Claude engine invocation. Co-authored-by: GitHub Ace <githubnext@users.noreply.github.com> Co-authored-by: David Slater <12449447+davidslater@users.noreply.github.com>
1 parent 10d8900 commit 8a70655

2 files changed

Lines changed: 23 additions & 7 deletions

File tree

pkg/engine/engine.go

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -331,13 +331,28 @@ func copilotHarnessAwfConfigEnvAt(harnessDefaultPath string) []string {
331331
// or a --reason argument — the exact shell-expansion surface the file transport
332332
// exists to remove — so the tool grant and the prompt must stay in step.
333333
//
334-
// This grants no capability beyond Bash, which can already write files.
335-
var resultToolClaudeTools = []string{"Bash", "Write", "Edit"}
334+
// Read, Read(/tmp/*), and Read(/tmp/gh-aw/*) let the model load the artifact
335+
// files the prompt tells it to analyze (workflow prompt file, agent output,
336+
// patch/bundle, comment memory) directly, without falling back to `Bash cat`.
337+
// Those artifacts live under /tmp/gh-aw/threat-detection/, outside the
338+
// process's working directory, so plain "Read" alone is not always sufficient
339+
// under Claude's workingDir sandbox; the path-scoped grants cover that case.
340+
//
341+
// This grants no capability beyond Bash, which can already read and write files.
342+
var resultToolClaudeTools = []string{"Bash", "Write", "Edit", "Read", "Read(/tmp/*)", "Read(/tmp/gh-aw/*)"}
343+
344+
// claudePermissionMode is passed via --permission-mode when the result-tool
345+
// grant is active. Claude Code defaults to interactive permission prompting,
346+
// which has no user to respond in a CI run and denies tool use (including
347+
// Read outside the working directory) after a few attempts. acceptEdits
348+
// makes --allowed-tools the effective boundary instead, matching gh-aw's own
349+
// Claude engine invocation (pkg/workflow/claude_engine.go).
350+
const claudePermissionMode = "acceptEdits"
336351

337352
func claudeArgs(model string, allowResultTool bool) []string {
338353
args := []string{"--print", "--verbose", "--output-format", "stream-json"}
339354
if allowResultTool {
340-
args = append(args, "--allowed-tools", strings.Join(resultToolClaudeTools, ","))
355+
args = append(args, "--permission-mode", claudePermissionMode, "--allowed-tools", strings.Join(resultToolClaudeTools, ","))
341356
}
342357
if model != "" {
343358
args = append(args, "--model", model)
@@ -352,7 +367,7 @@ func claudeArgs(model string, allowResultTool bool) []string {
352367
func claudeHarnessArgs(promptPath, model string, allowResultTool bool) []string {
353368
args := []string{"--print", "--verbose", "--output-format", "stream-json"}
354369
if allowResultTool {
355-
args = append(args, "--allowed-tools", strings.Join(resultToolClaudeTools, ","))
370+
args = append(args, "--permission-mode", claudePermissionMode, "--allowed-tools", strings.Join(resultToolClaudeTools, ","))
356371
}
357372
if model != "" {
358373
args = append(args, "--model", model)

pkg/engine/engine_test.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -248,15 +248,15 @@ func TestEngineCommandArgs(t *testing.T) {
248248

249249
t.Run("claude with result-tool grant", func(t *testing.T) {
250250
got := claudeArgs("claude-sonnet-4.6", true)
251-
want := []string{"--print", "--verbose", "--output-format", "stream-json", "--allowed-tools", "Bash,Write,Edit", "--model", "claude-sonnet-4.6", "-"}
251+
want := []string{"--print", "--verbose", "--output-format", "stream-json", "--permission-mode", "acceptEdits", "--allowed-tools", "Bash,Write,Edit,Read,Read(/tmp/*),Read(/tmp/gh-aw/*)", "--model", "claude-sonnet-4.6", "-"}
252252
if !reflect.DeepEqual(got, want) {
253253
t.Fatalf("claudeArgs() = %#v, want %#v", got, want)
254254
}
255255
})
256256

257257
t.Run("claude harness args", func(t *testing.T) {
258258
got := claudeHarnessArgs("/tmp/prompt.txt", "claude-sonnet-4.6", true)
259-
want := []string{"--print", "--verbose", "--output-format", "stream-json", "--allowed-tools", "Bash,Write,Edit", "--model", "claude-sonnet-4.6", "--prompt-file", "/tmp/prompt.txt"}
259+
want := []string{"--print", "--verbose", "--output-format", "stream-json", "--permission-mode", "acceptEdits", "--allowed-tools", "Bash,Write,Edit,Read,Read(/tmp/*),Read(/tmp/gh-aw/*)", "--model", "claude-sonnet-4.6", "--prompt-file", "/tmp/prompt.txt"}
260260
if !reflect.DeepEqual(got, want) {
261261
t.Fatalf("claudeHarnessArgs() = %#v, want %#v", got, want)
262262
}
@@ -279,7 +279,8 @@ func TestEngineCommandArgs(t *testing.T) {
279279
wantArgs := []string{
280280
harnessPath, "claude",
281281
"--print", "--verbose", "--output-format", "stream-json",
282-
"--allowed-tools", "Bash,Write,Edit",
282+
"--permission-mode", "acceptEdits",
283+
"--allowed-tools", "Bash,Write,Edit,Read,Read(/tmp/*),Read(/tmp/gh-aw/*)",
283284
"--model", "claude-sonnet-4.6",
284285
"--prompt-file", "/tmp/prompt.txt",
285286
}

0 commit comments

Comments
 (0)