> ## Documentation Index
> Fetch the complete documentation index at: https://docs.qoder.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Permission Control

The Qoder Agent SDK's permission control capabilities manage what the model can do within a single `query()` session. It can restrict which tools are visible to the model, set default authorization policies, delegate tool execution approval to the host application, and apply new rules to the current session after user authorization.

Permission control is not a standalone API but a set of configurations placed in `QoderAgentOptions`. Typically, you first decide which tools the model is allowed to use in this session, then decide under what conditions those tools can execute, and finally integrate runtime approval, dynamic rule updates, settings, or hooks as needed.

```python theme={null}
from qoder_agent_sdk import QoderAgentOptions, query


async for message in query(
    prompt="Inspect the repository and summarize risky changes.",
    options=QoderAgentOptions(
        cwd="/path/to/project",
        tools=["Read", "Grep", "Bash"],
        allowed_tools=["Read", "Grep"],
        disallowed_tools=["Bash"],
        permission_mode="default",
    ),
):
    print(message)
```

The example above expresses a common policy: the model can see `Read`, `Grep`, and `Bash`; `Read` and `Grep` are pre-authorized; `Bash` is denied. In real projects, you can further add `can_use_tool` to route unauthorized operations to your product UI, approval system, or risk control service.

<div id="capability-overview" />

<div id="capabilityoverview" />

## Capability Overview

The permission-related options on `query()` fall into roughly four categories. The first determines the default policy — for example, plan mode, auto-allow edits, or no interactive prompts. The second determines tool scope and tool rules. The third lets the host application participate in runtime approval. The fourth covers more advanced settings, hooks, and MCP tool policies.

| Problem to solve                                       | Recommended entry point              | Description                                                         |
| ------------------------------------------------------ | ------------------------------------ | ------------------------------------------------------------------- |
| Set the session's default permission behavior          | `permission_mode`                    | Determines how tool calls are handled when no explicit rule matches |
| Explicitly confirm skipping permission checks          | `allow_dangerously_skip_permissions` | Use only with `bypassPermissions` or `yolo`                         |
| Restrict tools visible in this session                 | `tools`                              | Tools not included are not provided to the model                    |
| Pre-authorize specific tools                           | `allowed_tools`                      | Matched tools typically skip authorization prompts                  |
| Deny specific tools                                    | `disallowed_tools`                   | Matched tools are denied; takes precedence over allow               |
| Have the host application approve tool calls           | `can_use_tool`                       | The SDK host returns allow or deny at runtime                       |
| Delegate approval to an external prompt tool           | `permission_prompt_tool_name`        | For environments that already provide a permission prompt tool      |
| Update the current session's rules after authorization | `PermissionUpdate`                   | Common for "allow once" or "always allow this session"              |
| Allow access to directories outside `cwd`              | `add_dirs`                           | Extend the directory scope accessible in this session               |
| Provide permission rules from settings                 | `settings`                           | Suitable for static permission configuration at session start       |
| Intercept or audit during the lifecycle                | `hooks`                              | Suitable for advanced interception, auditing, and alerting          |
| Declare tool policy on an MCP server                   | MCP tool policy                      | Declare per-tool allow/ask/deny in the MCP server config            |

Common selection patterns:

* Read-only code review with no modifications: set `tools`, pre-authorize `Read` and `Grep` via `allowed_tools`, and deny `Write`, `Edit`, `Bash` via `disallowed_tools`.
* Plan first without making actual changes: use `permission_mode="plan"`.
* Auto-approve safe edits: use `permission_mode="acceptEdits"` or `"auto"`.
* Show your own approval dialog: use `can_use_tool`.
* Let the user select "always allow this session": return `updated_permissions` from the allow result of `can_use_tool`.
* No interactive prompts at all, deny anything not pre-authorized: use `permission_mode="dontAsk"`.
* Access shared directories outside a monorepo: use `add_dirs`.

To stay focused on permission configuration, later examples omit the message-iteration code; in real usage you still need to consume the async message stream returned by `query()`. The Python SDK can use the local `qodercli` login state by default; if your application needs explicit authentication, pass your own auth configuration via `QoderAgentOptions.auth`.

<div id="quick-start-have-the-host-application-approve-tool-calls" />

<div id="quickstarthavethehostapplicationapprovetoolcalls" />

## Quick Start: Have the Host Application Approve Tool Calls

When you need to route tool calls through your own approval logic, use `can_use_tool`. The SDK passes the tool name, tool input, and a set of displayable approval information to your callback at runtime. When the callback returns `PermissionResultAllow`, the tool continues executing; when it returns `PermissionResultDeny`, the tool is rejected.

