Skip to main content

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.

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 query({ options }). 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.
const messages = query({
  prompt: 'Inspect the repository and summarize risky changes.',
  options: {
    auth: accessTokenFromEnv(),
    cwd: '/path/to/project',
    tools: ['Read', 'Grep', 'Bash'],
    allowedTools: ['Read', 'Grep'],
    disallowedTools: ['Bash'],
    permissionMode: 'default',
  },
});

for await (const message of messages) {
  console.log(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 canUseTool to route unauthorized operations to your product UI, approval system, or risk control service.

Quick Start: Host Application Approving Tool Calls

When you need to route tool calls through your own approval logic, use canUseTool. The SDK passes the tool name, tool input, and a set of displayable approval information to your callback at runtime. When the callback returns allow, the tool continues executing; when it returns deny, the tool is rejected.
const readOrder = tool(
  'read_order',
  'Read an order by ID.',
  { orderId: z.string() },
  async ({ orderId }) => ({
    content: [{ type: 'text', text: `order:${orderId}` }],
  }),
);

const server = createSdkMcpServer({
  name: 'orders',
  tools: [readOrder],
});

query({
  prompt: 'Read order 1001.',
  options: {
    auth: accessTokenFromEnv(),
    mcpServers: { orders: server },
    permissionMode: 'default',
    async canUseTool(toolName, input, options) {
      if (toolName !== 'mcp__orders__read_order') {
        return {
          behavior: 'deny',
          message: 'Only order reads are allowed in this workflow.',
          toolUseID: options.toolUseID,
        };
      }

      return {
        behavior: 'allow',
        updatedInput: input,
        toolUseID: options.toolUseID,
      };
    },
  },
});
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. canUseTool only allows this tool to execute and returns the original input as updatedInput. Returning toolUseID lets the runtime accurately match the approval result to this specific tool invocation.

Controlling Default Policy: permissionMode

permissionMode determines the session’s default permission policy. Use this to express “what mode is this session overall in,” such as planning first, auto-accepting edits, denying without asking, or skipping permission checks in controlled environments.
query({
  prompt: 'Plan the migration. Do not edit files yet.',
  options: {
    auth: accessTokenFromEnv(),
    cwd: '/path/to/project',
    permissionMode: 'plan',
    planModeInstructions: 'Only produce a concise migration checklist.',
  },
});
plan mode is designed for having the model produce a plan first. planModeInstructions can override the plan mode workflow instructions, having the model output the plan in your desired format.
ModeBehavior
defaultStandard permission behavior. Tool calls are processed according to tools, allow/deny rules, dynamic approval, or runtime policy
acceptEditsAuto-accepts file edit operations; use this when workspace modification is confirmed
bypassPermissionsSkips permission checks; must also set allowDangerouslySkipPermissions: true
yoloCompatibility alias for bypassPermissions; also requires allowDangerouslySkipPermissions: true
planPlan mode; designed for producing an execution plan first; no actual changes by default
dontAskNo interactive prompts. Operations not pre-authorized or allowed by rules are denied
autoRuntime capability automatically determines allow or deny. Safe in-workspace file edits may be auto-approved
To switch modes within the same session, use the returned Query object:
const q = query({
  prompt: 'Plan the change first.',
  options: {
    auth: accessTokenFromEnv(),
    cwd: '/path/to/project',
    permissionMode: 'plan',
  },
});

await q.setPermissionMode('default');
bypassPermissions and yolo are both high-risk modes. The SDK requires explicitly passing allowDangerouslySkipPermissions: true to prevent callers from accidentally turning a normal session into one that skips permission checks.
query({
  prompt: 'Run the trusted local maintenance task.',
  options: {
    auth: accessTokenFromEnv(),
    cwd: '/path/to/project',
    permissionMode: 'bypassPermissions',
    allowDangerouslySkipPermissions: true,
  },
});

Controlling Tool Scope: tools, allowedTools, disallowedTools

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.
query({
  prompt: 'Inspect the repo without modifying it.',
  options: {
    auth: accessTokenFromEnv(),
    cwd: '/path/to/project',
    tools: ['Read', 'Grep', 'Bash'],
    allowedTools: ['Read', 'Grep'],
    disallowedTools: ['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 won’t execute.
FieldPurposeSuitable Scenario
toolsRestrict the available tool set for this sessionNarrowing model capability boundaries
allowedToolsAdd allow rulesLet low-risk tools skip repeated approval
disallowedToolsAdd deny rulesExplicitly 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 SDK MCP server named orders and tool named read_order, the full tool name is mcp__orders__read_order.
query({
  prompt: 'Read order 1001.',
  options: {
    auth: accessTokenFromEnv(),
    mcpServers: { orders: server },
    allowedTools: ['mcp__orders__read_order'],
  },
});

Runtime Approval: canUseTool

canUseTool 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.
query({
  prompt: 'Create a changelog file for this release.',
  options: {
    auth: accessTokenFromEnv(),
    cwd: '/path/to/project',
    permissionMode: 'default',
    async canUseTool(toolName, input, options) {
      showApprovalDialog({
        title: options.title ?? toolName,
        description: options.description,
        input,
      });

      const approved = await waitForUserApproval(options.signal);

      if (!approved) {
        return {
          behavior: 'deny',
          message: 'Rejected by user.',
          toolUseID: options.toolUseID,
        };
      }

      return {
        behavior: 'allow',
        updatedInput: input,
        toolUseID: options.toolUseID,
      };
    },
  },
});
The canUseTool signature:
type CanUseTool = (
  toolName: string,
  input: Record<string, unknown>,
  options: {
    signal: AbortSignal;
    suggestions?: PermissionUpdate[];
    blockedPath?: string;
    decisionReason?: string;
    title?: string;
    displayName?: string;
    description?: string;
    toolUseID: string;
    agentID?: string;
  },
) => Promise<PermissionResult>;
Key field explanations:
FieldDescription
toolNameFull tool name, e.g., Read, Bash, mcp__orders__read_order
inputOriginal parameters for this tool invocation
options.toolUseIDThis tool invocation’s ID; recommended to include when returning approval results
options.signalAborts when the authorization request is cancelled; UI or remote approval should listen for it
options.title / displayName / descriptionHuman-readable text generated at runtime; can be used directly in approval UI
options.suggestionsPermission update suggestions from runtime; can be used for “always allow this session”
options.blockedPathRestricted path in path-related authorization scenarios
options.decisionReasonHuman-readable approval reason from runtime; can be used for display or audit
options.agentIDAgent ID when a sub-Agent initiates the tool call
Returning allow means the tool continues executing:
return {
  behavior: 'allow',
  updatedInput: input,
  toolUseID: options.toolUseID,
};
updatedInput 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 deny means the tool is rejected:
return {
  behavior: 'deny',
  message: 'This command is not allowed in the current workspace.',
  toolUseID: options.toolUseID,
};
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 canUseTool 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:
type SDKPermissionDeniedMessage = {
  type: 'system';
  subtype: 'permission_denied';
  tool_name: string;
  tool_use_id?: string;
  message?: string;
  decision_reason?: string;
  decision_reason_type?: string;
};
These messages are common in permissionMode: 'dontAsk', auto-deny, or rule-deny scenarios. Host applications can use them to update UI state or write audit logs.

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.
async function canUseTool(toolName, input, options) {
  const decision = await showApprovalDialog({
    toolName,
    suggestions: options.suggestions,
  });

  if (decision === 'always-allow-this-session') {
    return {
      behavior: 'allow',
      updatedInput: input,
      toolUseID: options.toolUseID,
      updatedPermissions: options.suggestions,
    };
  }

  if (decision === 'allow-once') {
    return {
      behavior: 'allow',
      updatedInput: input,
      toolUseID: options.toolUseID,
    };
  }

  return {
    behavior: 'deny',
    message: 'Rejected by user.',
    toolUseID: options.toolUseID,
  };
}
You can also construct rules directly:
return {
  behavior: 'allow',
  updatedInput: input,
  toolUseID: options.toolUseID,
  updatedPermissions: [
    {
      type: 'addRules',
      behavior: 'allow',
      destination: 'session',
      rules: [{ toolName: 'mcp__orders__read_order' }],
    },
  ],
};
Supported update types:
TypePurpose
addRulesAppend allow, ask, or deny rules
replaceRulesReplace rules
removeRulesRemove rules
setModeSwitch permission mode
addDirectoriesAppend allowed access directories
removeDirectoriesRemove directory authorizations
Recommended to write dynamic permission updates to the current session:
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.

Accessing Additional Directories: additionalDirectories

By default, the session uses cwd as the primary working directory. When the model needs to read or modify directories outside cwd, explicitly pass additionalDirectories.
query({
  prompt: 'Inspect the app and the shared package.',
  options: {
    auth: accessTokenFromEnv(),
    cwd: '/repo/app',
    additionalDirectories: ['/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:
return {
  behavior: 'allow',
  updatedInput: input,
  toolUseID: options.toolUseID,
  updatedPermissions: [
    {
      type: 'addDirectories',
      destination: 'session',
      directories: ['/repo/packages/shared'],
    },
  ],
};
Directory authorization is part of the permission boundary. Don’t add broad directories to additionalDirectories as a universal default; the safer approach is to add the minimal directory set needed per task.

External Authorization Tool: permissionPromptToolName

permissionPromptToolName is used to delegate permission requests to a permission prompt tool in the runtime environment, rather than implementing canUseTool in the SDK host. Use this when you have existing external approval tools, remote execution environments, or unified permission gateways.
query({
  prompt: 'Run the task.',
  options: {
    auth: accessTokenFromEnv(),
    permissionPromptToolName: 'mcp__permission_server__approve',
  },
});
Three things to note:
  • permissionPromptToolName must be a prompt tool name recognizable by the current runtime environment.
  • permissionPromptToolName and canUseTool are mutually exclusive; they cannot be passed simultaneously.
  • When the SDK host needs to handle approval itself, prefer canUseTool.
The permission prompt tool receives the following input:
type PermissionPromptToolInput = {
  tool_name: string;
  input: Record<string, unknown>;
  tool_use_id?: string;
};
It needs to return a permission result:
type PermissionPromptToolOutput =
  | {
      behavior: 'allow';
      updatedInput: Record<string, unknown>;
      updatedPermissions?: PermissionUpdate[];
      toolUseID?: string;
    }
  | {
      behavior: 'deny';
      message: string;
      interrupt?: boolean;
      toolUseID?: string;
    };
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.

Using settings to Provide Permission Rules

settings is ideal for providing static permission configuration before the session starts. It’s more appropriate than canUseTool for expressing “what this project allows by default, what it denies, and what additional directories exist.”
query({
  prompt: 'Inspect the project.',
  options: {
    auth: accessTokenFromEnv(),
    cwd: '/path/to/project',
    settings: {
      permissions: {
        allow: ['Read', 'Grep'],
        deny: ['Bash'],
        ask: ['Write'],
        defaultMode: 'default',
        additionalDirectories: ['/path/to/shared-lib'],
      },
    },
  },
});
Field descriptions:
FieldDescription
permissions.allowAllow rules
permissions.denyDeny rules
permissions.askAlways-ask rules
permissions.defaultModeDefault permission mode
permissions.disableBypassPermissionsModeSet to 'disable' to disable bypass permissions mode
permissions.additionalDirectoriesAdditional 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.

Using hooks for Advanced Interception and Auditing

Hooks are suitable when you’ve already integrated the SDK hooks system and want finer-grained control in the tool lifecycle. Compared to canUseTool, hooks are better suited for cross-cutting concerns such as auditing, alerting, unified interception, and recording denial reasons.
query({
  prompt: 'Inspect the repo.',
  options: {
    auth: accessTokenFromEnv(),
    cwd: '/path/to/project',
    hooks: {
      PreToolUse: [
        {
          matcher: 'Bash',
          hooks: [
            async (input) => {
              return {
                hookSpecificOutput: {
                  hookEventName: 'PreToolUse',
                  permissionDecision: 'deny',
                  permissionDecisionReason: 'Shell commands are disabled here.',
                },
              };
            },
          ],
        },
      ],
    },
  },
});
The main permission-related hooks are three types:
HookTrigger TimingCommon Use
PreToolUseBefore tool invocationPre-allow, deny, request ask, or pass to subsequent flow
PermissionRequestWhen entering a permission requestReturn allow or deny directly before the normal prompt
PermissionDeniedAfter permission is deniedAuditing, alerting, recording denial reasons
PreToolUse can return:
{
  hookSpecificOutput: {
    hookEventName: 'PreToolUse',
    permissionDecision: 'allow' | 'deny' | 'ask' | 'defer',
    permissionDecisionReason?: string,
    updatedInput?: Record<string, unknown>,
  },
}
PermissionRequest can return a permission result similar to tool approval:
{
  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.

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 allowedTools or disallowedTools.
query({
  prompt: 'Use repo tools.',
  options: {
    auth: accessTokenFromEnv(),
    mcpServers: {
      repo_tools: {
        type: 'http',
        url: process.env.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:
PolicyBehavior
always_allowMatched tool is directly allowed
always_askMatched tool enters authorization flow
always_denyMatched 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.