A server asks a question and the hook answers first
MCP, short for Model Context Protocol, lets a server your session is connected to pause mid-task and ask a human a question. You do not have to wait for that dialog. Before Claude Code shows the question, Elicitation runs. A hook here can supply the answer itself, so the request never reaches a person, or it can refuse the request outright. The matcher filters on mcp_server_name, and omitting it runs the handler for every server that asks.
Form mode and URL mode
The payload carries permission_mode, mcp_server_name, and message, plus optional mode, url, elicitation_id, and requested_schema.
| Field | What it holds |
|---|---|
mode | Which kind of request this is. The hooks reference shows two, form and URL. |
requested_schema | For a form request, the object schema describing the fields the server wants filled in. |
url | For a URL request, a browser-authentication link. |
elicitation_id | The identifier this request carries. |
The reference does not list any mode beyond those two, and it does not define which subset of JSON Schema requested_schema may use.
Accept, decline, or cancel
The JSON answer lives under hookSpecificOutput with hookEventName: "Elicitation" and an action of accept, decline, or cancel. content carries form values and applies only when the action is accept. systemMessage and continue are discarded.
Exit 0 shows the normal dialog unless the JSON supplies one of those actions. Exit 1 is the same, with the JSON still read. The reference does not say how the server sees the difference between decline and cancel on its side.
Exit 2 throws your JSON away
Exit 2 denies the request. Stderr is shown nowhere, not to you and not to Claude. And hookSpecificOutput is ignored even when stdout holds valid JSON. On most events valid JSON still applies on a non-zero code. On Elicitation, and on ElicitationResult, it does not. The exit code wins by discarding the JSON, not by outranking it.
You want to refuse one server’s requests and say why, so the hook exits 2 with the reason on stderr.
The request is denied and the reason reaches nobody: not you, not Claude, not the server. You add a JSON decline and keep the exit 2, and the JSON is discarded too. From the outside, the dialog never appeared and nothing explained why.
Return action: "decline" and exit 0. Exit 2 is the silent form of the same refusal.
Module 09 lists this event in the blocking inventory, and module 10 is where you connect a server that could ask.