From ceab611528f47c477886353a57a6e29432932ee4 Mon Sep 17 00:00:00 2001 From: Bruno Bornsztein Date: Thu, 22 Jan 2026 18:07:15 -0600 Subject: [PATCH] fix: use relative paths for task attachments to match permission patterns The permission pattern Read(.claude/attachments/**) uses relative paths, but attachments were being listed with absolute filesystem paths in the prompt. This caused Claude to be unable to read attachment files without permission prompts, since the absolute paths didn't match the relative permission pattern. Changed getAttachmentsSection to convert absolute paths to relative paths by stripping the worktree prefix. This ensures the paths in the prompt (like .claude/attachments/task-123/file.txt) match the pre-approved permission pattern. Co-Authored-By: Claude Opus 4.5 --- internal/executor/executor.go | 18 +++++++++++++----- internal/executor/executor_test.go | 25 ++++++++++++++++++++----- 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/internal/executor/executor.go b/internal/executor/executor.go index 3d6c4487..c516b22f 100644 --- a/internal/executor/executor.go +++ b/internal/executor/executor.go @@ -915,7 +915,7 @@ func (e *Executor) executeTask(ctx context.Context, task *db.Task) { // This is important when attachments are added after the initial run or when resuming feedbackWithAttachments := retryFeedback if len(attachmentPaths) > 0 { - feedbackWithAttachments = retryFeedback + "\n" + e.getAttachmentsSection(task.ID, attachmentPaths) + feedbackWithAttachments = retryFeedback + "\n" + e.getAttachmentsSection(task.ID, attachmentPaths, workDir) } e.logLine(task.ID, "system", fmt.Sprintf("Resuming previous session with feedback (executor: %s)", executorName)) execResult := taskExecutor.Resume(taskCtx, task, workDir, prompt, feedbackWithAttachments) @@ -1094,7 +1094,9 @@ func (e *Executor) prepareAttachments(taskID int64, worktreePath string) ([]stri } // getAttachmentsSection returns a prompt section describing attachments. -func (e *Executor) getAttachmentsSection(taskID int64, paths []string) string { +// The worktreePath parameter is used to convert absolute paths to relative paths +// so they match the permission pattern Read(.claude/attachments/**). +func (e *Executor) getAttachmentsSection(taskID int64, paths []string, worktreePath string) string { if len(paths) == 0 { return "" } @@ -1103,7 +1105,13 @@ func (e *Executor) getAttachmentsSection(taskID int64, paths []string) string { section.WriteString("\n## Attachments\n\n") section.WriteString("The following files are attached to this task:\n") for _, p := range paths { - section.WriteString(fmt.Sprintf("- %s\n", p)) + // Convert absolute paths to relative paths so they match permission patterns + relPath := p + if worktreePath != "" && strings.HasPrefix(p, worktreePath) { + relPath = strings.TrimPrefix(p, worktreePath) + relPath = strings.TrimPrefix(relPath, string(filepath.Separator)) + } + section.WriteString(fmt.Sprintf("- %s\n", relPath)) } section.WriteString("\nYou can read these files using the Read tool.\n\n") return section.String() @@ -1134,8 +1142,8 @@ func (e *Executor) buildPrompt(task *db.Task, attachmentPaths []string) string { // Check for conversation history (from previous runs/retries) conversationHistory := e.getConversationHistory(task.ID) - // Get attachments section - attachments := e.getAttachmentsSection(task.ID, attachmentPaths) + // Get attachments section (use relative paths to match permission patterns) + attachments := e.getAttachmentsSection(task.ID, attachmentPaths, task.WorktreePath) // Look up task type instructions from database if task.Type != "" { diff --git a/internal/executor/executor_test.go b/internal/executor/executor_test.go index 80d2a2a0..5a500f30 100644 --- a/internal/executor/executor_test.go +++ b/internal/executor/executor_test.go @@ -265,9 +265,10 @@ func TestAttachmentsInPrompt(t *testing.T) { } }) - t.Run("getAttachmentsSection uses Read tool", func(t *testing.T) { + t.Run("getAttachmentsSection uses Read tool with relative paths", func(t *testing.T) { + worktreePath := "/worktree" paths := []string{"/worktree/.claude/attachments/notes.txt", "/worktree/.claude/attachments/data.json"} - section := exec.getAttachmentsSection(task.ID, paths) + section := exec.getAttachmentsSection(task.ID, paths, worktreePath) if !strings.Contains(section, "## Attachments") { t.Error("section should contain Attachments header") @@ -278,13 +279,19 @@ func TestAttachmentsInPrompt(t *testing.T) { if strings.Contains(section, "View tool") { t.Error("section should NOT mention View tool") } - if !strings.Contains(section, "/worktree/.claude/attachments/notes.txt") { - t.Error("section should contain file path") + // Paths should be relative, not absolute + if !strings.Contains(section, ".claude/attachments/notes.txt") { + t.Error("section should contain relative file path") + } + if strings.Contains(section, "/worktree/.claude/attachments/notes.txt") { + t.Error("section should NOT contain absolute file path") } }) t.Run("buildPrompt includes attachments section", func(t *testing.T) { worktreePath := t.TempDir() + // Set task's WorktreePath so buildPrompt can convert to relative paths + task.WorktreePath = worktreePath paths, cleanup := exec.prepareAttachments(task.ID, worktreePath) defer cleanup() @@ -296,6 +303,10 @@ func TestAttachmentsInPrompt(t *testing.T) { if !strings.Contains(prompt, "Read tool") { t.Error("prompt should mention Read tool") } + // Verify paths are relative in the prompt + if !strings.Contains(prompt, ".claude/attachments/") { + t.Error("prompt should contain relative attachment paths") + } }) t.Run("retry feedback includes attachments when present", func(t *testing.T) { @@ -307,7 +318,7 @@ func TestAttachmentsInPrompt(t *testing.T) { retryFeedback := "Please fix the bug" feedbackWithAttachments := retryFeedback if len(paths) > 0 { - feedbackWithAttachments = retryFeedback + "\n" + exec.getAttachmentsSection(task.ID, paths) + feedbackWithAttachments = retryFeedback + "\n" + exec.getAttachmentsSection(task.ID, paths, worktreePath) } // Verify attachments are included in the retry feedback @@ -320,6 +331,10 @@ func TestAttachmentsInPrompt(t *testing.T) { if !strings.Contains(feedbackWithAttachments, "Please fix the bug") { t.Error("retry feedback should still contain original feedback") } + // Verify paths are relative + if !strings.Contains(feedbackWithAttachments, ".claude/attachments/") { + t.Error("retry feedback should contain relative attachment paths") + } }) }