```python theme={null}
from typing import Any

from qoder_agent_sdk import create_sdk_mcp_server, query, tool
from qoder_agent_sdk.types import (
    PermissionResultAllow,
    PermissionResultDeny,
    QoderAgentOptions,
    ToolPermissionContext,
)


@tool("read_order", "Read an order by ID.", {"order_id": str})
async def read_order(args: dict[str, Any]) -> dict[str, Any]:
    return {"content": [{"type": "text", "text": f"order:{args['order_id']}"}]}


server = create_sdk_mcp_server(name="orders", tools=[read_order])


async def can_use_tool(
    tool_name: str,
    input_data: dict[str, Any],
    context: ToolPermissionContext,
):
    if tool_name != "mcp__orders__read_order":
        return PermissionResultDeny(
            message="Only order reads are allowed in this workflow."
        )

    return PermissionResultAllow(updated_input=input_data)


async for _ in query(
    prompt="Read order 1001.",
    options=QoderAgentOptions(
        mcp_servers={"orders": server},
        permission_mode="default",
        can_use_tool=can_use_tool,
    ),
):
    pass
```

In this example, `read_order` is an SDK MCP tool. When the model invokes it, the full tool name will be `mcp__orders__read_order`. `can_use_tool` only allows this tool to execute and returns the original input as `updated_input`.

<div id="controlling-default-policy-permission_mode" />

<div id="controllingdefaultpolicypermission_mode" />

## Controlling Default Policy: permission\_mode

`permission_mode` determines the session's default permission policy. Use it to express "what mode is this session overall in" — for example, plan first, auto-accept edits, deny without asking, or skip permission checks in controlled environments.

```python theme={null}
QoderAgentOptions(
    cwd="/path/to/project",
    permission_mode="plan",
)
```

`plan` mode is designed for having the model produce a plan first, with no actual changes by default.

| Mode                | Behavior                                                                                                                         |
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `default`           | Standard permission behavior. Tool calls are processed according to tools, allow/deny rules, dynamic approval, or runtime policy |
| `acceptEdits`       | Auto-accepts file edit operations; use when workspace modification is confirmed                                                  |
| `bypassPermissions` | Skips permission checks; must also set `allow_dangerously_skip_permissions=True`                                                 |
| `yolo`              | Compatibility alias for `bypassPermissions`; also requires `allow_dangerously_skip_permissions=True`                             |
| `plan`              | Plan mode; designed for producing an execution plan first, with no actual changes by default                                     |
| `dontAsk`           | No interactive prompts. Operations not pre-authorized or allowed by rules are denied                                             |
| `auto`              | Runtime capability automatically determines allow or deny. Safe in-workspace file edits may be auto-approved                     |

To switch modes within the same session, use `QoderSDKClient`:

```python theme={null}
from qoder_agent_sdk import QoderSDKClient


async with QoderSDKClient(
    options=QoderAgentOptions(
        cwd="/path/to/project",
        permission_mode="plan",
    )
) as client:
    await client.set_permission_mode("default")
```

`bypassPermissions` and `yolo` are both high-risk modes. The SDK requires explicitly passing `allow_dangerously_skip_permissions=True` to prevent callers from accidentally turning a normal session into one that skips permission checks.

```python theme={null}
QoderAgentOptions(
    cwd="/path/to/project",
    permission_mode="bypassPermissions",
    allow_dangerously_skip_permissions=True,
)
```

<div id="controlling-tool-scope-tools-allowed_tools-disallowed_tools" />

<div id="controllingtoolscopetoolsallowed_toolsdisallowed_tools" />

## Controlling Tool Scope: tools, allowed\_tools, disallowed\_tools

Tool control answers "which tools can the model see, and which tools are allowed or denied by default." These three fields often appear together but have different semantics.

```python theme={null}
QoderAgentOptions(
    cwd="/path/to/project",
    tools=["Read", "Grep", "Bash"],
    allowed_tools=["Read", "Grep"],
    disallowed_tools=["Bash"],
)
```

This configuration means: only provide `Read`, `Grep`, and `Bash` tools for this session; `Read` and `Grep` are pre-authorized; `Bash` is denied — even if the model wants to call it, it will not execute.

| Field              | Purpose                                          | Suitable Scenario                         |
| ------------------ | ------------------------------------------------ | ----------------------------------------- |
| `tools`            | Restrict the available tool set for this session | Narrowing model capability boundaries     |
| `allowed_tools`    | Add allow rules                                  | Let low-risk tools skip repeated approval |
| `disallowed_tools` | Add deny rules                                   | Explicitly deny high-risk tools           |

