Skip to content

Commit beef892

Browse files
idoubiclaude
andcommitted
fix: permission system overhaul - bypass mode, smart auto-approve
The permission callback was always showing interactive prompts even in bypassPermissions mode because we overrode CanUseTool for all modes. Now the permission logic is: - bypassPermissions: allow everything, zero prompts - acceptEdits: auto-approve read-only + Edit/Write/Bash - plan: allow all (planning can read/analyze) - default: auto-approve read-only, ask for write operations Also: - /permissions bypass (shortcut aliases: bypass, off, y, yes) - /permissions auto (alias for acceptEdits) - Clearer mode descriptions with emoji indicators - Read-only tools (Read, Glob, Grep, WebFetch, WebSearch) never prompt Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 19048e9 commit beef892

2 files changed

Lines changed: 72 additions & 16 deletions

File tree

internal/slash/slash.go

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -370,18 +370,52 @@ func (h *Handler) showConfig() Result {
370370
func (h *Handler) permissions(args []string) Result {
371371
if len(args) == 0 {
372372
cfg := h.app.GetConfig()
373+
mode := cfg.PermissionMode
374+
if mode == "" {
375+
mode = "default"
376+
}
377+
indicator := map[string]string{
378+
"default": "🔒 Asks for write operations",
379+
"acceptEdits": "✎ Auto-approves all file edits + bash",
380+
"bypassPermissions": "⚡ Allows everything (no prompts)",
381+
"plan": "📋 Plan only, no execution",
382+
}
383+
desc := indicator[mode]
373384
return Result{
374-
Message: fmt.Sprintf("Current mode: %s\n\nAvailable modes:\n default Ask for each tool\n acceptEdits Auto-approve read-only tools\n bypassPermissions Skip all prompts\n plan Plan mode (no execution)", cfg.PermissionMode),
385+
Message: fmt.Sprintf("Current mode: %s — %s\n\nSwitch with:\n /permissions bypass Skip all prompts\n /permissions default Ask for write ops\n /permissions auto Auto-approve edits\n /permissions plan Plan only", mode, desc),
375386
}
376387
}
377388

378389
mode := args[0]
390+
// Aliases for convenience
391+
switch strings.ToLower(mode) {
392+
case "bypass", "bypasspermissions", "off", "y", "yes":
393+
mode = "bypassPermissions"
394+
case "auto", "acceptedits", "accept":
395+
mode = "acceptEdits"
396+
case "default", "on", "ask":
397+
mode = "default"
398+
case "plan":
399+
mode = "plan"
400+
}
401+
379402
switch mode {
380403
case "default", "acceptEdits", "bypassPermissions", "plan":
381404
h.app.SetPermissionMode(mode)
382-
return Result{Message: fmt.Sprintf("Permission mode changed to: %s", mode)}
405+
indicator := ""
406+
switch mode {
407+
case "bypassPermissions":
408+
indicator = " ⚡ All tools auto-approved"
409+
case "acceptEdits":
410+
indicator = " ✎ File edits auto-approved"
411+
case "plan":
412+
indicator = " 📋 Plan only mode"
413+
case "default":
414+
indicator = " 🔒 Will ask for write operations"
415+
}
416+
return Result{Message: fmt.Sprintf("Permission mode: %s%s", mode, indicator)}
383417
default:
384-
return Result{Message: fmt.Sprintf("Unknown permission mode: %s", mode)}
418+
return Result{Message: fmt.Sprintf("Unknown mode: %s\nUse: bypass, auto, default, plan", mode)}
385419
}
386420
}
387421

internal/tui/model.go

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -211,12 +211,8 @@ func (m *Model) initAgent() tea.Cmd {
211211
AllowedTools: m.cfg.AllowedTools,
212212
}
213213

214-
pm := m.cfg.GetPermissionMode()
215-
if pm == types.PermissionModeBypassPermissions {
216-
opts.PermissionMode = types.PermissionModeBypassPermissions
217-
} else {
218-
opts.CanUseTool = m.createPermissionCallback()
219-
}
214+
// Always use our interactive callback — it checks mode internally
215+
opts.CanUseTool = m.createPermissionCallback()
220216

221217
a := agent.New(opts)
222218
m.agent = a
@@ -236,18 +232,44 @@ func (m *Model) initAgent() tea.Cmd {
236232
}
237233

238234
func (m *Model) createPermissionCallback() types.CanUseToolFn {
235+
allow := &types.PermissionDecision{Behavior: types.PermissionAllow}
236+
239237
return func(tool types.Tool, input map[string]interface{}) (*types.PermissionDecision, error) {
240-
// Check persisted rules first
238+
mode := m.cfg.GetPermissionMode()
239+
240+
// bypassPermissions: allow everything, no questions asked
241+
if mode == types.PermissionModeBypassPermissions {
242+
return allow, nil
243+
}
244+
245+
// Check persisted "always allow" rules
241246
if m.permRules.IsAllowed(tool.Name()) {
242-
return &types.PermissionDecision{Behavior: types.PermissionAllow}, nil
247+
return allow, nil
248+
}
249+
250+
// acceptEdits: auto-approve read-only tools + file edit tools
251+
if mode == types.PermissionModeAcceptEdits {
252+
if tool.IsReadOnly(input) {
253+
return allow, nil
254+
}
255+
// Also auto-approve Edit, Write (the "edits" in acceptEdits)
256+
switch tool.Name() {
257+
case "Edit", "Write", "Bash":
258+
return allow, nil
259+
}
260+
}
261+
262+
// plan mode: allow all tools (planning can read/analyze)
263+
if mode == types.PermissionModePlan {
264+
return allow, nil
243265
}
244266

245-
// Read-only auto-approve in acceptEdits mode
246-
if m.cfg.GetPermissionMode() == types.PermissionModeAcceptEdits && tool.IsReadOnly(input) {
247-
return &types.PermissionDecision{Behavior: types.PermissionAllow}, nil
267+
// default mode: auto-approve read-only tools, ask for write tools
268+
if tool.IsReadOnly(input) {
269+
return allow, nil
248270
}
249271

250-
// Ask user via TUI
272+
// Ask user via TUI for write operations
251273
respCh := make(chan *types.PermissionDecision, 1)
252274
if m.program != nil {
253275
m.program.Send(permissionRequestMsg{
@@ -256,7 +278,7 @@ func (m *Model) createPermissionCallback() types.CanUseToolFn {
256278
respCh: respCh,
257279
})
258280
} else {
259-
return &types.PermissionDecision{Behavior: types.PermissionAllow}, nil
281+
return allow, nil
260282
}
261283

262284
select {

0 commit comments

Comments
 (0)