Trace Tool Approval Requests in Kimi CLI: From Execution to User Prompt

Tool approval requests in Kimi CLI flow from the tool layer through an approval runtime, across the wire protocol to the UI, and back to the tool via async resolution.

The Kimi CLI codebase implements a permission gate for any tool that can modify the system or external resources. Understanding how it handles tool approval requests requires tracing the path from src/kimi_cli/soul/approval.py through the runtime and wire layers to the React frontend and back again.

Stage 1: Tool Execution Creates the Request

When a tool requires user consent, it invokes Approval.request(...) in src/kimi_cli/soul/approval.py. This method first checks the current tool call context using get_current_tool_call_or_none to associate the request with the correct call ID.

The method then builds an ApprovalRequestRecord defined in src/kimi_cli/approval_runtime/models.py and registers it with an ApprovalRuntime instance. As implemented in src/kimi_cli/approval_runtime/runtime.py, the ApprovalRuntime.create_request method persists the pending request and publishes a request_created event. Simultaneously, the request is serialized into an ApprovalRequest message and dispatched to the wire layer.


# ---- Inside a tool implementation (e.g., a file‑write tool) ----

async def run(self, path: str, content: str):
    # Request user approval before writing the file

    approval = await self.approval.request(
        sender="file_tool",
        action="write_file",
        description=f"Write {len(content)} bytes to {path}",
        display=[DisplayBlock(type="diff", data=diff_of_content)],
    )
    if not approval:
        raise approval.rejection_error()   # raises ToolRejectedError

    # Approved – perform the write

    with open(path, "w") as f:
        f.write(content)

If the session operates in AFK or YOLO mode, Approval.is_auto_approve() short-circuits the flow and immediately returns an approved ApprovalResult without prompting the user.

Stage 2: Wire Protocol Delivers the Request to the UI

The wire hub receives the ApprovalRequest and forwards it to all UI subscribers. In the React frontend, the ApprovalDialog component in web/src/features/chat/components/approval-dialog.tsx monitors incoming chat messages. It locates the message whose variant === "tool" and whose approval field indicates an approval‑requested state.

From that message, the component extracts the request ID, description, action, and optional display blocks. It then renders a modal with Approve, Approve for session, Decline, and Decline with feedback buttons.

// ---- React component handling the pending request ----
const handleResponse = useCallback(
  async (decision: ApprovalResponseDecision, reason?: string) => {
    if (pendingApproval && onApprovalResponse) {
      await onApprovalResponse(pendingApproval.approval.id, decision, reason);
    }
  },
  [pendingApproval, onApprovalResponse],
);

Stage 3: User Response Resolves the Pending Request

When the user clicks a button or triggers a keyboard shortcut, ApprovalDialog invokes the onApprovalResponse callback with the request ID and a decision. The wire layer routes this back to ApprovalRuntime.resolve(request_id, decision, ...) in src/kimi_cli/approval_runtime/runtime.py.

The runtime marks the request as resolved, records any feedback, and publishes a request_resolved event. It then wakes the awaiting coroutine in Approval.request() by setting the result on a waiter future. The original tool call receives an ApprovalResult object. If approved, execution continues; otherwise, the runtime raises a ToolRejectedError.


# ---- Runtime side that resolves the request ----

def resolve(self, request_id: str, decision: ApprovalResponseKind, feedback: str = "", approved_via_session_cache: bool = False):
    request = self._requests[request_id]
    request.status = "resolved"
    request.response = decision
    request.feedback = feedback
    request.approved_via_session_cache = approved_via_session_cache
    self._publish_event(ApprovalRuntimeEvent(kind="request_resolved", request=request))
    # Wake the awaiting coroutine in Approval.request()

    waiter = self._waiters.pop(request_id, None)
    if waiter and not waiter.done():
        waiter.set_result((decision, feedback))

If the user selects Approve for session, the action name is added to self._state.auto_approve_actions. Future tool approval requests with the same action bypass the UI and are auto‑approved. Every outcome branch emits a permission_approval_result telemetry event via _track_permission_result.

Summary

Frequently Asked Questions

How does Kimi CLI associate an approval request with the correct tool call?

The Approval.request() method retrieves the active tool call context via get_current_tool_call_or_none, which is stored in a ContextVar. This ensures each ApprovalRequestRecord is linked to the specific call that initiated it.

What happens when a user chooses "Approve for session"?

The runtime adds the tool action name to self._state.auto_approve_actions. Subsequent tool approval requests for that same action are automatically approved without displaying the UI modal.

Where is the pending request state held while waiting for user input?

Pending requests are stored in the ApprovalRuntime instance defined in src/kimi_cli/approval_runtime/runtime.py. The runtime maintains an internal registry of request records and blocked waiter coroutines until resolve() is invoked.

Which message types travel between the runtime and the UI?

The wire layer in src/kimi_cli/wire/types.py defines ApprovalRequest messages for outbound requests and ApprovalResponse messages for inbound user decisions. These types enable the bidirectional flow between the Python backend and the React frontend.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →