Published Build3 min read
Your agent supplies its own confirmation: confirm=True is not a permission boundary
A builder audited the human-in-the-loop gate on his own MCP write tool and found a keyword argument the caller sets. Optional evidence makes an optional check.
Written for builders.See today for builders
What happened
- The author of a dev.to post says he built something that looks like a gate in front of a dangerous tool, then read his own code closely enough to notice it is not one.
- The tool in question is update_article in the MCP server the author runs for his DEV.to publishing pipeline; it edits a live article by id, covering title, body and published flag.
- Overwriting a published article's content is the dangerous case because DEV.to keeps no version history, so a bad write is gone the moment it lands.
- In July the author added a gate computing live_content_write as the article already being published and the call including title or body_markdown.
- If live_content_write is true and confirm is not set, the tool returns applied: False with the proposed diff and a reason stating that title/body_markdown changes to a published article require confirm=True because DEV.to has no version history to undo the write.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
An engineer running an MCP server for his DEV.to publishing pipeline went back and read the safety gate he had written in front of his own live-article write tool, and concluded in a post on dev.to that it was not a permission boundary [1][2]. The reason generalises to most of the human-in-the-loop checks being wired into agent tools right now: the confirmation is a keyword argument, and the agent is the caller [6].
The tool, update_article, edits a live article by id, including title, body and published flag [2]. The dangerous case is overwriting the body of something already published, because DEV.to keeps no version history and a bad write is gone the moment it lands [3]. So in July he added the check everyone adds: if the target is published and the call touches title or body_markdown, and confirm is not set, return the proposed diff with applied set to false instead of writing [4][5].
That does something. It does not do what it looks like. There is no code path that forces a caller through the preview step before it can set confirm=True [6]. An agent that never saw the docstring, or that decided on its own the article needed fixing right now, can call update_article with a new body_markdown and confirm=True as its very first move, and for that call the gate is simply not there [7]. In his words, it only stops the caller who was already going to stop and ask [8].
The second layer is the more instructive failure, because it is a better check that breaks the same way. A few weeks later he added expected_fingerprint, a hash of the article state at preview time, so the tool refuses a stale write even when confirm=True is supplied [9][10]. That catches drift the first version could not, such as someone editing the article on-site between the preview and the confirm [11]. But expected_fingerprint defaults to None, and omitting it skips the check rather than the write [12]. His own docstring says the check is opt-in, not mandatory [13].
The asymmetry is the part worth stealing. confirm defaults to a value that withholds the write, while expected_fingerprint defaults to a value that permits it [14]. A check that fires only when the caller supplies the evidence for it is a check the caller can opt out of by omission, not just by an explicit bypass flag [15]. The risky caller is not the one passing confirm=False; it is the one that never learned the parameter exists, and Python does not make that caller pass anything at all [16].
The fix he describes is one line: annotate expected_fingerprint as a required string rather than defaulting it to None, and drop the is not None from the condition, so a live content write arriving without a fingerprint is refused instead of waved through [17][18]. He has not shipped it, and his stated reason deserves to be read straight rather than as an excuse: the tool is only ever invoked by him, through Claude Desktop, on his own machine, so the realistic threat model is his own mistake or a confused agent turn, not an adversarial caller [19][20]. He also concedes what that leaves the design resting on, which is that he mostly remembers to preview first [21].
What to watch is which side of the default each destructive tool in a stack falls on, and that is testable without reading any docstrings: call the tool with only the arguments it actually requires and see whether the write lands. The broader agent-tooling conversation this post is responding to is full of signed capabilities, policy layers and pre-write human checks [22]; the ones that live in the tool's own signature inherit this hole, and the ones that sit outside the callable do not.
Claim ledger
Ranked by verification strength, evidence, and original report placement.
- [1]
The author of a dev.to post says he built something that looks like a gate in front of a dangerous tool, then read his own code closely enough to notice it is not one.
ReportedSource: dev.to post, 'The Permission Boundary My MCP Server Doesn't Actually Have'View cited source - [2]
The tool in question is update_article in the MCP server the author runs for his DEV.to publishing pipeline; it edits a live article by id, covering title, body and published flag.
ReportedView cited source - [3]
Overwriting a published article's content is the dangerous case because DEV.to keeps no version history, so a bad write is gone the moment it lands.
ReportedView cited source - [4]
In July the author added a gate computing live_content_write as the article already being published and the call including title or body_markdown.
ReportedView cited source - [5]
If live_content_write is true and confirm is not set, the tool returns applied: False with the proposed diff and a reason stating that title/body_markdown changes to a published article require confirm=True because DEV.to has no version history to undo the write.
ReportedView cited source - [6]
confirm is a keyword argument the caller supplies, and there is no code path that forces an agent through the preview step before it can set confirm=True.
ReportedView cited source
Sources & coverage · 1 publisher
The reporting this story was synthesized from, earliest first. Every link goes to the original.
- dev.toEnjoy KumawatAug 15The Permission Boundary My MCP Server Doesn't Actually Have
Cited in this coverage: dev.to post, 'The Permission Boundary My MCP Server Doesn't Actually Have'

