Skip to content

Approval requests and sandbox policies

0.4.0 migration: manual response streams must opt in

Set approval_mode="manual" when responding to approval_requests() without a callback. The default still automatically declines; reading the stream does not change that. Callback users keep their existing setup. See the migration guide, including stricter checks for late or duplicate responses.

Thread-level policy and sandbox mode

ThreadConfig controls defaults persisted for subsequent turns on that thread.

from codex_app_server_sdk import ThreadConfig

cfg = ThreadConfig(
    approval_policy="on-request",
    sandbox="workspace-write",
)

approval_policy values:

  • untrusted
  • on-failure (deprecated by protocol, still accepted)
  • on-request
  • never

sandbox values:

  • read-only
  • workspace-write
  • danger-full-access

Turn-level sandbox policy

Use TurnOverrides.sandbox_policy for per-turn policy payloads.

from codex_app_server_sdk import TurnOverrides

turn = TurnOverrides(
    sandbox_policy={
        "type": "workspaceWrite",
        "networkAccess": False,
        "writableRoots": ["/tmp"],
    },
    approval_policy="on-request",
)

TurnOverrides.sandbox_policy accepts either:

  • typed SandboxPolicy structures, or
  • raw mapping payloads (for compatibility with server extensions).

Approval request handling

The client handles v2 server-initiated approval requests:

  • item/commandExecution/requestApproval
  • item/fileChange/requestApproval

In the default approval_mode="auto", no handler means the SDK responds with decline (continue turn). With approval_mode="manual" and no handler, the request remains pending for an explicit response.

Callback mode

from codex_app_server_sdk import (
    CodexClient,
    CommandApprovalRequest,
    CommandApprovalWithExecpolicyAmendment,
    FileChangeApprovalRequest,
)


async def handler(req):
    if isinstance(req, CommandApprovalRequest):
        if req.reason and "network" in req.reason.lower():
            return "decline"
        return CommandApprovalWithExecpolicyAmendment(["uv", "run"])

    if isinstance(req, FileChangeApprovalRequest):
        return "accept_for_session"

    return "decline"


async with CodexClient.connect_stdio() as client:
    client.set_approval_handler(handler)
    result = await client.chat_once("Run a command that may request approval.")
    print(result.final_text)

Stream mode (manual response)

Set approval_mode="manual" before starting the turn. Without a callback, requests then remain pending until you answer them or the server resolves them. The default approval_mode="auto" still declines requests without a callback.

import asyncio
from contextlib import suppress
from codex_app_server_sdk import CodexClient, CommandApprovalRequest


async with CodexClient.connect_stdio(approval_mode="manual") as client:
    async def approval_loop():
        async for req in client.approval_requests():
            if isinstance(req, CommandApprovalRequest):
                await client.approve_approval(req, for_session=True)
            else:
                await client.decline_approval(req)

    approval_task = asyncio.create_task(approval_loop())
    try:
        result = await client.chat_once("Run a task that may need approvals.")
        print(result.final_text)
    finally:
        approval_task.cancel()
        with suppress(asyncio.CancelledError):
            await approval_task

If a callback is configured, it handles requests in either mode and approval_requests() is observational. Choose one response mechanism per request. Long human waits still follow the client's inactivity timeout; disable that timeout or retain and resume the continuation when needed.

For plan-mode user questions (item/tool/requestUserInput), see Human-in-the-loop.