When the same tool matches both allow and deny, deny takes priority. This ensures deny rules cannot be bypassed by broader allow rules.

MCP tools also use full tool name matching. For example, with an SDK MCP server named `orders` and a tool named `read_order`, the full tool name is `mcp__orders__read_order`.

```python theme={null}
QoderAgentOptions(
    mcp_servers={"orders": server},
    allowed_tools=["mcp__orders__read_order"],
)
```

<div id="runtime-approval-can_use_tool" />

<div id="runtimeapprovalcan_use_tool" />

<div id="canusetool" />

<div id="can-use-tool" />

<div id="can_use_tool" />

## Runtime Approval: can\_use\_tool

`can_use_tool` is designed for scenarios where the host application needs to participate in approval. For example, you want to display permission requests in your own UI for the user to click "allow once," "always allow this session," or "deny"; or you need to call an enterprise risk control service to determine whether a command can execute.

```python theme={null}
async def can_use_tool(
    tool_name: str,
    input_data: dict[str, Any],
    context: ToolPermissionContext,
):
    show_approval_dialog(
        title=context.title or tool_name,
        description=context.description,
        input_data=input_data,
    )

    approved = await wait_for_user_approval(context.signal)

    if not approved:
        return PermissionResultDeny(message="Rejected by user.")

    return PermissionResultAllow(updated_input=input_data)


QoderAgentOptions(
    cwd="/path/to/project",
    permission_mode="default",
    can_use_tool=can_use_tool,
)
```

The `can_use_tool` signature:

```python theme={null}
CanUseTool = Callable[
    [str, dict[str, Any], ToolPermissionContext],
    Awaitable[PermissionResult],
]
```

Key field explanations:

| Field                                            | Description                                                                                 |
| ------------------------------------------------ | ------------------------------------------------------------------------------------------- |
| `tool_name`                                      | Full tool name, e.g., `Read`, `Bash`, `mcp__orders__read_order`                             |
| `input_data`                                     | Original parameters for this tool invocation                                                |
| `context.tool_use_id`                            | This tool invocation's ID                                                                   |
| `context.signal`                                 | Set when the authorization request is cancelled; UI or remote approval should listen for it |
| `context.title` / `display_name` / `description` | Human-readable text generated at runtime, suitable for use directly in approval UI          |
| `context.suggestions`                            | Permission update suggestions from runtime; can be used for "always allow this session"     |
| `context.blocked_path`                           | Restricted path in path-related authorization scenarios                                     |
| `context.decision_reason`                        | Human-readable approval reason from runtime; can be used for display or audit               |
| `context.agent_id`                               | Agent ID when a sub-Agent initiates the tool call                                           |

Returning `PermissionResultAllow` means the tool continues executing:

```python theme={null}
return PermissionResultAllow(updated_input=input_data)
```

`updated_input` is the final parameters the tool receives. You can return them as-is, or modify them after approval — for example, add a tenant ID to queries, rewrite paths to a safe directory, or remove disallowed fields.

Returning `PermissionResultDeny` means the tool is rejected:

```python theme={null}
return PermissionResultDeny(
    message="This command is not allowed in the current workspace."
)
```

`deny.message` is required; it becomes part of the denial reason, available to the model, logs, or host application. When the SDK receives a CLI authorization request but no `can_use_tool` is configured, it returns an error rather than defaulting to allow.

When the permission system directly denies a tool call, a structured permission denial message may appear in the message stream:

```python theme={null}
from qoder_agent_sdk import SDKPermissionDeniedMessage


if isinstance(message, SDKPermissionDeniedMessage):
    print(
        message.tool_name,
        message.tool_use_id,
        message.message,
        message.decision_reason,
        message.decision_reason_type,
    )
```

These messages are common in `permission_mode="dontAsk"`, auto-deny, or rule-deny scenarios. Host applications can use them to update UI state or write audit logs.

<div id="updating-permissions-within-a-session-permissionupdate" />

<div id="updatingpermissionswithinasessionpermissionupdate" />

## Updating Permissions Within a Session: PermissionUpdate

`PermissionUpdate` is used to update permission rules in the current session after an approval. The most common scenario is when a user selects "always allow this session" in the approval UI. You can return the runtime-provided `suggestions` as-is, or construct explicit rules yourself.

