Skip to main content
Conversations & Sessions

Approval and User Input

Two situations require user participation while the Agent is working:
  1. The Agent wants to run a tool and needs the user to approve it.
  2. The Agent lacks required information and needs the user to answer a question.
When the host application handles user interaction through canUseTool (can_use_tool in Python), both requests arrive through the same callback. Your application displays an approval prompt or question and returns the user's choice to the SDK.
What the user seesTypical toolWhat the user providesWhat the application returns
Tool approvalBash, Write, or an MCP toolWhether to allow this operationThe original or reviewed tool input
Clarification questionAskUserQuestionThe actual answerquestions and answers
You can configure the callback with either a single-message or streaming prompt. A one-off query() can also display tool approvals and clarification questions.

When canUseTool runs

After the Agent requests a tool, the SDK evaluates your permission configuration. One of three outcomes follows:
  • Already allowed: the tool runs without calling canUseTool.
  • Already denied: the tool is rejected without calling canUseTool.
  • User approval required: the SDK calls canUseTool and waits for your application to allow or deny the request.
AskUserQuestion is different from an ordinary tool because it requires a real user answer. When canUseTool / can_use_tool is configured, the SDK calls the callback for an answer unless user interaction has been explicitly disabled. Because the callback does not receive every tool call, do not use canUseTool as a complete tool-execution log. Use hooks to observe every tool call. See Permission Control for the permission evaluation order. To let your application handle dynamic approval or AskUserQuestion, configure canUseTool / can_use_tool, or use an external permission prompt tool. If the runtime sends a permission request but the SDK has no corresponding callback, the SDK fails closed and reports an error; it never executes the tool automatically.

Configure the callback

The following snippets only show callback configuration. showApprovalDialog / show_approval_dialog represents an approval UI implemented by the host application; it is not an SDK function. For a runnable terminal implementation, see the complete example.
import { accessTokenFromEnv, query } from '@qoder-ai/qoder-agent-sdk';

query({
  prompt: 'Read the release configuration and generate a changelog. Ask before writing files.',
  options: {
    auth: accessTokenFromEnv(),
    permissionMode: 'default',
    allowedTools: ['Read'],
    async canUseTool(toolName, input, context) {
      const approved = await showApprovalDialog({
        toolName,
        input,
        title: context.title,
        description: context.description,
        signal: context.signal,
      });

      if (!approved) {
        return {
          behavior: 'deny',
          message: 'The user denied this operation.',
          toolUseID: context.toolUseID,
        };
      }

      return {
        behavior: 'allow',
        updatedInput: input,
        toolUseID: context.toolUseID,
      };
    },
  },
});
In TypeScript, context.signal is an AbortSignal. In Python, it is asyncio.Event | None. If the task has been cancelled, close the approval UI and end the callback. The TypeScript example returns toolUseID to identify the specific tool call being handled. Use canUseTool / can_use_tool when your application supplies the approval UI. If an external permission prompt tool already exists, use permissionPromptToolName / permission_prompt_tool_name instead. You cannot configure both approaches at the same time.

Return approval results

Allow once

To allow the tool to run, return its input:
return {
  behavior: 'allow',
  updatedInput: input,
  toolUseID: context.toolUseID,
};
updatedInput / updated_input is the input the tool ultimately receives. You can return it unchanged, or remove unsafe flags, restrict a path, or modify another field before allowing the call. Validate any modified input against that tool's input format.

Deny

When denying a call, include a short reason. The Agent can use it to choose another approach or explain why the operation was not performed:
return {
  behavior: 'deny',
  message: 'This command is not allowed in production.',
  toolUseID: context.toolUseID,
};
To stop the entire task immediately after the denial, set interrupt: true (interrupt=True in Python).

Always allow for this session

context.suggestions may contain permission rules that the SDK recommends saving. When the user selects “Always allow for this session,” include those suggestions in the allow result:
return {
  behavior: 'allow',
  updatedInput: input,
  updatedPermissions: context.suggestions,
  toolUseID: context.toolUseID,
};
These rules can prevent the same type of operation from being requested again during the current session. For the complete PermissionUpdate type and custom rule construction, see Update permissions during a session.

