docs
0
# Permission Control
1
2
Permission control provides fine-grained control over which tools Claude can execute. You can use permission modes for broad control or implement custom permission callbacks for precise per-tool decisions.
3
4
## Capabilities
5
6
### Permission Modes
7
8
Predefined permission modes for controlling tool execution.
9
10
```python { .api }
11
PermissionMode = Literal["default", "acceptEdits", "plan", "bypassPermissions"]
12
"""
13
Permission modes for tool execution.
14
15
Permission modes provide preset configurations for how Claude handles
16
tool permissions:
17
18
- 'default': Prompt user for dangerous operations (interactive mode)
19
- 'acceptEdits': Auto-accept file edits, prompt for other dangerous operations
20
- 'plan': Generate execution plan without actually executing tools
21
- 'bypassPermissions': Allow all tools without prompting (use with extreme caution)
22
23
Used in ClaudeAgentOptions.permission_mode.
24
"""
25
```
26
27
### Permission Result Types
28
29
Result types returned by permission callbacks.
30
31
```python { .api }
32
PermissionResult = PermissionResultAllow | PermissionResultDeny
33
"""
34
Union of permission result types.
35
36
Permission callbacks return either PermissionResultAllow or PermissionResultDeny
37
to indicate whether a tool should be allowed or blocked.
38
"""
39
```
40
41
### Permission Result Allow
42
43
Allow a tool to execute, optionally with modifications.
44
45
```python { .api }
46
@dataclass
47
class PermissionResultAllow:
48
"""
49
Allow permission result.
50
51
Indicates that a tool should be allowed to execute. Optionally includes
52
modifications to the tool input or permission updates.
53
54
Attributes:
55
behavior: Always "allow"
56
updated_input: Modified tool input parameters
57
updated_permissions: Permission updates to apply
58
"""
59
60
behavior: Literal["allow"] = "allow"
61
"""Behavior marker.
62
63
Always set to "allow" to indicate permission is granted.
64
"""
65
66
updated_input: dict[str, Any] | None = None
67
"""Modified tool input.
68
69
If set, the tool will be executed with these input parameters instead
70
of the original ones. Allows you to modify tool behavior on-the-fly.
71
72
Example:
73
# Original input: {"command": "rm -rf /"}
74
# Modified input: {"command": "echo 'blocked dangerous command'"}
75
updated_input={"command": "echo 'blocked dangerous command'"}
76
"""
77
78
updated_permissions: list[PermissionUpdate] | None = None
79
"""Permission updates to apply.
80
81
List of permission updates to apply after allowing this tool use.
82
Can add rules, change modes, update directories, etc.
83
84
See PermissionUpdate for details.
85
"""
86
```
87
88
### Permission Result Deny
89
90
Deny a tool execution.
91
92
```python { .api }
93
@dataclass
94
class PermissionResultDeny:
95
"""
96
Deny permission result.
97
98
Indicates that a tool should not be allowed to execute. Includes
99
a message explaining why and whether to interrupt the conversation.
100
101
Attributes:
102
behavior: Always "deny"
103
message: Explanation for denial
104
interrupt: Whether to interrupt the conversation
105
"""
106
107
behavior: Literal["deny"] = "deny"
108
"""Behavior marker.
109
110
Always set to "deny" to indicate permission is denied.
111
"""
112
113
message: str = ""
114
"""Denial message.
115
116
Human-readable explanation of why the tool was blocked. This message
117
may be shown to the user or logged.
118
119
Example:
120
"Dangerous command blocked for security reasons"
121
"Tool not available in readonly mode"
122
"""
123
124
interrupt: bool = False
125
"""Interrupt flag.
126
127
If True, the conversation will be interrupted immediately after denying
128
this tool. If False, Claude may try alternative approaches.
129
130
Use True for critical security issues, False for normal denials.
131
"""
132
```
133
134
### Permission Update
135
136
Configuration for updating permissions during execution.
137
138
```python { .api }
139
@dataclass
140
class PermissionUpdate:
141
"""
142
Permission update configuration.
143
144
Defines a change to permission settings. Can add/remove rules, change
145
modes, or modify directory access. Used in PermissionResultAllow.
146
147
Attributes:
148
type: Type of permission update
149
rules: Permission rules (for rule-based updates)
150
behavior: Permission behavior (for rule-based updates)
151
mode: Permission mode (for mode updates)
152
directories: Directories (for directory updates)
153
destination: Where to save the update
154
"""
155
156
type: Literal[
157
"addRules",
158
"replaceRules",
159
"removeRules",
160
"setMode",
161
"addDirectories",
162
"removeDirectories",
163
]
164
"""Update type.
165
166
Specifies what kind of permission change to make:
167
- 'addRules': Add new permission rules
168
- 'replaceRules': Replace existing permission rules
169
- 'removeRules': Remove permission rules
170
- 'setMode': Change the permission mode
171
- 'addDirectories': Add directories to allowed list
172
- 'removeDirectories': Remove directories from allowed list
173
"""
174
175
rules: list[PermissionRuleValue] | None = None
176
"""Permission rules.
177
178
List of rules to add, replace, or remove. Required for rule-based
179
update types (addRules, replaceRules, removeRules).
180
181
See PermissionRuleValue for rule structure.
182
"""
183
184
behavior: PermissionBehavior | None = None
185
"""Permission behavior.
186
187
Behavior to apply to rules: "allow", "deny", or "ask".
188
Required for rule-based update types.
189
"""
190
191
mode: PermissionMode | None = None
192
"""Permission mode.
193
194
The mode to set. Required for 'setMode' update type.
195
"""
196
197
directories: list[str] | None = None
198
"""Directories list.
199
200
List of directory paths. Required for directory-based update types
201
(addDirectories, removeDirectories).
202
"""
203
204
destination: PermissionUpdateDestination | None = None
205
"""Update destination.
206
207
Where to save this permission update:
208
- 'userSettings': User-level settings
209
- 'projectSettings': Project-level settings
210
- 'localSettings': Local settings
211
- 'session': Current session only
212
213
If None, applies to current session only.
214
"""
215
216
def to_dict(self) -> dict[str, Any]:
217
"""
218
Convert PermissionUpdate to dictionary format.
219
220
Converts the dataclass to a dictionary matching the TypeScript
221
control protocol format.
222
223
Returns:
224
Dictionary representation of the permission update
225
"""
226
```
227
228
### Permission Rule Value
229
230
Individual permission rule configuration.
231
232
```python { .api }
233
@dataclass
234
class PermissionRuleValue:
235
"""
236
Permission rule value.
237
238
Defines a single permission rule for a tool.
239
240
Attributes:
241
tool_name: Name of the tool this rule applies to
242
rule_content: Optional rule details or pattern
243
"""
244
245
tool_name: str
246
"""Tool name.
247
248
The name of the tool this rule applies to. Examples:
249
- "Bash"
250
- "Write"
251
- "Read"
252
- "custom_tool"
253
"""
254
255
rule_content: str | None = None
256
"""Rule content.
257
258
Optional additional details about the rule. Can be:
259
- A pattern to match against tool input
260
- A condition to evaluate
261
- Additional metadata
262
263
Format depends on the tool and use case.
264
"""
265
```
266
267
### Permission Behavior
268
269
Behavior types for permission rules.
270
271
```python { .api }
272
PermissionBehavior = Literal["allow", "deny", "ask"]
273
"""
274
Permission behaviors for rules.
275
276
- 'allow': Always allow the tool
277
- 'deny': Always deny the tool
278
- 'ask': Prompt the user for decision
279
"""
280
```
281
282
### Permission Update Destination
283
284
Where to save permission updates.
285
286
```python { .api }
287
PermissionUpdateDestination = Literal[
288
"userSettings", "projectSettings", "localSettings", "session"
289
]
290
"""
291
Permission update destinations.
292
293
Specifies where to persist permission updates:
294
- 'userSettings': Save to user-level settings (all projects)
295
- 'projectSettings': Save to project-level settings (current project)
296
- 'localSettings': Save to local settings (current directory)
297
- 'session': Apply only to current session (not persisted)
298
"""
299
```
300
301
### Tool Permission Context
302
303
Context information passed to permission callbacks.
304
305
```python { .api }
306
@dataclass
307
class ToolPermissionContext:
308
"""
309
Context information for tool permission callbacks.
310
311
Provides additional context when making permission decisions.
312
313
Attributes:
314
signal: Abort signal support (future feature)
315
suggestions: Permission suggestions from CLI
316
"""
317
318
signal: Any | None = None
319
"""Future: abort signal support.
320
321
Reserved for future use to allow canceling permission checks.
322
Currently always None.
323
"""
324
325
suggestions: list[PermissionUpdate] = field(default_factory=list)
326
"""Permission suggestions from CLI.
327
328
The CLI may provide suggested permission updates based on the
329
tool use context. Your callback can choose to apply these
330
suggestions or make its own decisions.
331
"""
332
```
333
334
### Can Use Tool Callback
335
336
Type alias for tool permission callbacks.
337
338
```python { .api }
339
CanUseTool = Callable[
340
[str, dict[str, Any], ToolPermissionContext],
341
Awaitable[PermissionResult]
342
]
343
"""
344
Tool permission callback type.
345
346
An async function that determines whether a specific tool use should
347
be allowed. Called before each tool execution.
348
349
Args:
350
tool_name: Name of the tool Claude wants to use
351
tool_input: Input parameters for the tool
352
context: Additional context including suggestions
353
354
Returns:
355
PermissionResult (either PermissionResultAllow or PermissionResultDeny)
356
357
Example:
358
async def can_use_tool(
359
tool_name: str,
360
tool_input: dict[str, Any],
361
context: ToolPermissionContext
362
) -> PermissionResult:
363
if tool_name == "Bash" and "rm" in tool_input.get("command", ""):
364
return PermissionResultDeny(
365
message="Dangerous command blocked",
366
interrupt=False
367
)
368
return PermissionResultAllow()
369
"""
370
```
371
372
## Usage Examples
373
374
### Using Permission Modes
375
376
```python
377
from claude_agent_sdk import ClaudeAgentOptions, query
378
379
# Default mode - prompt for dangerous operations
380
options = ClaudeAgentOptions(
381
permission_mode='default',
382
allowed_tools=["Read", "Write", "Bash"]
383
)
384
385
# Accept edits mode - auto-accept file changes
386
options = ClaudeAgentOptions(
387
permission_mode='acceptEdits',
388
allowed_tools=["Read", "Write", "Edit"]
389
)
390
391
# Plan mode - generate plan without execution
392
options = ClaudeAgentOptions(
393
permission_mode='plan',
394
allowed_tools=["Read", "Write", "Bash"]
395
)
396
397
# Bypass permissions - allow everything (use with caution!)
398
options = ClaudeAgentOptions(
399
permission_mode='bypassPermissions',
400
allowed_tools=["Read", "Write", "Bash"]
401
)
402
403
async for msg in query(prompt="Modify files", options=options):
404
print(msg)
405
```
406
407
### Basic Permission Callback
408
409
```python
410
from claude_agent_sdk import (
411
ClaudeAgentOptions, query, PermissionResultAllow,
412
PermissionResultDeny, ToolPermissionContext
413
)
414
415
async def can_use_tool(
416
tool_name: str,
417
tool_input: dict,
418
context: ToolPermissionContext
419
) -> PermissionResult:
420
"""Simple permission callback."""
421
# Allow Read operations
422
if tool_name == "Read":
423
return PermissionResultAllow()
424
425
# Block Bash commands
426
if tool_name == "Bash":
427
return PermissionResultDeny(
428
message="Bash commands not allowed",
429
interrupt=False
430
)
431
432
# Allow everything else
433
return PermissionResultAllow()
434
435
options = ClaudeAgentOptions(
436
allowed_tools=["Read", "Write", "Bash"],
437
can_use_tool=can_use_tool
438
)
439
440
async for msg in query(prompt="Analyze files", options=options):
441
print(msg)
442
```
443
444
### Blocking Dangerous Commands
445
446
```python
447
from claude_agent_sdk import (
448
ClaudeAgentOptions, query, PermissionResultAllow,
449
PermissionResultDeny, ToolPermissionContext
450
)
451
452
async def block_dangerous_commands(
453
tool_name: str,
454
tool_input: dict,
455
context: ToolPermissionContext
456
):
457
"""Block dangerous bash commands."""
458
if tool_name == "Bash":
459
command = tool_input.get("command", "")
460
461
dangerous_patterns = [
462
"rm -rf",
463
"dd if=",
464
"mkfs",
465
"> /dev/",
466
"chmod 777"
467
]
468
469
for pattern in dangerous_patterns:
470
if pattern in command:
471
return PermissionResultDeny(
472
message=f"Blocked dangerous command pattern: {pattern}",
473
interrupt=True # Stop immediately
474
)
475
476
return PermissionResultAllow()
477
478
options = ClaudeAgentOptions(
479
allowed_tools=["Bash"],
480
can_use_tool=block_dangerous_commands
481
)
482
483
async for msg in query(prompt="Clean up files", options=options):
484
print(msg)
485
```
486
487
### Modifying Tool Input
488
489
```python
490
from claude_agent_sdk import (
491
ClaudeAgentOptions, query, PermissionResultAllow, ToolPermissionContext
492
)
493
494
async def modify_bash_commands(
495
tool_name: str,
496
tool_input: dict,
497
context: ToolPermissionContext
498
):
499
"""Modify bash commands to add safety flags."""
500
if tool_name == "Bash":
501
command = tool_input.get("command", "")
502
503
# Add -i flag to rm commands for interactive prompts
504
if "rm" in command and "-i" not in command:
505
modified_command = command.replace("rm ", "rm -i ")
506
return PermissionResultAllow(
507
updated_input={"command": modified_command}
508
)
509
510
return PermissionResultAllow()
511
512
options = ClaudeAgentOptions(
513
allowed_tools=["Bash"],
514
can_use_tool=modify_bash_commands
515
)
516
517
async for msg in query(prompt="Delete old files", options=options):
518
print(msg)
519
```
520
521
### Path Restrictions
522
523
```python
524
from pathlib import Path
525
from claude_agent_sdk import (
526
ClaudeAgentOptions, query, PermissionResultAllow,
527
PermissionResultDeny, ToolPermissionContext
528
)
529
530
ALLOWED_PATHS = ["/home/user/project", "/tmp"]
531
532
async def enforce_path_restrictions(
533
tool_name: str,
534
tool_input: dict,
535
context: ToolPermissionContext
536
):
537
"""Restrict file operations to allowed paths."""
538
if tool_name in ["Read", "Write", "Edit"]:
539
file_path = tool_input.get("file_path", "")
540
abs_path = Path(file_path).resolve()
541
542
# Check if path is within allowed directories
543
allowed = any(
544
str(abs_path).startswith(str(Path(allowed_path).resolve()))
545
for allowed_path in ALLOWED_PATHS
546
)
547
548
if not allowed:
549
return PermissionResultDeny(
550
message=f"Access denied: {file_path} is outside allowed paths",
551
interrupt=False
552
)
553
554
return PermissionResultAllow()
555
556
options = ClaudeAgentOptions(
557
allowed_tools=["Read", "Write", "Edit"],
558
can_use_tool=enforce_path_restrictions
559
)
560
561
async for msg in query(prompt="Modify configuration", options=options):
562
print(msg)
563
```
564
565
### Time-Based Restrictions
566
567
```python
568
from datetime import datetime
569
from claude_agent_sdk import (
570
ClaudeAgentOptions, query, PermissionResultAllow,
571
PermissionResultDeny, ToolPermissionContext
572
)
573
574
async def business_hours_only(
575
tool_name: str,
576
tool_input: dict,
577
context: ToolPermissionContext
578
):
579
"""Only allow certain tools during business hours."""
580
hour = datetime.now().hour
581
582
# Restrict Bash during off-hours (before 9am or after 5pm)
583
if tool_name == "Bash" and (hour < 9 or hour >= 17):
584
return PermissionResultDeny(
585
message="Bash commands only allowed during business hours (9am-5pm)",
586
interrupt=False
587
)
588
589
return PermissionResultAllow()
590
591
options = ClaudeAgentOptions(
592
allowed_tools=["Bash", "Read", "Write"],
593
can_use_tool=business_hours_only
594
)
595
596
async for msg in query(prompt="Run deployment script", options=options):
597
print(msg)
598
```
599
600
### Logging All Tool Uses
601
602
```python
603
import logging
604
from claude_agent_sdk import (
605
ClaudeAgentOptions, query, PermissionResultAllow, ToolPermissionContext
606
)
607
608
logging.basicConfig(level=logging.INFO)
609
610
async def log_and_allow(
611
tool_name: str,
612
tool_input: dict,
613
context: ToolPermissionContext
614
):
615
"""Log all tool uses and allow them."""
616
logging.info(f"Tool use: {tool_name}")
617
logging.info(f"Input: {tool_input}")
618
619
return PermissionResultAllow()
620
621
options = ClaudeAgentOptions(
622
allowed_tools=["Read", "Write", "Bash"],
623
can_use_tool=log_and_allow
624
)
625
626
async for msg in query(prompt="Analyze project", options=options):
627
print(msg)
628
```
629
630
### User Confirmation
631
632
```python
633
from claude_agent_sdk import (
634
ClaudeAgentOptions, query, PermissionResultAllow,
635
PermissionResultDeny, ToolPermissionContext
636
)
637
638
async def ask_user_permission(
639
tool_name: str,
640
tool_input: dict,
641
context: ToolPermissionContext
642
):
643
"""Ask user for confirmation on file writes."""
644
if tool_name in ["Write", "Edit"]:
645
file_path = tool_input.get("file_path", "unknown")
646
647
print(f"\nClaude wants to modify: {file_path}")
648
print(f"Tool: {tool_name}")
649
650
# In a real application, you'd use a proper input method
651
# This is just for demonstration
652
response = input("Allow? (y/n): ")
653
654
if response.lower() != 'y':
655
return PermissionResultDeny(
656
message="User denied permission",
657
interrupt=False
658
)
659
660
return PermissionResultAllow()
661
662
options = ClaudeAgentOptions(
663
allowed_tools=["Read", "Write", "Edit"],
664
can_use_tool=ask_user_permission
665
)
666
667
async for msg in query(prompt="Fix bugs", options=options):
668
print(msg)
669
```
670
671
### Permission Updates
672
673
```python
674
from claude_agent_sdk import (
675
ClaudeAgentOptions, query, PermissionResultAllow,
676
PermissionUpdate, PermissionRuleValue, ToolPermissionContext
677
)
678
679
async def add_permission_after_use(
680
tool_name: str,
681
tool_input: dict,
682
context: ToolPermissionContext
683
):
684
"""Add permission rule after first use."""
685
if tool_name == "Read":
686
# After first read, allow all future reads automatically
687
return PermissionResultAllow(
688
updated_permissions=[
689
PermissionUpdate(
690
type="addRules",
691
rules=[PermissionRuleValue(tool_name="Read")],
692
behavior="allow",
693
destination="session" # Session only
694
)
695
]
696
)
697
698
return PermissionResultAllow()
699
700
options = ClaudeAgentOptions(
701
allowed_tools=["Read", "Write"],
702
can_use_tool=add_permission_after_use
703
)
704
705
async for msg in query(prompt="Read multiple files", options=options):
706
print(msg)
707
```
708
709
### Rate Limiting
710
711
```python
712
from datetime import datetime, timedelta
713
from claude_agent_sdk import (
714
ClaudeAgentOptions, query, PermissionResultAllow,
715
PermissionResultDeny, ToolPermissionContext
716
)
717
718
# Rate limiting state
719
tool_usage = {}
720
RATE_LIMIT = 10 # Max 10 uses per minute
721
722
async def rate_limit_tools(
723
tool_name: str,
724
tool_input: dict,
725
context: ToolPermissionContext
726
):
727
"""Rate limit tool usage."""
728
if tool_name not in tool_usage:
729
tool_usage[tool_name] = []
730
731
now = datetime.now()
732
733
# Remove old entries
734
tool_usage[tool_name] = [
735
ts for ts in tool_usage[tool_name]
736
if now - ts < timedelta(minutes=1)
737
]
738
739
# Check limit
740
if len(tool_usage[tool_name]) >= RATE_LIMIT:
741
return PermissionResultDeny(
742
message=f"Rate limit exceeded for {tool_name} ({RATE_LIMIT}/min)",
743
interrupt=False
744
)
745
746
# Record usage
747
tool_usage[tool_name].append(now)
748
749
return PermissionResultAllow()
750
751
options = ClaudeAgentOptions(
752
allowed_tools=["Bash"],
753
can_use_tool=rate_limit_tools
754
)
755
756
async for msg in query(prompt="Run many tests", options=options):
757
print(msg)
758
```
759
760
### Conditional Permissions
761
762
```python
763
from claude_agent_sdk import (
764
ClaudeAgentOptions, query, PermissionResultAllow,
765
PermissionResultDeny, ToolPermissionContext
766
)
767
768
class AppState:
769
def __init__(self):
770
self.readonly = False
771
self.trusted = False
772
773
state = AppState()
774
775
async def conditional_permissions(
776
tool_name: str,
777
tool_input: dict,
778
context: ToolPermissionContext
779
):
780
"""Apply conditional permissions based on app state."""
781
# Block writes in readonly mode
782
if state.readonly and tool_name in ["Write", "Edit", "Bash"]:
783
return PermissionResultDeny(
784
message="System is in readonly mode",
785
interrupt=False
786
)
787
788
# Allow everything if trusted
789
if state.trusted:
790
return PermissionResultAllow()
791
792
# Otherwise, apply normal rules
793
if tool_name == "Bash":
794
return PermissionResultDeny(
795
message="Bash not allowed in untrusted mode",
796
interrupt=False
797
)
798
799
return PermissionResultAllow()
800
801
options = ClaudeAgentOptions(
802
allowed_tools=["Read", "Write", "Bash"],
803
can_use_tool=conditional_permissions
804
)
805
806
# Enable readonly mode
807
state.readonly = True
808
809
async for msg in query(prompt="Analyze code", options=options):
810
print(msg)
811
```
812
813
### Using Permission Suggestions
814
815
```python
816
from claude_agent_sdk import (
817
ClaudeAgentOptions, query, PermissionResultAllow, ToolPermissionContext
818
)
819
820
async def use_suggestions(
821
tool_name: str,
822
tool_input: dict,
823
context: ToolPermissionContext
824
):
825
"""Use permission suggestions from CLI."""
826
# Check if CLI provided suggestions
827
if context.suggestions:
828
print(f"CLI suggests {len(context.suggestions)} permission updates")
829
830
# Accept suggestions
831
return PermissionResultAllow(
832
updated_permissions=context.suggestions
833
)
834
835
return PermissionResultAllow()
836
837
options = ClaudeAgentOptions(
838
allowed_tools=["Read", "Write"],
839
can_use_tool=use_suggestions
840
)
841
842
async for msg in query(prompt="Work on project", options=options):
843
print(msg)
844
```
845