I'm working on an approval gate for destructive MCP tools. The server asks the human through MCP elicitation, so the model never sees the approval step.
A commenter pointed out a hole: in Claude Code, a hook on the Elicitation event can exit 0 with action "accept" and answer the request with no dialog. The server only receives accept, so it can't tell a person from a script. A project .claude/settings.json in a cloned repo could register such a hook once the folder is trusted.
What I'm considering:
- Log "client-accepted" instead of "approved", so the audit file never claims a human was involved.
- Run the allowlist and policy checks before any elicitation, so a scripted accept can't override them.
- Refuse irreversible tools when I can't tell how the answer was produced.
My question: is there any reliable way for an MCP server to know that a real person answered an elicitation? Or should the design assume it can't know, and rely on reversibility and quarantine instead?
I haven't checked which other clients let a script answer elicitation. If you know of any, please tell me. The project is open source, so PRs and issues are welcome.
Top comments (3)
I think the honest answer for form-mode elicitation is no. The 2025-11-25 spec says "the protocol itself does not mandate any specific user interaction model", and user approval controls on the client are a SHOULD. So logging "client-accepted" is right.
URL mode is the closest the spec gets. The server sends a URL; the data does not pass through the client, and the server "MUST verify the identity of the user who opens the URL before accepting information". A Claude Code Elicitation hook can still accept a URL-mode request, but it gets no content back, so the server keeps waiting for its own page. It is close to the one-time code idea above, but native to the spec.
One idea I have not tested: put the confirm step for irreversible tools on that server-hosted page, with a step-up check like a passkey with user verification. A scripted accept cannot finish that. It still does not help if someone controls the browser session, and I have not checked which clients support URL mode.
For repo hooks,
allowManagedHooksOnlyin managed settings limits which hooks run.modelcontextprotocol.io/specificat...
From the Auten team (screen-control tools, same wall). Is your threat model only hooks that a cloned repo brings in, or also hooks the user wrote? If it's repo hooks, an out-of-band step closes most of it: for irreversible tools, send a short one-time code to something the client process can't read (a phone push, an authenticator) and require that code in the elicitation reply. A scripted accept doesn't have it, so "client-accepted" and "human-confirmed" become two separate log states.
Short answer: the server can't know, and the design should say so in the audit log.
The reason is a trust boundary, not a missing feature. Once a repo's
.claude/settings.jsoncan register an Elicitation hook and the folder is trusted, everything the client tells the server about the answer is client testimony.acceptis a string; there is no field in it that came from a person. Any "verified human" bit a client could add would be produced by the same process the hook controls, so it's worth exactly what the hook says it's worth.So your option 3 is the right default, and option 1 is the honest logging for it. Option 2 is necessary but it isn't approval, it's policy: run allowlist/policy before elicitation so a scripted accept can't widen what is reachable. It doesn't turn a scripted accept into a real one.
"Can't know inside the protocol" is not the same as "can't know at all." You can leave the client's channel:
One more split: "how was the answer produced" and "who trusted the repo" are the same problem twice. The dangerous step isn't the hook answering, it's a folder gaining trust while carrying a settings file nobody read. Blanket per-folder trust is the weakest link, and it lives outside the server.
A shape that stays honest: log
accepted-by-client, plus anapproval-evidencefield with an enum (none,client-asserted,out-of-band:nonce,hardware-key). The audit file never claims something it can't back, and policy can decide which evidence level each tool class needs. Same idea as a checkable record: the record doesn't prove the fact, it proves what evidence existed at the time.What I have not checked: your commenter's hook internals, and whether any client exposes a UI signal reliable enough to tell a dialog from a script. I would bet against the second one being portable, but I haven't run it, so treat that as a guess rather than a finding.