When the surface can act
The line that actually matters is not what the UI displays but what it can initiate — a surface where model-supplied content triggers a fetch, a navigation or a tool call has stopped being a display.
The review gate has one property worth being proud of: nothing downstream fires until a human approves. Per-field confidence, the source crop, the raw text, and then a person decides. That is human-in-the-loop as it is supposed to work, and it is the design most agentic products claim to have.
This lesson is about the two ways that claim quietly stops being true. The first is that other things on the page can initiate without going through the gate. The second is worse, and almost nobody checks it: the gate can be showing the human a description of the action rather than the action.
The line: displays versus initiates
Take any component that receives model output and ask one question: can anything here cause a request, a navigation or a state change that the user did not explicitly ask for? Not “can the user do something” — can the content cause something.
The answers are less obvious than they sound, because most of them are framework defaults rather than decisions:
- An image the browser fetches on render. Covered in the rendering lesson. Initiates. No click.
- Link prefetching. Next.js documents that “Prefetching happens when a
<Link />component enters the user’s viewport” and that “Prefetching is only enabled in production” (Next.js docs — a vendor documenting its own framework). This applies to client-side, same-origin navigation, so a model-supplied external URL is not what gets prefetched. The general point survives anyway, and it is the one to carry: a link component in a modern framework fetches before the click, in production and not in development, which is the worst possible combination for noticing it. - Suggested-action chips. A model proposes three follow-ups and your UI renders them as buttons. The content now determines what a one-click affordance does. This is the pattern most agentic UI kits ship, and it is a capability decision wearing a layout decision’s clothes.
- Auto-executing tool calls. The model returns a tool invocation and the client dispatches it. Initiates, definitionally.
- Forms whose action or target comes from output. MDN documents
form-actionas restricting “the URLs which can be used as the target of form submissions from a given context” — with a live caveat that whether it should block post-submission redirects is debated and browser behaviour is inconsistent.
A surface where any of those is true is not a display. Calling it one in a design review is the mistake this lesson exists to stop, because “it just shows the answer” is the sentence that ends the security conversation before it starts.
Excessive agency, and why it climbed the list
OWASP moved Excessive Agency from sixth to third in the 2026 edition, while improper output handling fell to tenth. Read those two moves together: the list is reweighting from “what does the model emit” toward “what can the system do about it.”
“An LLM-based system is often granted a degree of agency by its developer: the ability to call functions or interface with other systems via tools (also called extensions, plugins, or skills by different vendors) to undertake actions in response to a prompt.” (OWASP, LLM03:2026)
The mitigations are a repeated word: minimum. Limit the tools an agent may call to the minimum necessary; limit the functions implemented in those tools to the minimum necessary; limit the permissions those tools hold on other systems to the minimum necessary. And then the one that lands on your desk:
“Use human-in-the-loop control to require a human to approve high-impact actions before they are taken.” (OWASP, LLM03:2026)
Three of those four mitigations belong to whoever owns the agent runtime. The fourth is a screen. Somebody has to build the screen, and the quality of the control is entirely a property of the screen.
The confirmation-fidelity problem
Here is the failure this course most wants you to be able to spot, because it is invisible from the backend and obvious from the front end once you know to look.
A human-in-the-loop gate approves an action. The screen shows the human something. The question is whether the thing shown is derived from the same data the action will execute, or whether it is a separate, model-generated summary of it.
If the confirmation card says “Send the March invoice to the client” and the tool call underneath carries a different recipient, the human approved a sentence. Prompt injection does not need to defeat the approval gate; it needs to write the label on the button. And the label is model output, which is the untrusted content this module opened with.
This is a rendering decision. It lives in the confirmation component. It is testable in an afternoon, and the test is: render the confirmation directly from the action payload the executor will use, not from anything the model wrote in prose. Where the payload is unreadable — an opaque ID, a base64 blob — show the opaque thing rather than a friendly gloss of it, or resolve it through a trusted lookup and show what the lookup returned.
Cutting a leg, and the vocabulary for asking someone else to
The lethal trifecta from the untrusted-input lesson — private data, untrusted content, external communication — comes back here as a decision procedure rather than a diagnosis. Willison’s prescription is to avoid the combination entirely rather than defend it, and his read is that exfiltration is usually the easiest leg to remove. For a surface you own, that translates directly: the third leg is mostly made of rendering and capability defaults you can turn off.
For the legs you do not own, what you need is not a solution but the right nouns. The Design Patterns for Securing LLM Agents against Prompt Injections paper (arXiv 2506.08837) names six: Action-Selector, Plan-Then-Execute, LLM Map-Reduce, Dual LLM, Code-Then-Execute, and Context-Minimization. Willison rates the paper highly specifically because its authors accept a real constraint — each pattern buys security by giving up some generality — rather than claiming a universal fix.
Knowing those six names is not knowing how to implement them, and this course is not going to teach you to. It is enough to let you ask an architecture question instead of a vibes question: “which of these patterns is this system closest to, and what did we give up to get there?” That question gets answered. “Is this secure?” does not.
Check your recall
Answer from memory — no scrolling back.
Retrieval check
Your advisory chatbot recommends flights and executes nothing. Does anything in this lesson apply to it?
Check your answer
Yes, in two places. First, a recommendation becomes a link the moment it is useful — “book this” pointing at a model-supplied URL is a navigation initiated by untrusted content, landing a user on a page chosen by whatever influenced the model. Advisory describes what the backend does, not what the page does.
Second, the confirmation-fidelity question has an advisory form. If the prose recommendation and the structured itinerary come from different generations, the user reads one and acts on the other. That is the same fidelity gap with a lower ceiling on the damage, and it is the version you can go and check today.
Hands on
Build the initiate inventory for one surface
Done when: A written table listing every element the surface renders from model output, each classified displays or initiates, with the initiating ones naming the exact mechanism and the code path — plus one confirmation-fidelity finding stated as pass, fail, or not applicable, with the two file paths that justify it.
- Pick one surface. The review gate’s approve-or-correct panel is the most instructive because it is the one you would have called a display.
- List every element rendered from model output: text, fields, links, images, buttons, chips, anything. Aim for completeness over judgement — the classification comes next.
- Classify each one displays or initiates. For every initiate, name the mechanism (browser fetch on render, framework prefetch, click handler, dispatched tool call) and the file it lives in. If you are unsure, write unsure — then resolve it by watching the network panel rather than by reading.
- Find the confirmation path. Trace what the approve control actually sends downstream, and separately trace what the screen displays above it. Write down whether they derive from the same payload.
- Record the finding as pass (same payload), fail (screen shows a separate model-written summary), or not applicable, and cite the two file paths. A fail here is not an emergency; it is a well-formed flag, which is the entire point.
- Update the
FLAG-LOG.mdrow and bring it in. I’ll push on anything classified “displays” that you have not actually watched, and on a fidelity verdict with only one file path behind it.
What this does not cover
This module ends with the attack surface. It has said nothing about the leaks that happen when nobody is attacking anything — a session-replay script recording a review gate that is displaying somebody’s identity document, a trace view built to show every input and output of every step, a transcript store with no retention policy because nobody was asked for one, an inference call crossing a border that a customer contract said it would not. Those are the module on data in the UI, and the flags they produce go to different owners than these ones do.
It has also said nothing about how to raise any of this. Everything in your flag log so far has a “what I saw” and no owner, on purpose. The module on raising it properly is where the owner column gets filled in — and where SOC 2 turns out to be the reason a front-end telemetry choice was never only a front-end choice.
Read this next — primary source
The lethal trifecta for AI agents: private data, untrusted content, and external communicationSimon Willison, 16 June 2025 — free, about 10 minutes
This lesson takes the three legs and the instruction to cut one. The post itself is written for people assembling their own agent tooling rather than for people building it, and that audience shift is why it is worth reading in full: it shows you the shape of the argument as a non-engineer will hear it, which is the register you need when the person you are flagging to is an operating partner rather than a security engineer. It also carries his running commentary on why vendor guardrails keep failing, which is the part of the argument you will need more than once.
Stuck, curious, or think this lesson is wrong? Ask your teaching agent. The lessons are the scaffold; the conversation is where the learning gets unstuck.