{
  "question": "How do I require human confirmation (a HITL gate) before MCP write and other side-effecting tools run?",
  "slug": "how-to-require-human-confirmation-before-mcp-write-tools-run",
  "url": "https://www.rightshold.com/answers/how-to-require-human-confirmation-before-mcp-write-tools-run",
  "short_answer": "Splitting read-only tools from write tools (and lazy-loading schemas) reduces accidents; it does not authorize a write. Put a human-in-the-loop gate in the MCP host or gateway that every side-effecting tools/call must pass: classify write/send/pay/delete/deploy tools on your own allowlist (do not trust readOnlyHint alone), pause before the call, show the person the exact tool name and full arguments (not the model's summary), bind their yes to a hash of those arguments, and refuse if anything changed. The MCP tools specification says there SHOULD always be a human able to deny tool invocations, and that clients SHOULD prompt for confirmation on sensitive operations and show tool inputs before calling the server. OWASP's MCP Security Cheat Sheet says the same: display full parameters, never auto-approve in multi-server setups, and make sure the confirmation UI cannot be bypassed by LLM-crafted responses. Pair the gate with least-privilege credentials and a short-lived approval record outside the agent's session.",
  "checklist": [
    "Maintain your own write/side-effect allowlist (write, send, pay, delete, deploy, merge, grant). Unannotated or untrusted tools default to side-effecting until proven otherwise by credentials and review.",
    "In the host or gateway, intercept tools/call for those tools before the server runs. Do not rely on the model to 'ask first'.",
    "Render the confirmation from the real tool name and arguments: recipients, paths, amounts, URLs, record ids — escaped for display. Never let a description or chat summary decide what the approver sees.",
    "Hash the canonical arguments (for example RFC 8785 / JCS) and store the hash with the approval. verify (or re-hash) immediately before execution; if the hash differs, require a new confirmation.",
    "Expire approvals quickly (minutes for high-risk writes). Single-use for payments, deletes and production changes.",
    "Keep confirmations on even after a toolset was lazy-loaded or enabled for the session — enablement is not a standing yes for every later call.",
    "Fail closed on timeout or UI errors: no silent auto-approve. Log allow/deny with tool, argument hash, approver and time outside the agent.",
    "Use separate, narrower credentials for write tools so a read-only session cannot call them even if the gate is misconfigured."
  ],
  "sections": [
    {
      "heading": "Why lazy-load alone is not enough",
      "points": [
        "Withholding write schemas until a task needs them (progressive discovery) shrinks context and reduces accidental selection. The companion guide on separating read-only tools and lazy-loading covers that catalog split.",
        "Once a write tool is visible, the model can still call it because of prompt injection, a mistaken plan or a tool-poisoned description. The boundary has to sit in host code that the model cannot skip.",
        "MCP annotations (readOnlyHint, destructiveHint) are hints for UX. The specification says clients MUST treat annotations as untrusted unless they come from trusted servers — so a flipped hint must not disable your confirmation gate."
      ]
    },
    {
      "heading": "What a binding confirmation looks like",
      "points": [
        "Show the tool name and every argument the server will receive. For money: payee, amount, currency. For messages: channel and full body. For files or tickets: resolved paths or ids.",
        "Bind the yes to those arguments with a hash. The OpenAI Agents SDK human-in-the-loop pattern and LangGraph interrupts keep approval state outside a client-uploaded run blob for the same reason: resume must verify the pending call still matches.",
        "MCP elicitation (when you need a yes from inside a server tool) is a separate path: use it only with clients that declared the capability, never for passwords or payment card data in form mode, and still keep a host-side gate for tools you classify as side-effecting.",
        "Record who approved, when, which tool and which argument hash. That is what you need later if a write is disputed — chat history is not enough."
      ]
    },
    {
      "heading": "Host patterns that work",
      "points": [
        "Claude Code permission rules: put write MCP tools in ask (or deny), evaluated deny → ask → allow, so a broad ask wins over a narrower allow.",
        "Cursor Run Modes / Auto-review: evaluate each MCP call; do not switch a session to bypass confirmations for write servers.",
        "OpenAI Agents SDK: needsApproval on tools that mutate; render pending name+arguments yourself; verify before resume.",
        "A gateway in front of many MCP servers: centralize the write classifier and confirmation UI once, instead of hoping every server implements elicitation correctly."
      ]
    },
    {
      "heading": "What can go wrong",
      "points": [
        "Confirmations are turned off after the first yes for the session, so later writes skip the person.",
        "The UI shows the model's one-line summary ('updating the config') while the arguments rename a production table.",
        "Approval is bound to the tool name only, not the arguments, so amount or recipient changes reuse the same yes.",
        "You trust readOnlyHint: true from an unpinned third-party server and auto-approve a tool that still writes.",
        "The only copy of approved=true lives in the agent's session JSON, which the next turn overwrites."
      ]
    }
  ],
  "checks": [],
  "ask_rightshold": {
    "start": "https://www.rightshold.com/start?ref=seo",
    "mcp_endpoint": "https://www.rightshold.com/mcp",
    "ask": "Before my agent runs this MCP write tool, ask me to approve the exact tool name and argument hash, expire the yes in 15 minutes, and wait for my decision.",
    "mcp_call": {
      "tool": "request_human_approval",
      "arguments": {
        "summary": "Allow support-agent to call mcp__tickets__update_ticket status=closed on ticket T-1042",
        "action": {
          "type": "mcp_tools_call",
          "server": "tickets",
          "tool": "update_ticket",
          "arguments_sha256": "<sha256 of canonical args>",
          "ticket_id": "T-1042",
          "status": "closed"
        },
        "expires_in_minutes": 15
      }
    },
    "also": [
      "Needs your API key. The action is hashed; you decide once with decide_human_approval. verify_human_approval (free) fails if arguments changed after the yes.",
      "record_ai_run can seal the allow/deny (tool, arguments_sha256, approver, time) outside the agent's session.",
      "precall_check / check_tool_integrity before enabling an unfamiliar write server so the tool definition you reviewed still matches.",
      "How to separate read-only MCP tools from side-effecting and lazy-load by task covers the catalog split this gate sits on top of."
    ],
    "scope": "Rightshold records human approvals and signed run receipts for agents that call it. It does not sit in your MCP host, render your confirmation UI, or stop a client that auto-approves write tools. Enforce the HITL gate in your host or gateway. Not legal advice."
  },
  "sources": [
    {
      "title": "MCP specification (2025-11-25): Tools — human in the loop and security considerations",
      "url": "https://modelcontextprotocol.io/specification/2025-11-25/server/tools"
    },
    {
      "title": "OWASP Cheat Sheet Series: MCP Security (human-in-the-loop)",
      "url": "https://cheatsheetseries.owasp.org/cheatsheets/MCP_Security_Cheat_Sheet.html"
    },
    {
      "title": "OpenAI Agents SDK (JS): Human-in-the-loop",
      "url": "https://openai.github.io/openai-agents-js/guides/human-in-the-loop/"
    },
    {
      "title": "LangGraph: Interrupts",
      "url": "https://docs.langchain.com/oss/python/langgraph/interrupts"
    },
    {
      "title": "Claude Code docs: Permissions",
      "url": "https://code.claude.com/docs/en/permissions"
    },
    {
      "title": "How to separate read-only MCP tools from side-effecting and lazy-load by task (Rightshold)",
      "url": "https://www.rightshold.com/answers/how-to-separate-read-only-mcp-tools-from-side-effecting-and-lazy-load-by-task"
    }
  ],
  "related": [
    {
      "title": "How to separate read-only MCP tools from side-effecting and lazy-load by task",
      "url": "https://www.rightshold.com/answers/how-to-separate-read-only-mcp-tools-from-side-effecting-and-lazy-load-by-task"
    },
    {
      "title": "How to keep MCP as a thin adapter and recheck authorization on every tool call",
      "url": "https://www.rightshold.com/answers/how-to-keep-mcp-as-a-thin-adapter-and-recheck-authorization-on-every-tool-call"
    },
    {
      "title": "How to show a human the actual change before approving an AI agent action",
      "url": "https://www.rightshold.com/answers/how-to-show-a-human-the-actual-change-before-approving-an-ai-agent-action"
    },
    {
      "title": "Are MCP tool annotations like readOnlyHint a security guarantee?",
      "url": "https://www.rightshold.com/answers/are-mcp-tool-annotations-like-readonlyhint-a-security-guarantee"
    }
  ],
  "computed_from": "Curated guidance; license verdicts computed from Rightshold's license profiles at render time. Not legal advice.",
  "updated": "2026-11-07"
}