or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

agent-definitions.mdagents.mdclient.mdconfiguration-options.mdcontent-blocks.mdcore-query-interface.mdcustom-tools.mderror-handling.mderrors.mdhook-system.mdhooks.mdindex.mdmcp-config.mdmcp-server-configuration.mdmessages-and-content.mdmessages.mdoptions.mdpermission-control.mdpermissions.mdquery.mdtransport.md
COMPLETION_SUMMARY.md

permission-control.mddocs/

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