diff --git a/internal/session/instance.go b/internal/session/instance.go index 4d74c9892..a67ecc485 100644 --- a/internal/session/instance.go +++ b/internal/session/instance.go @@ -632,6 +632,8 @@ func (i *Instance) buildClaudeExtraFlags(opts *ClaudeOptions) string { if opts != nil { if opts.SkipPermissions { flags = append(flags, "--dangerously-skip-permissions") + } else if opts.AutoMode { + flags = append(flags, "--permission-mode auto") } else if opts.AllowSkipPermissions { flags = append(flags, "--allow-dangerously-skip-permissions") } @@ -4080,6 +4082,7 @@ func (i *Instance) buildClaudeResumeCommand() string { opts = NewClaudeOptions(userConfig) } dangerousMode := opts.SkipPermissions + autoMode := opts.AutoMode allowDangerousMode := opts.AllowSkipPermissions // Check if session has actual conversation data @@ -4092,10 +4095,12 @@ func (i *Instance) buildClaudeResumeCommand() string { slog.Bool("use_resume", useResume), ) - // Build dangerous mode flag (--dangerously-skip-permissions wins over --allow-...) + // Build permission flag (--dangerously-skip-permissions wins over --permission-mode auto wins over --allow-...) dangerousFlag := "" if dangerousMode { dangerousFlag = " --dangerously-skip-permissions" + } else if autoMode { + dangerousFlag = " --permission-mode auto" } else if allowDangerousMode { dangerousFlag = " --allow-dangerously-skip-permissions" } diff --git a/internal/session/tooloptions.go b/internal/session/tooloptions.go index f64c017db..b12d48be5 100644 --- a/internal/session/tooloptions.go +++ b/internal/session/tooloptions.go @@ -25,6 +25,10 @@ type ClaudeOptions struct { // AllowSkipPermissions adds --allow-dangerously-skip-permissions flag // Only used when SkipPermissions is false (SkipPermissions takes precedence) AllowSkipPermissions bool `json:"allow_skip_permissions,omitempty"` + // AutoMode adds --permission-mode auto flag + // Uses a classifier model to auto-approve safe operations while blocking risky ones. + // Only used when SkipPermissions is false (SkipPermissions takes precedence). + AutoMode bool `json:"auto_mode,omitempty"` // UseChrome adds --chrome flag UseChrome bool `json:"use_chrome,omitempty"` // UseTeammateMode adds --teammate-mode tmux flag @@ -60,6 +64,8 @@ func (o *ClaudeOptions) ToArgs() []string { // Permission flags (mutually exclusive, SkipPermissions takes precedence) if o.SkipPermissions { args = append(args, "--dangerously-skip-permissions") + } else if o.AutoMode { + args = append(args, "--permission-mode", "auto") } else if o.AllowSkipPermissions { args = append(args, "--allow-dangerously-skip-permissions") } @@ -80,6 +86,8 @@ func (o *ClaudeOptions) ToArgsForFork() []string { if o.SkipPermissions { args = append(args, "--dangerously-skip-permissions") + } else if o.AutoMode { + args = append(args, "--permission-mode", "auto") } else if o.AllowSkipPermissions { args = append(args, "--allow-dangerously-skip-permissions") } @@ -100,6 +108,7 @@ func NewClaudeOptions(config *UserConfig) *ClaudeOptions { } if config != nil { opts.SkipPermissions = config.Claude.GetDangerousMode() + opts.AutoMode = config.Claude.AutoMode opts.AllowSkipPermissions = config.Claude.AllowDangerousMode } return opts diff --git a/internal/session/tooloptions_test.go b/internal/session/tooloptions_test.go index c26d57b18..01234ffbf 100644 --- a/internal/session/tooloptions_test.go +++ b/internal/session/tooloptions_test.go @@ -99,6 +99,29 @@ func TestClaudeOptions_ToArgs(t *testing.T) { }, expected: []string{"--dangerously-skip-permissions"}, }, + { + name: "auto mode only", + opts: ClaudeOptions{ + AutoMode: true, + }, + expected: []string{"--permission-mode", "auto"}, + }, + { + name: "skip permissions takes precedence over auto mode", + opts: ClaudeOptions{ + SkipPermissions: true, + AutoMode: true, + }, + expected: []string{"--dangerously-skip-permissions"}, + }, + { + name: "auto mode takes precedence over allow skip permissions", + opts: ClaudeOptions{ + AutoMode: true, + AllowSkipPermissions: true, + }, + expected: []string{"--permission-mode", "auto"}, + }, } for _, tt := range tests { @@ -174,6 +197,21 @@ func TestClaudeOptions_ToArgsForFork(t *testing.T) { }, expected: []string{"--dangerously-skip-permissions"}, }, + { + name: "auto mode for fork", + opts: ClaudeOptions{ + AutoMode: true, + }, + expected: []string{"--permission-mode", "auto"}, + }, + { + name: "skip permissions takes precedence over auto mode for fork", + opts: ClaudeOptions{ + SkipPermissions: true, + AutoMode: true, + }, + expected: []string{"--dangerously-skip-permissions"}, + }, } for _, tt := range tests { @@ -205,6 +243,25 @@ func TestNewClaudeOptions_WithConfig(t *testing.T) { } } +func TestNewClaudeOptions_AutoMode(t *testing.T) { + dangerousModeFalse := false + config := &UserConfig{ + Claude: ClaudeSettings{ + DangerousMode: &dangerousModeFalse, + AutoMode: true, + }, + } + + opts := NewClaudeOptions(config) + + if opts.SkipPermissions { + t.Error("expected SkipPermissions=false when dangerous_mode=false") + } + if !opts.AutoMode { + t.Error("expected AutoMode=true when auto_mode=true") + } +} + func TestNewClaudeOptions_NilConfig(t *testing.T) { opts := NewClaudeOptions(nil) diff --git a/internal/session/userconfig.go b/internal/session/userconfig.go index 6da112555..c34f2044a 100644 --- a/internal/session/userconfig.go +++ b/internal/session/userconfig.go @@ -543,6 +543,13 @@ type ClaudeSettings struct { // Default: false AllowDangerousMode bool `toml:"allow_dangerous_mode"` + // AutoMode enables --permission-mode auto flag for Claude sessions + // A classifier model reviews commands before they run, blocking scope escalation + // and hostile-content-driven actions while letting routine work proceed without prompts. + // Ignored when dangerous_mode is true (the stronger flag takes precedence). + // Default: false + AutoMode bool `toml:"auto_mode"` + // EnvFile is a .env file specific to Claude sessions // Sourced AFTER global [shell].env_files // Path can be absolute, ~ for home, $HOME/${VAR} for env vars, or relative to session working directory diff --git a/internal/ui/claudeoptions.go b/internal/ui/claudeoptions.go index 3ebcc8f40..87f4eb0af 100644 --- a/internal/ui/claudeoptions.go +++ b/internal/ui/claudeoptions.go @@ -17,6 +17,7 @@ type ClaudeOptionsPanel struct { // Checkbox states skipPermissions bool allowSkipPermissions bool + autoMode bool useChrome bool useTeammateMode bool // Focus tracking @@ -67,6 +68,7 @@ func (p *ClaudeOptionsPanel) SetDefaults(config *session.UserConfig) { if config != nil { p.skipPermissions = config.Claude.GetDangerousMode() p.allowSkipPermissions = config.Claude.AllowDangerousMode + p.autoMode = config.Claude.AutoMode } } @@ -86,6 +88,7 @@ func (p *ClaudeOptionsPanel) SetFromOptions(opts *session.ClaudeOptions) { } p.skipPermissions = opts.SkipPermissions p.allowSkipPermissions = opts.AllowSkipPermissions + p.autoMode = opts.AutoMode p.useChrome = opts.UseChrome p.useTeammateMode = opts.UseTeammateMode p.updateInputFocus() @@ -119,6 +122,7 @@ func (p *ClaudeOptionsPanel) GetOptions() *session.ClaudeOptions { opts := &session.ClaudeOptions{ SkipPermissions: p.skipPermissions, AllowSkipPermissions: p.allowSkipPermissions, + AutoMode: p.autoMode, UseChrome: p.useChrome, UseTeammateMode: p.useTeammateMode, } @@ -209,8 +213,10 @@ func (p *ClaudeOptionsPanel) handleSpaceKey() { case 0: p.skipPermissions = !p.skipPermissions case 1: - p.useChrome = !p.useChrome + p.autoMode = !p.autoMode case 2: + p.useChrome = !p.useChrome + case 3: p.useTeammateMode = !p.useTeammateMode } } else { @@ -221,6 +227,8 @@ func (p *ClaudeOptionsPanel) handleSpaceKey() { p.sessionMode = (p.sessionMode + 1) % 3 case "skipPermissions": p.skipPermissions = !p.skipPermissions + case "autoMode": + p.autoMode = !p.autoMode case "chrome": p.useChrome = !p.useChrome case "teammateMode": @@ -236,8 +244,10 @@ func (p *ClaudeOptionsPanel) getFocusType() string { case 0: return "skipPermissions" case 1: - return "chrome" + return "autoMode" case 2: + return "chrome" + case 3: return "teammateMode" } } else { @@ -257,12 +267,16 @@ func (p *ClaudeOptionsPanel) getFocusType() string { if idx == 1 { return "skipPermissions" } - // 3: chrome + // 3: auto mode if idx == 2 { - return "chrome" + return "autoMode" } - // 4: teammate mode + // 4: chrome if idx == 3 { + return "chrome" + } + // 5: teammate mode + if idx == 4 { return "teammateMode" } } @@ -272,10 +286,10 @@ func (p *ClaudeOptionsPanel) getFocusType() string { // getFocusCount returns the number of focusable elements func (p *ClaudeOptionsPanel) getFocusCount() int { if p.isForkMode { - return 3 // skip, chrome, teammate + return 4 // skip, auto, chrome, teammate } - count := 4 // session mode, skip, chrome, teammate + count := 5 // session mode, skip, auto, chrome, teammate if p.sessionMode == 2 { count++ // resume input } @@ -319,8 +333,12 @@ func (p *ClaudeOptionsPanel) viewForkMode(labelStyle, activeStyle, dimStyle, hea var content string content += headerStyle.Render("─ Advanced Options ─") + "\n" content += renderCheckboxLine("Skip permissions", p.skipPermissions, p.focusIndex == 0) - content += renderCheckboxLine("Chrome mode", p.useChrome, p.focusIndex == 1) - content += renderCheckboxLine("Teammate mode", p.useTeammateMode, p.focusIndex == 2) + content += renderCheckboxLine("Auto mode", p.autoMode, p.focusIndex == 1) + if p.autoMode && p.skipPermissions { + content += dimStyle.Render(" ↑ overridden by skip permissions") + "\n" + } + content += renderCheckboxLine("Chrome mode", p.useChrome, p.focusIndex == 2) + content += renderCheckboxLine("Teammate mode", p.useTeammateMode, p.focusIndex == 3) return content } @@ -355,6 +373,13 @@ func (p *ClaudeOptionsPanel) viewNewMode(labelStyle, activeStyle, dimStyle, head content += renderCheckboxLine("Skip permissions", p.skipPermissions, p.focusIndex == focusIdx) focusIdx++ + // Auto mode checkbox + content += renderCheckboxLine("Auto mode", p.autoMode, p.focusIndex == focusIdx) + if p.autoMode && p.skipPermissions { + content += dimStyle.Render(" ↑ overridden by skip permissions") + "\n" + } + focusIdx++ + // Chrome checkbox content += renderCheckboxLine("Chrome mode", p.useChrome, p.focusIndex == focusIdx) focusIdx++ diff --git a/internal/ui/setup_wizard.go b/internal/ui/setup_wizard.go index fd697a9e9..b9b7691ef 100644 --- a/internal/ui/setup_wizard.go +++ b/internal/ui/setup_wizard.go @@ -24,10 +24,11 @@ type SetupWizard struct { // Step 2: Claude settings (only if Claude selected) dangerousMode bool + autoMode bool useDefaultConfigDir bool customConfigDir string configDirInput textinput.Model - claudeSettingsCursor int // 0=dangerous mode, 1=config dir + claudeSettingsCursor int // 0=dangerous mode, 1=auto mode, 2=config dir // Theme setting selectedTheme int // 0=dark, 1=light @@ -146,6 +147,7 @@ func (w *SetupWizard) GetConfig() *session.UserConfig { // Set Claude settings dangerousModeVal := w.dangerousMode config.Claude.DangerousMode = &dangerousModeVal + config.Claude.AutoMode = w.autoMode if !w.useDefaultConfigDir && w.customConfigDir != "" { config.Claude.ConfigDir = w.customConfigDir } @@ -221,7 +223,7 @@ func (w *SetupWizard) Update(msg tea.Msg) (*SetupWizard, tea.Cmd) { case stepClaudeSettings: w.claudeSettingsCursor-- if w.claudeSettingsCursor < 0 { - w.claudeSettingsCursor = 1 + w.claudeSettingsCursor = 2 } } return w, nil @@ -231,7 +233,7 @@ func (w *SetupWizard) Update(msg tea.Msg) (*SetupWizard, tea.Cmd) { case stepToolSelection: w.selectedTool = (w.selectedTool + 1) % len(w.toolOptions) case stepClaudeSettings: - w.claudeSettingsCursor = (w.claudeSettingsCursor + 1) % 2 + w.claudeSettingsCursor = (w.claudeSettingsCursor + 1) % 3 } return w, nil @@ -241,6 +243,8 @@ func (w *SetupWizard) Update(msg tea.Msg) (*SetupWizard, tea.Cmd) { case 0: w.dangerousMode = !w.dangerousMode case 1: + w.autoMode = !w.autoMode + case 2: w.useDefaultConfigDir = !w.useDefaultConfigDir if !w.useDefaultConfigDir { w.configDirInput.Focus() @@ -424,13 +428,29 @@ func (w *SetupWizard) View() string { content.WriteString(lipgloss.NewStyle().Foreground(ColorTextDim).Render(" Skip permission prompts (--dangerously-skip-permissions)")) content.WriteString("\n\n") - // Config directory radio buttons + // Auto mode checkbox + checkbox = checkboxOff + if w.autoMode { + checkbox = checkboxOn + } cursor = " " style = labelStyle if w.claudeSettingsCursor == 1 { cursor = "> " style = lipgloss.NewStyle().Foreground(ColorAccent).Bold(true) } + content.WriteString(cursor + checkbox + " " + style.Render("Enable auto mode")) + content.WriteString("\n") + content.WriteString(lipgloss.NewStyle().Foreground(ColorTextDim).Render(" Classifier-based auto-approval (--permission-mode auto)")) + content.WriteString("\n\n") + + // Config directory radio buttons + cursor = " " + style = labelStyle + if w.claudeSettingsCursor == 2 { + cursor = "> " + style = lipgloss.NewStyle().Foreground(ColorAccent).Bold(true) + } content.WriteString(cursor + style.Render("Claude config directory:")) content.WriteString("\n") diff --git a/skills/agent-deck/references/config-reference.md b/skills/agent-deck/references/config-reference.md index c83cc703d..c1d395eae 100644 --- a/skills/agent-deck/references/config-reference.md +++ b/skills/agent-deck/references/config-reference.md @@ -58,6 +58,7 @@ Claude Code integration settings. [claude] config_dir = "~/.claude" # Path to Claude config directory dangerous_mode = true # Enable --dangerously-skip-permissions +auto_mode = false # Enable --permission-mode auto (classifier-based) allow_dangerous_mode = false # Enable --allow-dangerously-skip-permissions env_file = "~/.claude.env" # .env file specific to Claude sessions @@ -69,8 +70,9 @@ config_dir = "~/.claude-work" # Optional override for profile "work" |-----|------|---------|-------------| | `config_dir` | string | `~/.claude` | Claude config directory. Override with `CLAUDE_CONFIG_DIR` env. | | `profiles..claude.config_dir` | string | none | Profile-specific Claude config directory. Takes precedence over `[claude].config_dir` when that profile is active. | -| `dangerous_mode` | bool | `false` | Adds `--dangerously-skip-permissions`. Forces bypass on. Takes precedence over `allow_dangerous_mode`. | -| `allow_dangerous_mode` | bool | `false` | Adds `--allow-dangerously-skip-permissions`. Unlocks bypass as an option without activating it. Ignored when `dangerous_mode` is true. | +| `dangerous_mode` | bool | `false` | Adds `--dangerously-skip-permissions`. Forces bypass on. Takes precedence over `auto_mode` and `allow_dangerous_mode`. | +| `auto_mode` | bool | `false` | Adds `--permission-mode auto`. A classifier model auto-approves safe operations while blocking risky ones. Ignored when `dangerous_mode` is true. | +| `allow_dangerous_mode` | bool | `false` | Adds `--allow-dangerously-skip-permissions`. Unlocks bypass as an option without activating it. Ignored when `dangerous_mode` or `auto_mode` is true. | | `env_file` | string | `""` | A .env file sourced for Claude sessions only. Sourced after global `[shell].env_files`. See [Path Resolution](#path-resolution). | Config resolution order for Claude config dir: