Skip to main content

Permissions

The SDK gives you fine-grained control over which tools the agent can execute. Using the can_use_tool callback, your application can approve, deny, or even modify tool inputs before they run.

How It Works

When the agent wants to use a tool and the permission mode requires approval, the SDK calls your can_use_tool function with:

  1. tool_name — the concrete tool being invoked (for example "bash", "read_file", or "replace_by_string")
  2. tool_input — the arguments the agent passed to the tool
  3. ctx — a ToolPermissionContext with metadata (call ID, confirmation details)

Your callback returns either PermissionResult.allow() or PermissionResult.deny(reason).

note

can_use_tool receives individual tool names such as "read_file", "bash", "create_file", or "replace_by_string" — not group names.

Basic Callback

from adal_agent_sdk import (
AdalAgentClient,
AdalAgentOptions,
PermissionResult,
ToolPermissionContext,
)


async def can_use_tool(
tool_name: str,
tool_input: dict,
ctx: ToolPermissionContext,
):
# Block dangerous commands
if tool_name == "bash":
command = tool_input.get("command", "")
if "rm -rf" in command:
return PermissionResult.deny("Blocked dangerous shell command")

# Allow everything else
return PermissionResult.allow()


options = AdalAgentOptions(
workspace=".",
permission_mode="default",
can_use_tool=can_use_tool,
)

Modifying Tool Input

You can approve a tool call while changing its arguments:

async def can_use_tool(tool_name, tool_input, ctx):
if tool_name == "bash":
command = tool_input.get("command", "")

# Force quiet mode on pytest
if command.strip() == "pytest":
return PermissionResult.allow(
updated_input={**tool_input, "command": "pytest -q"}
)

# Add timeout to long-running commands
if "npm install" in command:
return PermissionResult.allow(
updated_input={**tool_input, "timeout": 120}
)

return PermissionResult.allow()

Using Context

The ToolPermissionContext provides additional metadata:

async def can_use_tool(tool_name, tool_input, ctx):
print(f"Tool call ID: {ctx.tool_call_id}")

# Check confirmation details (e.g., diff preview for edits)
if ctx.confirmation:
diff = ctx.confirmation.get("diff", "")
if "+++ secret" in diff:
return PermissionResult.deny("Edit touches sensitive file")

return PermissionResult.allow()

Common Patterns

Allowlist Specific Tools

ALLOWED = {"read_file", "grep", "glob"}

async def can_use_tool(tool_name, tool_input, ctx):
if tool_name in ALLOWED:
return PermissionResult.allow()
return PermissionResult.deny(f"{tool_name} not in allowlist")

Block File Writes Outside a Directory

import os

SAFE_DIR = "/tmp/sandbox"

async def can_use_tool(tool_name, tool_input, ctx):
if tool_name in ("create_file", "replace_by_string", "rewrite_file"):
path = tool_input.get("file_path", "")
abs_path = os.path.abspath(path)
if not abs_path.startswith(SAFE_DIR):
return PermissionResult.deny(f"Writes only allowed in {SAFE_DIR}")

return PermissionResult.allow()

Log All Tool Calls

import logging

logger = logging.getLogger("adal_audit")

async def can_use_tool(tool_name, tool_input, ctx):
logger.info(f"Tool: {tool_name}, Input: {tool_input}, ID: {ctx.tool_call_id}")
return PermissionResult.allow()

Interactive Approval (Terminal)

async def can_use_tool(tool_name, tool_input, ctx):
print(f"\n⚠️ Agent wants to run: {tool_name}")
print(f" Input: {tool_input}")
answer = input(" Allow? [y/N]: ").strip().lower()

if answer == "y":
return PermissionResult.allow()
return PermissionResult.deny("User denied")

Permission Modes

The permission_mode option controls when can_use_tool is called:

ModeBehavior
"default"All tool calls trigger can_use_tool
"acceptEdits"Read-only tools auto-approved; writes trigger callback
"yolo"All tools auto-approved (callback never called)
tip

Use "yolo" for fully automated pipelines where you trust the prompt. Use "default" with a callback for production systems where you need an audit trail or safety guardrails.

Error Handling

If your callback raises an exception, the SDK automatically denies the tool call with the exception message:

async def can_use_tool(tool_name, tool_input, ctx):
# If this raises, the tool is denied with the error message
validate_input(tool_input)
return PermissionResult.allow()

Without a Callback

If permission_mode="default" is set but no can_use_tool callback is provided, all tool calls are denied by default. Always provide a callback when using "default" mode.