```python theme={null}
async def can_use_tool(
    tool_name: str,
    input_data: dict[str, Any],
    context: ToolPermissionContext,
):
    decision = await show_approval_dialog(
        tool_name=tool_name,
        suggestions=context.suggestions,
    )

    if decision == "always-allow-this-session":
        return PermissionResultAllow(
            updated_input=input_data,
            updated_permissions=context.suggestions,
        )

    if decision == "allow-once":
        return PermissionResultAllow(updated_input=input_data)

    return PermissionResultDeny(message="Rejected by user.")
```

You can also construct rules directly:

```python theme={null}
from qoder_agent_sdk.types import PermissionRuleValue, PermissionUpdate


return PermissionResultAllow(
    updated_input=input_data,
    updated_permissions=[
        PermissionUpdate(
            type="addRules",
            behavior="allow",
            destination="session",
            rules=[PermissionRuleValue(tool_name="mcp__orders__read_order")],
        )
    ],
)
```

Supported update types:

| Type                | Purpose                           |
| ------------------- | --------------------------------- |
| `addRules`          | Append allow, ask, or deny rules  |
| `replaceRules`      | Replace rules                     |
| `removeRules`       | Remove rules                      |
| `setMode`           | Switch permission mode            |
| `addDirectories`    | Append allowed access directories |
| `removeDirectories` | Remove directory authorizations   |

Recommended to write dynamic permission updates to the current session:

```python theme={null}
destination = "session"
```

`session` only affects permission checks for the remainder of the current query session. When persistence to local, project, or user-level configuration is needed, prefer using the settings management workflow rather than relying on dynamic updates in a single tool approval callback.

<div id="accessing-additional-directories-add_dirs" />

<div id="accessingadditionaldirectoriesadd_dirs" />

## Accessing Additional Directories: add\_dirs

By default, the session uses `cwd` as the primary working directory. When the model needs to read or modify directories outside `cwd`, explicitly pass `add_dirs`.

```python theme={null}
QoderAgentOptions(
    cwd="/repo/app",
    add_dirs=["/repo/packages/shared"],
)
```

This configuration means the session's main working directory is `/repo/app`, and the model is also allowed to access `/repo/packages/shared`. This works well for monorepos, cross-repository debugging, shared library investigation, and similar scenarios.

During execution, directory authorization can also be adjusted via `PermissionUpdate`:

```python theme={null}
return PermissionResultAllow(
    updated_input=input_data,
    updated_permissions=[
        PermissionUpdate(
            type="addDirectories",
            destination="session",
            directories=["/repo/packages/shared"],
        )
    ],
)
```

Directory authorization is part of the permission boundary. Don't add broad directories to `add_dirs` as a universal default; the safer approach is to add the minimal directory set needed per task.

<div id="external-authorization-tool-permission_prompt_tool_name" />

<div id="externalauthorizationtoolpermission_prompt_tool_name" />

## External Authorization Tool: permission\_prompt\_tool\_name

`permission_prompt_tool_name` is used to delegate permission requests to a permission prompt tool in the runtime environment, rather than implementing `can_use_tool` in the SDK host. Use this when you have existing external approval tools, remote execution environments, or unified permission gateways.

```python theme={null}
QoderAgentOptions(
    permission_prompt_tool_name="mcp__permission_server__approve",
)
```

Three things to note:

* `permission_prompt_tool_name` must be a prompt tool name recognizable by the current runtime environment.
* `permission_prompt_tool_name` and `can_use_tool` are mutually exclusive; they cannot be passed simultaneously.
* When the SDK host needs to handle approval itself, prefer `can_use_tool`.

The permission prompt tool receives the following input:

```python theme={null}
{
    "tool_name": "mcp__orders__read_order",
    "input": {"order_id": "1001"},
    "tool_use_id": "toolu_...",
}
```

It needs to return a permission result:

```python theme={null}
{
    "behavior": "allow",
    "updatedInput": {"order_id": "1001"},
    "updatedPermissions": [],
    "toolUseID": "toolu_...",
}

{
    "behavior": "deny",
    "message": "Denied by policy.",
    "interrupt": True,
    "toolUseID": "toolu_...",
}
```

`allow.updatedInput` is the final parameters used when executing the tool. If you want to keep the original parameters, return the received `input` as-is. `deny.message` is required. `interrupt=True` means deny and also interrupt the current Agent flow.

<div id="using-settings-to-provide-permission-rules" />

<div id="usingsettingstoprovidepermissionrules" />

## Using settings to Provide Permission Rules

`settings` is ideal for providing static permission configuration before the session starts. It is more appropriate than `can_use_tool` for expressing "what this project allows by default, what it denies, and what additional directories exist."