How permission modes affect the callback

permissionMode / permission_mode sets the default tool-request behavior for the session. It affects whether the user sees an approval prompt and whether canUseTool is called. The following table assumes that canUseTool / can_use_tool is configured and that AskUserQuestion is visible in tools:
Permission modeOrdinary toolsAskUserQuestion
defaultTools that need approval call the callbackCalls the callback for the user's answer
acceptEditsCommon file edits are allowed directly; other tools may still require approvalCalls the callback for the user's answer
planDoes not perform actual modifications; approval may still be requested when neededCan ask questions to clarify requirements or a plan
autoThe SDK automatically allows or denies some tools, so the callback is not guaranteed for every callCalls the callback for the user's answer
dontAskTools that were not pre-authorized are denied without promptingThe question is also denied, so the Agent cannot wait for an answer
bypassPermissions / yoloOrdinary tools run without an approval promptStill calls the callback; the SDK cannot answer on the user's behalf
Keep these constraints in mind:
  • If disallowedTools / disallowed_tools or a deny rule blocks AskUserQuestion, the Agent cannot ask the user a question.
  • If tools is set explicitly, include AskUserQuestion; otherwise the Agent cannot see the tool.
  • Use bypassPermissions and yolo only in trusted environments. They skip approval for ordinary tools but do not answer questions for the user.
  • To show every not-yet-authorized operation in your own approval UI, normally use default and do not add those tools to allowedTools / allowed_tools.
For the complete behavior and risks of every mode, see Default policy: permissionMode.

Handle AskUserQuestion

AskUserQuestion asks a small number of structured questions before the Agent continues the current task—for example, to choose a target environment, implementation approach, or output format. It is not a new chat message. After the user answers, the Agent resumes the current task. When canUseTool receives toolName === 'AskUserQuestion', the callback's input has this runtime structure:
type AskUserQuestionRuntimeInput = {
  questions: Array<{
    question: string;
    header: string;
    options: Array<{
      label: string;
      description: string;
      preview?: string;
    }>;
    multiSelect?: boolean;
  }>;
};
  • A request contains 1–4 questions.
  • Each question has a short header and 2–4 options.
  • If multiSelect is false or omitted, the user can select one option. If it is true, the user can select multiple options.
  • Your UI should also let the user enter an answer that is not one of the options.
  • An option can include preview. In TypeScript, toolConfig.askUserQuestion.previewFormat specifies whether previews are interpreted as markdown or html. The Python SDK currently has no equivalent tool_config option.
In a canUseTool callback, use the runtime questions / answers structure shown here, not singular question / answer fields.

Return question answers

Return behavior: 'allow' when submitting answers. Here, allow means “accept these answers and continue the task.” Include both the questions and answers in updatedInput:
{
  behavior: 'allow',
  updatedInput: {
    questions: input.questions,
    answers: {
      'Which environment should receive the deployment?': 'Staging',
      'Which checks should be enabled?': 'Type checking, Unit tests',
      'Any other requirements?': 'Keep compatibility with Node.js 18',
    },
  },
}
Encode answers as follows:
  • Each key must be the complete question text for the corresponding question.
  • For a single selection, use the option's label. For a custom answer, use the text entered by the user.
  • For multiple selections, join the labels into one string with , .
  • If the user cancels the questions, return deny with a reason. Do not continue with invented or blank answers.

Complete example

The following example handles both questions and ordinary tool approvals. A real application can replace terminal input with a dialog, web page, or approval service.
import { createInterface } from 'node:readline/promises';
import {
  accessTokenFromEnv,
  query,
  type CanUseTool,
} from '@qoder-ai/qoder-agent-sdk';

type Question = {
  question: string;
  header: string;
  options: Array<{ label: string; description: string }>;
  multiSelect?: boolean;
};

const readline = createInterface({
  input: process.stdin,
  output: process.stdout,
});

function parseAnswer(raw: string, question: Question): string {
  const indexes = raw
    .split(',')
    .map((part) => Number.parseInt(part.trim(), 10) - 1)
    .filter((index) => index >= 0 && index < question.options.length);

  if (indexes.length > 0) {
    const selected = question.multiSelect ? indexes : indexes.slice(0, 1);
    return selected.map((index) => question.options[index].label).join(', ');
  }

  return raw.trim();
}

