C.W.K.
Stream
Lesson 01 of 05 · published

Permission Modes in Practice

~14 min · permissions, permission-mode, safety

Level 0Observer
0 XP0/64 lessons0/13 achievements
0/150 XP to next level150 XP to go0% complete

The four modes, and what they actually do

permission_mode on ClaudeAgentOptions takes one of four values. 'default' prompts the user before any tool that has side effects. 'acceptEdits' auto-approves Write/Edit but still prompts for Bash. 'bypassPermissions' approves everything (use for trusted CI only). 'plan' puts the agent in dry-run mode — it plans actions without executing.

Pick by who is on the other end

If a developer is at the keyboard reviewing each prompt, 'acceptEdits' often hits the right tradeoff (file edits flow, shell still asks). If the agent runs unattended in a server context, 'default' with a custom permission handler is the policy posture. 'bypassPermissions' is for trusted CI runners that already pass through review elsewhere.

Write your own permission handler

Beyond the modes, you can supply a callback that decides per-tool. The callback gets the tool name, input, and a signal to allow / deny / ask. cwkPippa's setup uses a custom handler for the chat-from-WebUI path so Dad can approve dangerous commands inline rather than via the CLI's TUI.

Principle: Permission posture is a UX decision as much as a security decision. Match it to who is actually in the loop.

Code

Mode selection by context·python
from claude_agent_sdk import ClaudeAgentOptions

# Interactive developer pairing — accept edits silently, ask for shell.
dev_options = ClaudeAgentOptions(
    cwd="/Users/me/repo",
    permission_mode="acceptEdits",
)

# Server-side autonomous worker — ask for everything, route prompts to your UI.
server_options = ClaudeAgentOptions(
    cwd="/srv/jobs",
    permission_mode="default",
    can_use_tool=permission_handler,  # your function
)

# Trusted CI in a sandboxed container — bypass to keep latency down.
ci_options = ClaudeAgentOptions(
    cwd="/workspace",
    permission_mode="bypassPermissions",
    allowed_tools=["Read", "Edit", "Bash"],  # narrow the tool set instead
)

# Plan-only mode for dry runs.
plan_options = ClaudeAgentOptions(
    cwd="/srv",
    permission_mode="plan",
)
Custom permission handler·python
from claude_agent_sdk import ClaudeAgentOptions, PermissionRequest, PermissionResult

DEMANDS_REVIEW = {"Bash", "Write"}

async def permission_handler(req: PermissionRequest) -> PermissionResult:
    if req.tool_name not in DEMANDS_REVIEW:
        return PermissionResult(behavior="allow")
    # Forward the request to your UI; user clicks approve or deny.
    decision = await ask_user_in_ui(req.tool_name, req.input)
    if decision == "approve":
        return PermissionResult(behavior="allow")
    return PermissionResult(behavior="deny", message="User denied via UI")

options = ClaudeAgentOptions(
    cwd="/srv",
    permission_mode="default",
    can_use_tool=permission_handler,
)

External links

Exercise

Audit one Agent SDK call in your project. Identify the permission_mode in use and one caller-fact that makes it the right choice. If the answer is 'I copy-pasted bypassPermissions', that is the bug to fix.
Hint
Think about who is the responsible human at runtime. The mode should match who can answer 'should I run this?'

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.