docs
0
# Permission Control
1
2
Runtime permission control via modes, programmatic callbacks, and permission updates. The permission system provides multiple layers of control over which tools Claude can execute and how.
3
4
## Capabilities
5
6
### Permission Modes
7
8
Predefined permission modes for common scenarios.
9
10
```python { .api }
11
PermissionMode = Literal["default", "acceptEdits", "plan", "bypassPermissions"]
12
```
13
14
**Modes:**
15
16
- `"default"`: CLI prompts user for dangerous tools
17
- `"acceptEdits"`: Auto-accept file edits (Read, Write, Edit, MultiEdit)
18
- `"plan"`: Plan mode, no actual execution
19
- `"bypassPermissions"`: Allow all tools without prompts (use with caution)
20
21
**Usage Example:**
22
23
```python
24
from claude_agent_sdk import ClaudeAgentOptions
25
26
# Default mode - prompt for dangerous operations
27
options = ClaudeAgentOptions(
28
permission_mode="default",
29
allowed_tools=["Read", "Write", "Bash"]
30
)
31
32
# Accept edits automatically
33
options = ClaudeAgentOptions(
34
permission_mode="acceptEdits",
35
allowed_tools=["Read", "Write", "Edit"]
36
)
37
38
# Plan mode - no execution
39
options = ClaudeAgentOptions(
40
permission_mode="plan",
41
allowed_tools=["Read", "Write", "Bash"]
42
)
43
44
# Bypass all permissions (dangerous!)
45
options = ClaudeAgentOptions(
46
permission_mode="bypassPermissions",
47
allowed_tools=["Read", "Write", "Bash"]
48
)
49
```
50
51
### Dynamic Permission Mode Changes
52
53
Change permission mode during conversation with `ClaudeSDKClient`.
54
55
**Usage Example:**
56
57
```python
58
from claude_agent_sdk import ClaudeSDKClient
59
60
async def main():
61
async with ClaudeSDKClient() as client:
62
# Start with default permissions (prompts user)
63
await client.query("Review this codebase")
64
async for msg in client.receive_response():
65
print(msg)
66
67
# Switch to auto-accept edits
68
await client.set_permission_mode('acceptEdits')
69
await client.query("Now implement the fixes we discussed")
70
async for msg in client.receive_response():
71
print(msg)
72
73
# Back to default for safety
74
await client.set_permission_mode('default')
75
```
76
77
### Permission Callback
78
79
Programmatic permission control via callback function.
80
81
```python { .api }
82
CanUseTool = Callable[
83
[str, dict[str, Any], ToolPermissionContext],
84
Awaitable[PermissionResult]
85
]
86
```
87
88
**Parameters:**
89
90
1. `tool_name` (str): Name of the tool being invoked
91
2. `tool_input` (dict[str, Any]): Tool input parameters
92
3. `context` (ToolPermissionContext): Permission context with signal and suggestions
93
94
**Returns:**
95
96
`PermissionResult`: Either `PermissionResultAllow` or `PermissionResultDeny`
97
98
**Usage Example:**
99
100
```python
101
from claude_agent_sdk import (
102
ClaudeAgentOptions, PermissionResultAllow, PermissionResultDeny
103
)
104
105
async def my_permission_handler(tool_name, tool_input, context):
106
# Allow read operations
107
if tool_name == "Read":
108
return PermissionResultAllow()
109
110
# Review bash commands
111
if tool_name == "Bash":
112
command = tool_input.get("command", "")
113
if "rm -rf" in command:
114
return PermissionResultDeny(
115
message="Dangerous command blocked",
116
interrupt=True
117
)
118
return PermissionResultAllow()
119
120
# Allow other operations
121
return PermissionResultAllow()
122
123
options = ClaudeAgentOptions(
124
can_use_tool=my_permission_handler,
125
allowed_tools=["Read", "Write", "Bash"]
126
)
127
```
128
129
### Tool Permission Context
130
131
Context provided to permission callbacks.
132
133
```python { .api }
134
@dataclass
135
class ToolPermissionContext:
136
"""Context for permission callbacks."""
137
138
signal: Any | None = None
139
suggestions: list[PermissionUpdate] = field(default_factory=list)
140
```
141
142
**Fields:**
143
144
- `signal` (Any | None): Future abort signal support. Currently always None.
145
146
- `suggestions` (list[PermissionUpdate]): Permission update suggestions from CLI based on the operation being performed.
147
148
**Usage Example:**
149
150
```python
151
async def smart_permission_handler(tool_name, tool_input, context):
152
# Use CLI suggestions
153
suggestions = context.suggestions
154
155
# Apply suggested permission updates
156
if suggestions:
157
return PermissionResultAllow(updated_permissions=suggestions)
158
159
return PermissionResultAllow()
160
```
161
162
### Permission Result Types
163
164
Permission decisions returned from callbacks.
165
166
#### Allow Result
167
168
```python { .api }
169
@dataclass
170
class PermissionResultAllow:
171
"""Allow permission decision."""
172
173
behavior: Literal["allow"] = "allow"
174
updated_input: dict[str, Any] | None = None
175
updated_permissions: list[PermissionUpdate] | None = None
176
```
177
178
**Fields:**
179
180
- `behavior` (Literal["allow"]): Always `"allow"`. Default value.
181
182
- `updated_input` (dict[str, Any] | None): Modified tool input parameters. Use this to sanitize or enhance tool inputs before execution.
183
184
- `updated_permissions` (list[PermissionUpdate] | None): Permission updates to apply. See [Permission Updates](#permission-update) below.
185
186
**Usage Examples:**
187
188
```python
189
# Simple allow
190
return PermissionResultAllow()
191
192
# Allow with modified input
193
modified_input = tool_input.copy()
194
modified_input["timeout"] = 30
195
return PermissionResultAllow(updated_input=modified_input)
196
197
# Allow with permission updates
198
updates = [PermissionUpdate(
199
type="addRules",
200
rules=[PermissionRuleValue(tool_name="Bash", rule_content=None)],
201
behavior="allow",
202
destination="session"
203
)]
204
return PermissionResultAllow(updated_permissions=updates)
205
206
# Allow with both
207
return PermissionResultAllow(
208
updated_input=modified_input,
209
updated_permissions=updates
210
)
211
```
212
213
#### Deny Result
214
215
```python { .api }
216
@dataclass
217
class PermissionResultDeny:
218
"""Deny permission decision."""
219
220
behavior: Literal["deny"] = "deny"
221
message: str = ""
222
interrupt: bool = False
223
```
224
225
**Fields:**
226
227
- `behavior` (Literal["deny"]): Always `"deny"`. Default value.
228
229
- `message` (str): Denial message shown to user and Claude. Default: empty string.
230
231
- `interrupt` (bool): Whether to interrupt the entire agent execution. Default: False.
232
233
**Usage Examples:**
234
235
```python
236
# Simple deny
237
return PermissionResultDeny()
238
239
# Deny with message
240
return PermissionResultDeny(
241
message="This operation is not allowed in production"
242
)
243
244
# Deny and interrupt
245
return PermissionResultDeny(
246
message="Critical security violation detected",
247
interrupt=True
248
)
249
```
250
251
#### Permission Result Union
252
253
```python { .api }
254
PermissionResult = PermissionResultAllow | PermissionResultDeny
255
```
256
257
### Permission Update
258
259
Configure permission changes to apply.
260
261
```python { .api }
262
@dataclass
263
class PermissionUpdate:
264
"""Permission configuration update."""
265
266
type: Literal[
267
"addRules",
268
"replaceRules",
269
"removeRules",
270
"setMode",
271
"addDirectories",
272
"removeDirectories"
273
]
274
rules: list[PermissionRuleValue] | None = None
275
behavior: PermissionBehavior | None = None
276
mode: PermissionMode | None = None
277
directories: list[str] | None = None
278
destination: PermissionUpdateDestination | None = None
279
280
def to_dict(self) -> dict[str, Any]:
281
"""Convert to dictionary for CLI."""
282
```
283
284
**Fields:**
285
286
- `type`: Update type determining which other fields are required:
287
- `"addRules"`: Add permission rules (requires `rules`, `behavior`)
288
- `"replaceRules"`: Replace permission rules (requires `rules`, `behavior`)
289
- `"removeRules"`: Remove permission rules (requires `rules`)
290
- `"setMode"`: Set permission mode (requires `mode`)
291
- `"addDirectories"`: Add permission directories (requires `directories`)
292
- `"removeDirectories"`: Remove permission directories (requires `directories`)
293
294
- `rules` (list[PermissionRuleValue] | None): Permission rules for add/replace/remove operations.
295
296
- `behavior` (PermissionBehavior | None): Rule behavior (`"allow"`, `"deny"`, `"ask"`).
297
298
- `mode` (PermissionMode | None): Permission mode for setMode operation.
299
300
- `directories` (list[str] | None): Directory list for add/remove operations.
301
302
- `destination` (PermissionUpdateDestination | None): Where to apply update:
303
- `"userSettings"`: User-level settings
304
- `"projectSettings"`: Project-level settings
305
- `"localSettings"`: Local directory settings
306
- `"session"`: Current session only
307
308
**Methods:**
309
310
- `to_dict() -> dict[str, Any]`: Convert to dictionary format for CLI protocol.
311
312
**Usage Examples:**
313
314
```python
315
from claude_agent_sdk import PermissionUpdate, PermissionRuleValue
316
317
# Add allow rule for session
318
update = PermissionUpdate(
319
type="addRules",
320
rules=[PermissionRuleValue(tool_name="Bash", rule_content=None)],
321
behavior="allow",
322
destination="session"
323
)
324
325
# Set permission mode
326
update = PermissionUpdate(
327
type="setMode",
328
mode="acceptEdits",
329
destination="session"
330
)
331
332
# Add directories
333
update = PermissionUpdate(
334
type="addDirectories",
335
directories=["/home/user/project/src"],
336
destination="projectSettings"
337
)
338
339
# Remove rules
340
update = PermissionUpdate(
341
type="removeRules",
342
rules=[PermissionRuleValue(tool_name="Bash", rule_content="rm -rf")],
343
destination="session"
344
)
345
346
# Convert to dict for protocol
347
update_dict = update.to_dict()
348
```
349
350
### Permission Rule Value
351
352
Individual permission rule.
353
354
```python { .api }
355
@dataclass
356
class PermissionRuleValue:
357
"""Permission rule value."""
358
359
tool_name: str
360
rule_content: str | None = None
361
```
362
363
**Fields:**
364
365
- `tool_name` (str): Tool name pattern (e.g., `"Bash"`, `"Write"`, `"*"` for all).
366
367
- `rule_content` (str | None): Optional rule content for fine-grained control (e.g., path patterns, command patterns).
368
369
**Usage Example:**
370
371
```python
372
from claude_agent_sdk import PermissionRuleValue
373
374
# Simple tool rule
375
rule = PermissionRuleValue(tool_name="Bash")
376
377
# Rule with content pattern
378
rule = PermissionRuleValue(
379
tool_name="Write",
380
rule_content="/tmp/*" # Allow writes only to /tmp
381
)
382
383
# Wildcard rule
384
rule = PermissionRuleValue(tool_name="*") # All tools
385
```
386
387
### Permission Behavior
388
389
Rule behavior types.
390
391
```python { .api }
392
PermissionBehavior = Literal["allow", "deny", "ask"]
393
```
394
395
**Values:**
396
397
- `"allow"`: Allow the operation
398
- `"deny"`: Deny the operation
399
- `"ask"`: Prompt the user
400
401
### Permission Update Destination
402
403
Where permission updates are applied.
404
405
```python { .api }
406
PermissionUpdateDestination = Literal[
407
"userSettings",
408
"projectSettings",
409
"localSettings",
410
"session"
411
]
412
```
413
414
**Values:**
415
416
- `"userSettings"`: User-level settings (persists across all projects)
417
- `"projectSettings"`: Project-level settings (persists for this project)
418
- `"localSettings"`: Local directory settings (persists for this directory)
419
- `"session"`: Current session only (does not persist)
420
421
## Complete Examples
422
423
### Security-Aware Permission Handler
424
425
```python
426
from claude_agent_sdk import (
427
ClaudeAgentOptions, ClaudeSDKClient,
428
PermissionResultAllow, PermissionResultDeny,
429
PermissionUpdate, PermissionRuleValue
430
)
431
import re
432
433
DANGEROUS_COMMANDS = [
434
r"rm\s+-rf\s+/",
435
r"mkfs\.",
436
r"dd\s+if=.*\s+of=/dev/",
437
r":\(\)\{\s*:\|:\&\s*\};:", # Fork bomb
438
]
439
440
async def security_permission_handler(tool_name, tool_input, context):
441
# Always allow read operations
442
if tool_name == "Read":
443
return PermissionResultAllow()
444
445
# Review bash commands
446
if tool_name == "Bash":
447
command = tool_input.get("command", "")
448
449
# Check for dangerous patterns
450
for pattern in DANGEROUS_COMMANDS:
451
if re.search(pattern, command):
452
return PermissionResultDeny(
453
message=f"Blocked dangerous command pattern: {pattern}",
454
interrupt=True
455
)
456
457
# Add timeout to long-running commands
458
if "timeout" not in tool_input:
459
modified_input = tool_input.copy()
460
modified_input["timeout"] = 60
461
return PermissionResultAllow(updated_input=modified_input)
462
463
# Review file writes
464
if tool_name in ["Write", "Edit", "MultiEdit"]:
465
file_path = tool_input.get("file_path", "")
466
467
# Block writes to system directories
468
if file_path.startswith("/etc/") or file_path.startswith("/sys/"):
469
return PermissionResultDeny(
470
message=f"Cannot write to system directory: {file_path}"
471
)
472
473
# Allow with default behavior
474
return PermissionResultAllow()
475
476
# Use with client
477
async def main():
478
options = ClaudeAgentOptions(
479
can_use_tool=security_permission_handler,
480
allowed_tools=["Read", "Write", "Bash"]
481
)
482
483
async with ClaudeSDKClient(options=options) as client:
484
await client.query("Review and fix the code")
485
async for msg in client.receive_response():
486
print(msg)
487
```
488
489
### Adaptive Permission Handler
490
491
```python
492
from claude_agent_sdk import (
493
PermissionResultAllow, PermissionResultDeny,
494
PermissionUpdate, PermissionRuleValue
495
)
496
497
class AdaptivePermissionHandler:
498
def __init__(self):
499
self.trust_level = 0
500
self.operations_count = 0
501
502
async def __call__(self, tool_name, tool_input, context):
503
self.operations_count += 1
504
505
# Build trust over successful operations
506
if self.operations_count > 10:
507
self.trust_level = min(self.trust_level + 1, 5)
508
509
# High trust - allow most operations
510
if self.trust_level >= 4:
511
return PermissionResultAllow()
512
513
# Medium trust - review risky operations
514
if self.trust_level >= 2:
515
if tool_name == "Bash":
516
command = tool_input.get("command", "")
517
if any(word in command for word in ["rm", "delete", "drop"]):
518
return PermissionResultDeny(
519
message="Risky operation requires higher trust level"
520
)
521
return PermissionResultAllow()
522
523
# Low trust - only allow safe operations
524
safe_tools = ["Read", "Glob", "Grep"]
525
if tool_name in safe_tools:
526
return PermissionResultAllow()
527
528
return PermissionResultDeny(
529
message="Build trust by performing safe operations first"
530
)
531
532
# Usage
533
handler = AdaptivePermissionHandler()
534
options = ClaudeAgentOptions(
535
can_use_tool=handler,
536
allowed_tools=["Read", "Write", "Bash", "Glob", "Grep"]
537
)
538
```
539
540
### Context-Aware Permission Handler
541
542
```python
543
from claude_agent_sdk import (
544
PermissionResultAllow, PermissionResultDeny,
545
PermissionUpdate, PermissionRuleValue
546
)
547
548
async def context_aware_handler(tool_name, tool_input, context):
549
# Use CLI suggestions
550
suggestions = context.suggestions
551
552
# Auto-apply safe suggestions
553
safe_suggestions = [
554
s for s in suggestions
555
if s.type in ["addDirectories", "setMode"]
556
]
557
558
if safe_suggestions:
559
return PermissionResultAllow(updated_permissions=safe_suggestions)
560
561
# Review bash commands in context
562
if tool_name == "Bash":
563
command = tool_input.get("command", "")
564
565
# Allow package managers
566
if any(pm in command for pm in ["pip", "npm", "apt-get"]):
567
# But add safety rules
568
updates = [PermissionUpdate(
569
type="addRules",
570
rules=[PermissionRuleValue(
571
tool_name="Bash",
572
rule_content="package manager"
573
)],
574
behavior="allow",
575
destination="session"
576
)]
577
return PermissionResultAllow(updated_permissions=updates)
578
579
return PermissionResultAllow()
580
581
options = ClaudeAgentOptions(
582
can_use_tool=context_aware_handler,
583
allowed_tools=["Read", "Write", "Bash"]
584
)
585
```
586
587
### Project-Based Permission Handler
588
589
```python
590
from pathlib import Path
591
from claude_agent_sdk import (
592
PermissionResultAllow, PermissionResultDeny,
593
PermissionUpdate, PermissionRuleValue
594
)
595
596
class ProjectPermissionHandler:
597
def __init__(self, project_root: Path, allowed_paths: list[Path]):
598
self.project_root = project_root
599
self.allowed_paths = [project_root / p for p in allowed_paths]
600
601
async def __call__(self, tool_name, tool_input, context):
602
# File operations
603
if tool_name in ["Write", "Edit", "MultiEdit"]:
604
file_path = Path(tool_input.get("file_path", ""))
605
606
# Check if path is within allowed directories
607
allowed = any(
608
file_path.is_relative_to(allowed_path)
609
for allowed_path in self.allowed_paths
610
)
611
612
if not allowed:
613
return PermissionResultDeny(
614
message=f"File path outside allowed directories: {file_path}"
615
)
616
617
# Add project directory to permissions
618
updates = [PermissionUpdate(
619
type="addDirectories",
620
directories=[str(self.project_root)],
621
destination="projectSettings"
622
)]
623
return PermissionResultAllow(updated_permissions=updates)
624
625
# Bash operations - restrict to project directory
626
if tool_name == "Bash":
627
cwd = tool_input.get("cwd", "")
628
if cwd and not Path(cwd).is_relative_to(self.project_root):
629
return PermissionResultDeny(
630
message="Bash commands must run in project directory"
631
)
632
633
return PermissionResultAllow()
634
635
# Usage
636
handler = ProjectPermissionHandler(
637
project_root=Path("/home/user/myproject"),
638
allowed_paths=[Path("src"), Path("tests"), Path("docs")]
639
)
640
641
options = ClaudeAgentOptions(
642
can_use_tool=handler,
643
allowed_tools=["Read", "Write", "Bash"],
644
cwd="/home/user/myproject"
645
)
646
```
647
648
## Best Practices
649
650
1. **Start Restrictive**: Begin with `permission_mode="default"` and selectively allow operations
651
652
2. **Use Callbacks for Complex Logic**: Permission callbacks provide fine-grained control beyond simple modes
653
654
3. **Sanitize Inputs**: Use `updated_input` to sanitize or enhance tool inputs before execution
655
656
4. **Provide Clear Messages**: Always include helpful denial messages so users understand why operations were blocked
657
658
5. **Layer Security**: Combine permission modes, callbacks, and hooks for defense in depth
659
660
6. **Session-Level Updates**: Use `destination="session"` for temporary permission changes
661
662
7. **Monitor Operations**: Log permission decisions for auditing and debugging
663
664
8. **Handle Errors**: Always handle exceptions in permission callbacks
665
666
9. **Test Thoroughly**: Test permission logic with various tool inputs and scenarios
667
668
10. **Document Rules**: Document your permission policies for team members
669
670
## Permission System Architecture
671
672
The permission system has multiple layers:
673
674
1. **Permission Mode**: Coarse-grained control (default, acceptEdits, plan, bypassPermissions)
675
2. **Allowed/Disallowed Tools**: Tool-level filtering
676
3. **Permission Callbacks** (`can_use_tool`): Fine-grained programmatic control
677
4. **Hooks** (`PreToolUse`): Intercept and modify before execution
678
5. **Permission Updates**: Dynamic rule changes during execution
679
680
These layers work together to provide comprehensive control over tool execution.
681