From 77c3b3cdd342f7bff7038338ee4f228da2ee60ea Mon Sep 17 00:00:00 2001 From: Bruno Bornsztein Date: Fri, 5 Jun 2026 15:46:43 -0500 Subject: [PATCH] Disambiguate "auto" permission mode from Claude Code's auto mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TaskYou's "auto" permission set predates Claude Code's own "auto mode" (the agentic flow gated by --enable-auto-mode). Our "auto" actually maps to Claude's acceptEdits (--permission-mode acceptEdits): auto-accept file edits while still prompting for risky actions. Reusing the word "auto" for that made agents and users conflate it with CC's separate auto mode — the 4-permission-set confusion Kyle reported. Surface this mode everywhere as "accept edits" / "accept-edits" instead: - Settings form, detail badge (AUTO -> ACCEPT EDITS), and queue/confirm prompts now read "accept edits". - CLI --permission-mode help + notifications and the MCP permission_mode schema describe it as accept-edits and explicitly note it is NOT --enable-auto-mode; "auto" is documented as a legacy alias. - NormalizePermissionMode now accepts "accept-edits"/"acceptEdits" (and is case/space-insensitive), all mapping to the canonical "auto" value, so stored rows, old CLI calls, and scripts keep working with no migration. Code comments spell out why the stored value stays "auto" for back-compat. Co-Authored-By: Claude Opus 4.8 --- cmd/task/main.go | 12 ++++---- internal/db/permission_mode_test.go | 16 ++++++---- internal/db/tasks.go | 37 +++++++++++++++++------ internal/executor/claude_executor.go | 2 +- internal/executor/permission_flag_test.go | 1 + internal/mcp/server.go | 4 +-- internal/ui/app.go | 2 +- internal/ui/detail.go | 6 ++-- internal/ui/kanban.go | 2 +- internal/ui/settings.go | 8 ++--- 10 files changed, 58 insertions(+), 32 deletions(-) diff --git a/cmd/task/main.go b/cmd/task/main.go index d91c303a..196d85a5 100644 --- a/cmd/task/main.go +++ b/cmd/task/main.go @@ -746,7 +746,7 @@ Examples: createCmd.Flags().String("effort", "", "Per-task Claude effort override: low, medium, high, xhigh, max (default: Claude's global default)") createCmd.Flags().BoolP("execute", "x", false, "Queue task for immediate execution") createCmd.Flags().Bool("dangerous", false, "Execute in dangerous mode (alias for --permission-mode dangerous)") - createCmd.Flags().String("permission-mode", "", "Permission mode: default (prompt), auto (auto-accept edits), dangerous (skip all). Defaults to the project's setting") + createCmd.Flags().String("permission-mode", "", "Permission mode: default (prompt), accept-edits (auto-accept file edits, still prompt for risky actions; alias: auto), dangerous (skip all). Defaults to the project's setting") createCmd.Flags().String("tags", "", "Task tags (comma-separated)") createCmd.Flags().Bool("pinned", false, "Pin the task to the top of its column") createCmd.Flags().StringP("branch", "b", "", "Existing branch to checkout for worktree (e.g., fix/ui-overflow)") @@ -1602,13 +1602,13 @@ Examples: case db.PermissionModeDangerous: msg += " (dangerous mode)" case db.PermissionModeAuto: - msg += " (auto mode)" + msg += " (accept-edits mode)" } fmt.Println(successStyle.Render(msg)) }, } executeCmd.Flags().Bool("dangerous", false, "Execute in dangerous mode (alias for --permission-mode dangerous)") - executeCmd.Flags().String("permission-mode", "", "Override permission mode: default (prompt), auto (auto-accept edits), dangerous (skip all)") + executeCmd.Flags().String("permission-mode", "", "Override permission mode: default (prompt), accept-edits (auto-accept file edits; alias: auto), dangerous (skip all)") rootCmd.AddCommand(executeCmd) statusCmd := &cobra.Command{ @@ -2532,7 +2532,7 @@ Examples: projectsCreateCmd.Flags().StringP("color", "c", "", "Hex color for display (e.g., #61AFEF)") projectsCreateCmd.Flags().StringP("aliases", "a", "", "Comma-separated aliases for lookup") projectsCreateCmd.Flags().String("claude-config-dir", "", "Override CLAUDE_CONFIG_DIR for this project") - projectsCreateCmd.Flags().String("permission-mode", "", "Default permission mode for tasks: default (prompt), auto (auto-accept edits), dangerous (skip all)") + projectsCreateCmd.Flags().String("permission-mode", "", "Default permission mode for tasks: default (prompt), accept-edits (auto-accept file edits; alias: auto), dangerous (skip all)") projectsCreateCmd.Flags().Bool("no-git", false, "Disable git worktrees (for non-git projects)") projectsCreateCmd.Flags().Bool("json", false, "Output in JSON format") projectsCreateCmd.MarkFlagRequired("path") @@ -2590,7 +2590,7 @@ Examples: projectsUpdateCmd.Flags().StringP("aliases", "a", "", "Comma-separated aliases for lookup") projectsUpdateCmd.Flags().String("claude-config-dir", "", "Override CLAUDE_CONFIG_DIR for this project") projectsUpdateCmd.Flags().String("context", "", "Cached project context summary") - projectsUpdateCmd.Flags().String("permission-mode", "", "Default permission mode for tasks: default (prompt), auto (auto-accept edits), dangerous (skip all)") + projectsUpdateCmd.Flags().String("permission-mode", "", "Default permission mode for tasks: default (prompt), accept-edits (auto-accept file edits; alias: auto), dangerous (skip all)") projectsUpdateCmd.Flags().Bool("no-git", false, "Disable git worktrees (for non-git projects)") projectsUpdateCmd.Flags().Bool("git", false, "Enable git worktrees (default)") projectsUpdateCmd.Flags().Bool("json", false, "Output in JSON format") @@ -5700,7 +5700,7 @@ func updateProjectCLI(currentName, newName, path, instructions, color, aliases, if permissionMode != "" { normalized := db.NormalizePermissionMode(permissionMode) if normalized == "" { - fmt.Fprintln(os.Stderr, errorStyle.Render("Error: invalid permission mode (use default, auto, or dangerous)")) + fmt.Fprintln(os.Stderr, errorStyle.Render("Error: invalid permission mode (use default, accept-edits, or dangerous)")) os.Exit(1) } project.DefaultPermissionMode = normalized diff --git a/internal/db/permission_mode_test.go b/internal/db/permission_mode_test.go index 1f7bbda3..4343ca3e 100644 --- a/internal/db/permission_mode_test.go +++ b/internal/db/permission_mode_test.go @@ -8,12 +8,16 @@ import ( func TestNormalizePermissionMode(t *testing.T) { cases := map[string]string{ - "auto": PermissionModeAuto, - "dangerous": PermissionModeDangerous, - "default": PermissionModeDefault, - "prompt": PermissionModeDefault, - "": "", - "bogus": "", + "auto": PermissionModeAuto, // legacy alias, kept for back-compat + "accept-edits": PermissionModeAuto, // unambiguous spelling normalizes to the canonical value + "accept_edits": PermissionModeAuto, + "acceptEdits": PermissionModeAuto, // Claude Code's own spelling + " Auto ": PermissionModeAuto, // trimmed + case-insensitive + "dangerous": PermissionModeDangerous, + "default": PermissionModeDefault, + "prompt": PermissionModeDefault, + "": "", + "bogus": "", } for in, want := range cases { if got := NormalizePermissionMode(in); got != want { diff --git a/internal/db/tasks.go b/internal/db/tasks.go index f21a30e5..e89c4366 100644 --- a/internal/db/tasks.go +++ b/internal/db/tasks.go @@ -32,7 +32,7 @@ type Task struct { PRNumber int // Pull request number (if associated with a PR) PRInfoJSON string // Cached PR state as JSON (state, checks, mergeable, etc.) DangerousMode bool // Whether task is running in dangerous mode (--dangerously-skip-permissions). Kept for backward compat; PermissionMode is authoritative. - PermissionMode string // Permission mode for execution: "default" (prompt), "auto" (acceptEdits), "dangerous" (skip permissions). Empty falls back to DangerousMode/global default. + PermissionMode string // Permission mode for execution: "default" (prompt), "auto"/"accept-edits" (Claude's acceptEdits — auto-accept file edits, still prompts for risky actions), "dangerous" (skip permissions). Empty falls back to DangerousMode/global default. Pinned bool // Whether the task is pinned to the top of its column Tags string // Comma-separated tags for categorization (e.g., "customer-support,email,influence-kit") SourceBranch string // Existing branch to checkout for worktree (e.g., "fix/ui-overflow") instead of creating new branch @@ -68,24 +68,43 @@ func IsInProgress(status string) bool { } // Permission modes control how the underlying agent handles permission prompts. +// +// IMPORTANT: the stored value "auto" is historical — it predates Claude Code's +// own "auto mode" (the agentic permission flow gated by --enable-auto-mode). +// In TaskYou, "auto" means Claude Code's ACCEPT-EDITS mode (--permission-mode +// acceptEdits): auto-accept file edits while still prompting for risky actions. +// It is NOT Claude Code's --enable-auto-mode. To avoid that overloaded word, +// surface this mode to users and agents as "accept edits" / "accept-edits"; +// the "auto" value is kept only for back-compat (and accepted as an alias). const ( // PermissionModeDefault prompts for permissions (the historical default). PermissionModeDefault = "default" - // PermissionModeAuto auto-accepts file edits but still gates risky actions - // (Claude's --permission-mode acceptEdits). This is the "auto mode" most - // users want: handles ~99% of permission prompts without the risk of - // fully bypassing permissions. + // PermissionModeAuto maps to Claude Code's accept-edits mode + // (--permission-mode acceptEdits): auto-accept file edits but still gate + // risky actions. This is the low-friction mode most users want — it handles + // ~99% of permission prompts without fully bypassing permissions. Despite + // the stored "auto" value, this is NOT Claude Code's --enable-auto-mode; + // display it as "accept edits" (see PermissionModeAutoLabel). PermissionModeAuto = "auto" // PermissionModeDangerous bypasses all permission checks // (Claude's --dangerously-skip-permissions). PermissionModeDangerous = "dangerous" ) +// PermissionModeAutoLabel is the unambiguous human-facing name for +// PermissionModeAuto. We deliberately avoid the word "auto" in UI and prompts +// because it collides with Claude Code's separate "auto mode" +// (--enable-auto-mode), which TaskYou does not currently expose. +const PermissionModeAutoLabel = "accept-edits" + // NormalizePermissionMode coerces a raw value into a known permission mode. -// "prompt" and "" are treated as default; unknown values return "". +// "prompt" and "" are treated as default. The unambiguous "accept-edits" name +// (and its variants, plus Claude's own "acceptEdits") all map to the canonical +// PermissionModeAuto value, so callers can use the clearer spelling while old +// "auto" values keep working. Unknown values return "". func NormalizePermissionMode(mode string) string { - switch mode { - case PermissionModeAuto: + switch strings.ToLower(strings.TrimSpace(mode)) { + case PermissionModeAuto, PermissionModeAutoLabel, "accept_edits", "acceptedits": return PermissionModeAuto case PermissionModeDangerous: return PermissionModeDangerous @@ -124,7 +143,7 @@ func (t *Task) IsDangerous() bool { return t.EffectivePermissionMode() == PermissionModeDangerous } -// IsAutoPermission reports whether the task runs in auto (acceptEdits) mode. +// IsAutoPermission reports whether the task runs in accept-edits (acceptEdits) mode. func (t *Task) IsAutoPermission() bool { return t.EffectivePermissionMode() == PermissionModeAuto } diff --git a/internal/executor/claude_executor.go b/internal/executor/claude_executor.go index 63daca0c..fe44bc89 100644 --- a/internal/executor/claude_executor.go +++ b/internal/executor/claude_executor.go @@ -104,7 +104,7 @@ func (c *ClaudeExecutor) ResumeProcess(taskID int64) bool { // BuildCommand returns the shell command to start an interactive Claude session. func (c *ClaudeExecutor) BuildCommand(task *db.Task, sessionID, prompt string) string { - // Build permission mode flag (dangerous, auto/acceptEdits, or none) + // Build permission mode flag (dangerous, accept-edits/acceptEdits, or none) dangerousFlag := claudePermissionFlag(task) // Build per-task effort override flag (empty = use Claude's global default) diff --git a/internal/executor/permission_flag_test.go b/internal/executor/permission_flag_test.go index 3349116f..b27f9f6b 100644 --- a/internal/executor/permission_flag_test.go +++ b/internal/executor/permission_flag_test.go @@ -18,6 +18,7 @@ func TestClaudePermissionFlag(t *testing.T) { {"default empty", &db.Task{}, ""}, {"explicit default", &db.Task{PermissionMode: db.PermissionModeDefault}, ""}, {"auto", &db.Task{PermissionMode: db.PermissionModeAuto}, "--permission-mode acceptEdits "}, + {"accept-edits alias", &db.Task{PermissionMode: "accept-edits"}, "--permission-mode acceptEdits "}, {"dangerous", &db.Task{PermissionMode: db.PermissionModeDangerous}, "--dangerously-skip-permissions "}, {"legacy dangerous bool", &db.Task{DangerousMode: true}, "--dangerously-skip-permissions "}, } diff --git a/internal/mcp/server.go b/internal/mcp/server.go index 655785a6..fda076c7 100644 --- a/internal/mcp/server.go +++ b/internal/mcp/server.go @@ -242,8 +242,8 @@ func (s *Server) handleRequest(req *jsonRPCRequest) { }, "permission_mode": map[string]interface{}{ "type": "string", - "description": "Permission mode for execution: 'default' (prompt), 'auto' (auto-accept edits), or 'dangerous' (skip all prompts). Defaults to the project's configured default.", - "enum": []string{"default", "auto", "dangerous"}, + "description": "Permission mode for execution: 'default' (prompt for each permission), 'accept-edits' (Claude Code's acceptEdits / --permission-mode acceptEdits: auto-accept file edits but still prompt for risky actions — this is NOT Claude Code's separate 'auto mode' from --enable-auto-mode), or 'dangerous' (skip all prompts / --dangerously-skip-permissions). The legacy value 'auto' is still accepted and means the same as 'accept-edits'. Defaults to the project's configured default.", + "enum": []string{"default", "accept-edits", "dangerous"}, }, }, "required": []string{"title"}, diff --git a/internal/ui/app.go b/internal/ui/app.go index 72a8d214..dda0e0c7 100644 --- a/internal/ui/app.go +++ b/internal/ui/app.go @@ -2900,7 +2900,7 @@ func (m *AppModel) updateNewTaskForm(msg tea.Msg) (tea.Model, tea.Cmd) { Options( huh.NewOption("No — save to backlog", "no"), huh.NewOption("Yes — execute now", "yes"), - huh.NewOption("Yes — execute in auto mode", "auto"), + huh.NewOption("Yes — execute in accept-edits mode", "auto"), huh.NewOption("Yes — execute in dangerous mode", "dangerous"), ). Value(&m.queueValue), diff --git a/internal/ui/detail.go b/internal/ui/detail.go index 34316335..b04afa9a 100644 --- a/internal/ui/detail.go +++ b/internal/ui/detail.go @@ -2358,7 +2358,9 @@ func (m *DetailModel) renderHeader() string { meta.WriteString(" ") } - // Auto mode badge (acceptEdits) for active tasks. + // Accept-edits badge (Claude's acceptEdits mode) for active tasks. Labeled + // "ACCEPT EDITS" rather than "AUTO" so it isn't confused with Claude Code's + // separate auto mode (--enable-auto-mode). if t.IsAutoPermission() && (t.Status == db.StatusProcessing || t.Status == db.StatusBlocked) { var autoStyle lipgloss.Style if m.focused { @@ -2373,7 +2375,7 @@ func (m *DetailModel) renderHeader() string { Background(dimmedBg). Foreground(dimmedFg) } - meta.WriteString(autoStyle.Render("AUTO")) + meta.WriteString(autoStyle.Render("ACCEPT EDITS")) meta.WriteString(" ") } diff --git a/internal/ui/kanban.go b/internal/ui/kanban.go index 2c477047..1af324e4 100644 --- a/internal/ui/kanban.go +++ b/internal/ui/kanban.go @@ -1165,7 +1165,7 @@ func (k *KanbanBoard) renderTaskCard(task *db.Task, width int, isSelected bool, indicators = append(indicators, dangerStyle.Render("●")) } } - // Auto mode indicator (green dot) for active tasks running in auto/acceptEdits mode. + // Accept-edits indicator (green dot) for active tasks running in Claude's acceptEdits mode. if task.IsAutoPermission() && (task.Status == db.StatusProcessing || task.Status == db.StatusBlocked) { if isSelected { indicators = append(indicators, "●") diff --git a/internal/ui/settings.go b/internal/ui/settings.go index 54eef64f..8b4f96cc 100644 --- a/internal/ui/settings.go +++ b/internal/ui/settings.go @@ -278,8 +278,8 @@ func (m *SettingsModel) showProjectForm(project *db.Project) (*SettingsModel, te m.projectFormUseWorktrees = project.UseWorktrees // Default permission mode. Use the project's explicit setting when present, - // otherwise pre-select the effective default (auto) so the form mirrors the - // mode tasks will actually run in. + // otherwise pre-select the effective default (accept-edits) so the form + // mirrors the mode tasks will actually run in. m.projectFormPermissionMode = db.NormalizePermissionMode(project.DefaultPermissionMode) if m.projectFormPermissionMode == "" { m.projectFormPermissionMode = project.EffectiveDefaultPermissionMode() @@ -342,9 +342,9 @@ func (m *SettingsModel) showProjectForm(project *db.Project) (*SettingsModel, te huh.NewSelect[string](). Key("permission_mode"). Title("Default Permission Mode"). - Description("How new tasks handle permissions. Auto handles ~99% without prompting."). + Description("How new tasks handle permissions. Accept Edits handles ~99% without prompting."). Options( - huh.NewOption("Auto — auto-accept edits (recommended)", db.PermissionModeAuto), + huh.NewOption("Accept Edits — auto-accept file edits, still prompt for risky actions (recommended)", db.PermissionModeAuto), huh.NewOption("Prompt — ask for each permission", db.PermissionModeDefault), huh.NewOption("Dangerous — skip all permission checks", db.PermissionModeDangerous), ).