async function readLine(
  prompt: string,
  signal: AbortSignal,
): Promise<string | null> {
  try {
    return await readline.question(prompt, { signal });
  } catch (error) {
    if (
      signal.aborted ||
      (error instanceof Error && error.name === 'AbortError')
    ) {
      return null;
    }
    throw error;
  }
}

const canUseTool: CanUseTool = async (toolName, input, context) => {
  if (context.signal.aborted) {
    return {
      behavior: 'deny',
      message: 'The request was cancelled.',
      toolUseID: context.toolUseID,
    };
  }

  if (toolName === 'AskUserQuestion') {
    const questions = (input.questions ?? []) as Question[];
    const answers: Record<string, string> = {};

    for (const question of questions) {
      console.log(`\n${question.header}: ${question.question}`);
      question.options.forEach((option, index) => {
        console.log(`${index + 1}. ${option.label}${option.description}`);
      });

      const hint = question.multiSelect
        ? 'Enter option numbers separated by commas, or a custom answer (/cancel to cancel): '
        : 'Enter an option number or a custom answer (/cancel to cancel): ';

      let answer = '';
      while (!answer) {
        const raw = await readLine(hint, context.signal);
        if (raw === null || raw.trim() === '/cancel') {
          return {
            behavior: 'deny',
            message: 'The user cancelled the questions.',
            toolUseID: context.toolUseID,
          };
        }

        answer = parseAnswer(raw, question);
        if (!answer) {
          console.log('An answer is required. Try again.');
        }
      }

      answers[question.question] = answer;
    }

    return {
      behavior: 'allow',
      updatedInput: { questions, answers },
      toolUseID: context.toolUseID,
    };
  }

  const answer = await readLine(
    `\nAllow ${toolName} with input ${JSON.stringify(input)}? [y/N] `,
    context.signal,
  );

  if (answer === null) {
    return {
      behavior: 'deny',
      message: 'The request was cancelled.',
      toolUseID: context.toolUseID,
    };
  }

  if (answer.trim().toLowerCase() !== 'y') {
    return {
      behavior: 'deny',
      message: `The user denied ${toolName}.`,
      toolUseID: context.toolUseID,
    };
  }

  return {
    behavior: 'allow',
    updatedInput: input,
    toolUseID: context.toolUseID,
  };
};

try {
  for await (const message of query({
    prompt: 'Inspect the project and produce release notes. Ask me when the release target is unclear.',
    options: {
      auth: accessTokenFromEnv(),
      tools: ['AskUserQuestion', 'Read', 'Write', 'Bash'],
      allowedTools: ['Read'],
      permissionMode: 'default',
      canUseTool,
    },
  })) {
    if (message.type === 'result') {
      console.log(message.subtype);
    }
  }
} finally {
  readline.close();
}
The Python terminal example uses blocking input(). After a task is cancelled, it cannot check context.signal until the current input call returns. A web or desktop application should not inherit this limitation: listen for context.signal, close the dialog immediately when it fires, and end the callback.

Capability boundaries

RequirementRecommended capabilityWhy
Confirm whether a tool should runcanUseTool / can_use_toolCan allow, deny, or revise tool input first
Collect 1–4 short answers before the Agent continuesAskUserQuestionThe Agent supplies questions and options, then continues after the user answers
Let the user follow up, add long text, or redirect the taskStreaming inputThis is a new user message
Collect fixed fields, enforce strict validation, upload files, or display a complex formCustom toolsThe application controls the data format and UI
Let an MCP server request form or authorization informationMCP ElicitationThese requests use onElicitation / on_elicitation
Record every tool call or apply a uniform tool interceptorHooks and Permission ControlcanUseTool does not receive calls that were automatically allowed or denied
Do not use AskUserQuestion as a replacement for multi-turn conversation. New messages initiated by the user should use streaming input. Do not use a normal text response in place of tool approval either, because the SDK requires an explicit allow or deny result.