```python theme={null}
QoderAgentOptions(
    cwd="/path/to/project",
    settings={
        "permissions": {
            "allow": ["Read", "Grep"],
            "deny": ["Bash"],
            "ask": ["Write"],
            "defaultMode": "default",
            "additionalDirectories": ["/path/to/shared-lib"],
        }
    },
)
```

Field descriptions:

| Field                                      | Description                                           |
| ------------------------------------------ | ----------------------------------------------------- |
| `permissions.allow`                        | Allow rules                                           |
| `permissions.deny`                         | Deny rules                                            |
| `permissions.ask`                          | Always-ask rules                                      |
| `permissions.defaultMode`                  | Default permission mode                               |
| `permissions.disableBypassPermissionsMode` | Set to `"disable"` to disable bypass permissions mode |
| `permissions.additionalDirectories`        | Additional accessible directories                     |

If your application reads and applies the default permission mode from settings, consider performing your own product-level confirmation before executing high-risk modes. Modes like `bypassPermissions` and `yolo` should only appear in explicitly trusted environments.

<div id="using-hooks-for-advanced-interception-and-auditing" />

<div id="usinghooksforadvancedinterceptionandauditing" />

## Using hooks for Advanced Interception and Auditing

Hooks are suitable when you have already integrated the SDK hooks system and want finer-grained control in the tool lifecycle. Compared to `can_use_tool`, hooks are better suited for cross-cutting concerns such as auditing, alerting, unified interception, and recording denial reasons.

```python theme={null}
from qoder_agent_sdk import HookMatcher


async def pre_tool_use_hook(inp, tool_use_id, context):
    return {
        "hookSpecificOutput": {
            "hookEventName": "PreToolUse",
            "permissionDecision": "deny",
            "permissionDecisionReason": "Shell commands are disabled here.",
        }
    }


QoderAgentOptions(
    cwd="/path/to/project",
    hooks={
        "PreToolUse": [
            HookMatcher(matcher="Bash", hooks=[pre_tool_use_hook]),
        ],
    },
)
```

The main permission-related hooks are three types:

| Hook                | Trigger Timing                     | Common Use                                               |
| ------------------- | ---------------------------------- | -------------------------------------------------------- |
| `PreToolUse`        | Before tool invocation             | Pre-allow, deny, request ask, or pass to subsequent flow |
| `PermissionRequest` | When entering a permission request | Return allow or deny directly before the normal prompt   |
| `PermissionDenied`  | After permission is denied         | Auditing, alerting, recording denial reasons             |

`PreToolUse` can return:

```python theme={null}
{
    "hookSpecificOutput": {
        "hookEventName": "PreToolUse",
        "permissionDecision": "deny",
        "permissionDecisionReason": "...",
        "updatedInput": {},
    }
}
```

Where `permissionDecision` can be `"allow"`, `"deny"`, `"ask"`, or `"defer"`.

`PermissionRequest` can return a permission result similar to tool approval:

```python theme={null}
{
    "hookSpecificOutput": {
        "hookEventName": "PermissionRequest",
        "decision": {
            "behavior": "deny",
            "message": "Denied by policy.",
        },
    }
}
```

`PermissionDenied` is typically used for observing results, not for allowing tools. Its input includes the denied tool name, tool input, tool invocation ID, and denial reason.

<div id="mcp-tool-policy" />

<div id="mcptoolpolicy" />

## MCP Tool Policy

If the permission policy naturally belongs to a specific MCP server, you can declare tool-level permission policy directly in the MCP server config. This way the policy follows the MCP server configuration rather than being scattered in global `allowed_tools` or `disallowed_tools`.

```python theme={null}
import os


QoderAgentOptions(
    mcp_servers={
        "repo_tools": {
            "type": "http",
            "url": os.environ["REPO_TOOLS_MCP_URL"],
            "tools": [
                {"name": "search", "permission_policy": "always_allow"},
                {"name": "write_file", "permission_policy": "always_ask"},
                {"name": "delete_file", "permission_policy": "always_deny"},
            ],
        }
    },
)
```

Policy meanings:

| Policy         | Behavior                               |
| -------------- | -------------------------------------- |
| `always_allow` | Matched tool is directly allowed       |
| `always_ask`   | Matched tool enters authorization flow |
| `always_deny`  | Matched tool is directly denied        |

`name` can be the MCP tool's original name or the full tool name, e.g., `mcp__repo_tools__search`. During actual matching, the runtime maps policy names to the current MCP tool invocation.
