Loopjacking: an approval for 20 units released 2,000 in lab tests of Agno and LangGraph agents
A new arXiv paper shows human approvals in released agent products being reused for a different action: a 20 unit transfer approved, 2,000 executed. It is a lab result with no reported abuse, but it shows what an approve button does and does not protect.
By Parminder Kumar Sharma · · 17 min read

An administrator approved 20 units and the tool received 2,000
In a test rig built on Agno AgentOS, an approval administrator reviewed a mock transfer of 20 units to an approved vendor and approved it. The tool then received a transfer of 2,000 units to an attacker sink: one hundred times the amount on screen, executed under the administrator's decision. The paper reports that result at each of seven Agno releases up to 3.0.9, in five of five attack trials at each, which is 35 of 35 substituted transfers. A second rig on LangGraph Agent Server did the same across 12 released versions up to 0.14.0. The figures come from Loopjacking: Hijacking Human-in-the-Loop Approval, posted to arXiv on 17 September 2026 by Adithyan Arun Kumar, an independent security researcher.
Here is what that number does not establish. It is not a rate of anything in the world. The four products were chosen on purpose, the ledger was a mock, the credentials were synthetic, the traffic never left loopback, and the "human" was a script that clicked approve after the test had recorded exactly what was on screen. The author says plainly that the results "do not estimate ecosystem prevalence". No source I found reports this being used against a real deployment. The LangGraph result also depends on an authorisation policy the operator has to write, one that lets a non-approver edit a pending thread. A 100 per cent success rate in a deterministic harness means the bug is deterministic, not that 100 per cent of anything is exposed.
What it does establish matters more to readers of this site than the headline. Human approval appears in a lot of agent security advice, including OWASP's, which lists it as a mitigation for prompt injection and excessive agency, and including our own checklists. The paper shows, in named released software, that the approval button and the action that runs can come apart. The button is not the control. The binding between the button and the action is.
What Loopjacking is, in two variants
The paper defines Loopjacking as a product failure in which "a human approves what they understand as operation A, while the implementation uses that decision for a materially different operation B". It splits the failure in two.
Representation-based. Operation B is already in the full request before anyone decides, but the approval view shows something materially less. The human approves an accurate picture of an incomplete thing.
Post-approval state substitution. The human sees the correct operation A and approves it. Before that approval is used, someone changes the pending task, thread, session or continuation to B, and the product applies the old decision to the new operation.
To stop the label swallowing every agent bug, the author admits a result only when six conditions all hold: a real human decision over a view understood as A; a material difference in B, not formatting; an attacker path to the mismatch that the product actually supports; product code, not the attacker, consuming the decision for B; a consequential sink where the exact operation is recorded; and no equivalent direct route for the attacker to get B without the approval. That last test is why Agno 2.5.5 does not count: it executed the substituted continuation too, but it also let the attacker run B directly, so nothing needed hijacking.
The definition also excludes ordinary persuasion, where someone knowingly approves a visible bad action, and prompt injection that never reuses an approval. The attacker in the model is someone with narrow rights, such as a low privilege task initiator, a repository writer or a remote agent, who can influence the request or pending state but cannot approve. They do not need to control the model and do not need to race the approver.
The four product paths, and what is on the record for each
The author tested three positive paths and one product expected to get it right, as a control. The table separates what the paper reproduced from what is fixed on the public record, and from what nobody has said.
Tested products, from the Loopjacking paper (Tables 1 and 2), the OpenClaw advisories, and GitHub advisory listings checked on 21 September 2026
| Product path | Reproduced in the paper | Fixed on the record | Not stated |
|---|---|---|---|
| Agno AgentOS, regular Agent over HTTP | State substitution at 7 releases: 2.5.6, 2.9.0, 3.0.1, 3.0.2, 3.0.3, 3.0.6, 3.0.9; 5 of 5 trials each | Nothing. No Agno advisory on approval binding; the paper knows no fixed release | Whether 3.0.10 (16 September) or untested patches in between behave the same; any vendor response |
| LangGraph Agent Server, in-memory, custom auth | State substitution at 12 versions, 0.7.5 to 0.14.0, when policy lets a non-approver update the thread | No product fix. A supported deny-update policy blocked it | Postgres production behaviour (not tested); 0.14.2 and 0.14.3; any vendor response |
| OpenClaw shell-wrapper approvals | Representation mismatch in 2026.2.23, 3 of 3; rejected in 2026.2.24, 3 of 3 | Yes: 2026.2.24, published to npm on 25 February 2026, advisory GHSA-6rcp-vxwf-3mfp | A CVE; a CVSS score |
| OpenAI Agents SDK, per-call approval | Negative control: mutated call rejected 3 of 3 in 0.22.0 and 3 of 3 in 0.22.2 | Not applicable: behaved as intended | Sticky always_approve decisions, or apps that build their own approval around the SDK |
Agno. The deployment gave three roles JSON Web Tokens: a maker who could start and continue its own run, a separate administrator who could resolve approvals, and outsiders. The maker started a transfer, AgentOS stored an approval record for the exact tool and arguments, and the administrator approved it. The maker then continued the same run and supplied a tool execution with the original call identity but new arguments marked confirmed. AgentOS ran it. No approval for the new operation ever appeared in the approval list. The paper's reading of the source is that the continuation route checks only whether any approval is still pending for the run; once none is, it installs the caller's tool execution and dispatches it without comparing the arguments against the approved record. A research wrapper that stored the approved descriptor and compared it at use time blocked the attack while still running the legitimate transfer. That wrapper is the author's, not an Agno fix.
LangGraph. This is the conditional case, and it should be reported as one. The author built a separation of duties with a supported custom authorisation policy: the maker could update a shared pending thread but not resume it or run the transfer; the approver could inspect and resume. The maker used Agent Server's A2A message command surface to replace the pending tool call, keeping the same ID. The approver, having read the earlier interrupt, approved. Agent Server resumed from current state and the tool received the 2,000 unit transfer under the approver's identity. The author ran 12 versions natively and scanned 130 stable wheels, finding the forwarding path in every release from 0.7.5 to 0.13.4. The same policy written to deny the maker's update stopped it. So the finding is that LangGraph will not save you from a policy that lets the wrong person edit pending work, not that a default LangGraph install is exposed.
OpenClaw. The representation case. For shell-wrapper commands such as /bin/sh -c, the request carried extra positional arguments after an inline payload of the form $0 "$1". The approval showed only that inline payload. After an allow-once decision, 2026.2.23 released the whole vector and the hidden command ran. OpenClaw's own advisory describes the same root cause, says approval text "could omit executed argv carriers", and credits a researcher with the handle tdjackey for the report. The paper did not discover this bug; it reproduced a fixed, publicly disclosed one and used it as its only affected and fixed release pair.
OpenAI Agents SDK. The control that matters. The author serialised a paused run, edited the raw pending tool call to B while keeping its call ID, and resumed. Restoration worked, but the SDK raised an error on resume and the ledger stayed empty, in all six trials across two versions. The SDK keeps a canonical record of the invocation and checks it; its documentation scopes per-call approvals to the specific call ID. That is the answer to anyone who says pause and resume is inherently unsafe: it is not, if the product binds the decision to the action.
Reading the success rates properly
The trial counts are small and every one of them is 100 per cent or 0 per cent. That is what you expect from a logic flaw tested with a deterministic model fixture: the same inputs give the same result every time. The numbers show the path is reliable once the preconditions hold, not how often the preconditions hold.
What each reported count establishes, from Table 2 and section 4.2 of the paper
| Reported count | What it establishes | What it does not establish |
|---|---|---|
| Agno: 5 of 5 attack trials at each of 7 releases (35 in total); direct attack denied 3 of 3 on 3.0.9 | The substitution is repeatable and needed the approval, at those exact releases | An uninterrupted affected range; behaviour in 3.0.10; how many AgentOS deployments give a maker continue rights |
| LangGraph: strict positive at 12 versions; per-version trial counts not stated | The composition works across more than seven months of releases when the policy allows the update | That any real deployment writes such a policy; Postgres behaviour |
| OpenClaw: 3 of 3 on 2026.2.23, 3 of 3 rejected on 2026.2.24 | The fix closes the tested path without breaking unchanged commands | Whether a person would have noticed the incomplete dialog |
| OpenAI Agents SDK: 6 of 6 mutations rejected | Per-call approval stayed bound across serialisation | Safety of sticky approvals or of approval code an application writes itself |
Two further limits. First, everything was run by one person, with repeats on clean macOS and Linux environments but no independent reproduction. Second, because the approval step was scripted, the study says nothing about how often a real reviewer would approve a misleading dialog. That second question belongs to separate work, notably OWASP's Lies in the Loop entry, which records Checkmarx researchers manipulating an approval dialog in Claude Code to reach code execution from a GitHub issue.
Lab result or observed in the wild
The honest position for a risk register is therefore: a demonstrated design weakness in named software, with public fixes in one product, no fix in two, and no reported abuse. That is a reason to test your own approval paths now, before someone else does, not a reason to declare an incident.
Disclosure and vendor responses
The paper says the work was done "under coordinated-disclosure constraints" but deliberately leaves out private coordination messages and "unverified chronology". The practical effect is that the paper does not say whether, when or how Agno or LangChain were told, or what they replied. I have not found a public statement from either on the paper. The public record I could check on 21 September 2026 is this:
Public disclosure record, from GitHub advisories, the npm and PyPI registries, and the A2A repository, checked 21 September 2026
| Date (2026) | Event | Source |
|---|---|---|
| 24 February | OpenClaw 2026.2.23 published to npm | npm registry |
| 25 February | OpenClaw 2026.2.24 published with the fix; advisory GHSA-6rcp-vxwf-3mfp published, severity medium, no CVE | OpenClaw advisory, npm |
| 28 May | Second OpenClaw advisory, GHSA-2j8v-hwgc-x698, describes a similar shell-wrapper mismatch and lists 2026.5.18 as patched | OpenClaw advisory |
| 28 to 30 July | A2A issue 2080 opened and pull request 2081 merged, adding section 7.6.4: an auth-required task state is a coordination signal, not an authorisation grant | A2A repository |
| 10 September | Evidence cutoff for the paper | Paper, section 1 |
| 16 September | Agno 3.0.10 released; not tested in the paper | PyPI |
| 17 September | Paper posted to arXiv | arXiv |
On the second OpenClaw advisory, the author reports that no stable 2026.5.16 artifact exists and that the May releases already carried the February fix, so treats it as a duplicate. The npm registry agrees that there is no 2026.5.16 version. Calling it a duplicate is the author's reading, not a vendor ruling, and the paper says so.
The A2A history is a lesson about specification clarity, not a protocol bug, and the author is careful to say so. Before the change, the Agent2Agent spec described a task waiting for authorisation, suggested accepting further messages on that task, and used human approval of a destructive action as an example, without defining what exactly the approval covered. Issue 2080 and pull request 2081 now say that implementations must define and check operation-level authorisation. If you build on A2A, that sentence is now your job in writing.
What human approval buys, and what it does not
"Human in the loop" is a comforting phrase, and like most comforting phrases in security it describes an intention rather than a mechanism. The paper's approval-binding rule is short: a decision may authorise an operation only if the complete operation at the moment of use is materially the same as what the human reviewed, for the same principal, task and scope. Everything else is decoration.
The control, restated, drawing on sections 2 and 6 of the paper and OWASP's Lies in the Loop entry
| Human approval does buy | Human approval does not buy |
|---|---|
| A pause before a consequential action, if the product enforces it | Any guarantee that the action shown is the action that runs |
| A named person accountable for a decision | Protection when someone else can edit pending work after the decision |
| A chance to catch an obviously wrong request | Protection from a view that omits the harmful part of a correct-looking request |
| An audit point, if the record holds the full operation | Safety of sticky approve-always decisions for later calls |
| A second role, if the approver differs from the requester | Resistance to a reviewer who is tired, rushed or deceived by the dialog itself |
So where this site has recommended human approval for agent actions, the recommendation stands but needs a second sentence: approval is only worth having if the product can show that the approved operation is the executed operation. Asking a vendor "do you support human approval?" gets a yes from all four products in this paper. The question that separates them is "what does your product compare, at the moment of execution, against what the human approved?"
The defences that worked in the tests
Two strategies blocked every tested attack without stopping legitimate transfers.
Bind and revalidate. Keep a canonical record of what was approved, rebuild the operation that is about to run, and compare the two at the last possible moment, after all templating, state merging, continuation handling, wrapper expansion and default insertion. OpenClaw 2026.2.24 does this for the full argument vector. The OpenAI Agents SDK does it for per-call approvals. The author's wrapper did it for Agno. The paper calls this the portable defence, because it still works when legitimate updates or retries change state: any material change simply demands a fresh decision.
The paper lists what the approval record should bind at minimum: the tool identity and every material argument; the target, destination and type of side effect; the requesting principal and the task, thread or session scope; the approver and the policy that interpreted the request; a nonce, creation time, expiry and consumed flag; and a digest over the same descriptor shown to the human. The display must be generated from that record, not from a friendlier summary string while execution uses something richer. At use time: reject a missing, expired, consumed or out-of-scope decision; reject or re-ask on any material change; re-run policy on the resolved effect; consume the decision atomically with the action; and log both descriptors.
The paper also names three bindings that are not enough. Binding only a call ID fails when state can swap the arguments behind that ID, as in LangGraph. Binding only a run-level "approved" flag fails when a continuation can install a different tool call, as in Agno. Checking only when the dialog is shown fails when state changes before dispatch.
Prevent unauthorised mutation. Deny anyone but the approver, or the system, any write to pending approved state. The LangGraph deny-update policy did this. It is narrower: it depends on your role model and it does not help when legitimate processes can change pending operations.
Method, interests and limits
The method is careful in the right places. Each positive result had to pass paired controls: the unchanged approved action still ran, the attacker could not run B directly, outsiders were refused, and a fix or safe configuration blocked the mismatch while keeping the legitimate action. The author inspected the arguments that reached the tool, not HTTP status codes or model output. The paper also refuses to claim novelty for the underlying rule, crediting earlier work on consent integrity, action-bound approval and authorisation continuity; its contribution is the two-variant classification and evidence from released products.
The limits are the ones already stated: a purposive sample of four, one operator, no independent reproduction, configuration-specific results, and no measurement of human behaviour. None of the positive findings amounts to an accusation that a vendor ships a broken default. Agno's result depends on giving a maker continue rights; LangGraph's depends on a permissive custom policy; OpenClaw published a fixed release and an advisory the day after the affected release.
One interest worth noting without reading too much into it: the author discloses that OpenAI Codex helped with repository inspection, experiment scripts, evidence normalisation and drafting, and the one product that came out clean is OpenAI's. The author states that Codex output was not treated as evidence and the retained bundles are hash-checked. I see no sign that the control was chosen to flatter, and the SDK's documented per-call binding matches the result, but readers should know. There is a public evidence archive that reproduces the attacks; this briefing does not link it.
What to do, in order
Take this with you
Actions worth doing, in this order
- List every agent workflow where a human approval gates a consequential action: payments, shell commands, deployments, data deletion, outbound email, permission changes.
- For each, write down who can change pending work between the approval and the action: the requester, other users, remote agents, repository writers, background jobs.
- If you run Agno AgentOS, do not give the role that requests an action the right to continue a run whose approvals another role resolves, until you have tested 3.0.10 or later against a changed-arguments continuation.
- If you run LangGraph Agent Server with custom auth, deny non-approvers any update to a thread with a pending interrupt, and test the denial; confirm langgraph-sdk is 0.4.4 or later.
- If you run OpenClaw, confirm every node host and gateway is on 2026.2.24 or later and avoid durable allowlists for shell-wrapper commands.
- If you use the OpenAI Agents SDK, keep sticky always_approve decisions for low-risk tools only; the negative control does not cover them.
- Ask every agent vendor what their product compares at execution time against the approved operation, and whether the approval view is generated from the same record.
- Add a regression test per workflow: change one material field after approval through each route that can reach pending state, and assert at the tool that it was rejected or re-approved.
- Make the approval log hold the approved descriptor and the executed descriptor side by side, so a mismatch is visible in review.
- Rewrite any policy or checklist line that says only "require human approval" to say "require human approval bound to the exact operation executed".
The question that exposes the gap
Every product in this paper has an approve button, and a supplier questionnaire that asks whether human approval is supported would have scored them all the same. The paper's evidence says the difference lies in one line of code at the moment of execution. So the question to put to your agent platform, and to your own developers, is this: when the tool finally runs, what exactly does the system check against what the human saw, and who else could have touched it in between?
Key facts
Sources
- PrimaryLoopjacking: Hijacking Human-in-the-Loop Approval, abstract page: author, submission date 17 September 2026, subjectsarXivaccessed 2026-09-21
- PrimaryLoopjacking full text (HTML, read in full; PDF also retrieved): definition, variants, six conditions, product traces, Tables 1 to 3, trial counts, defences, limitations, ethics and AI usage statementarXivaccessed 2026-09-21
- PrimaryOpenClaw advisory GHSA-6rcp-vxwf-3mfp: shell-wrapper argv hidden under approval text, affected up to 2026.2.23, patched 2026.2.24, medium, reporter credit, published 25 February 2026OpenClawaccessed 2026-09-21
- PrimaryOpenClaw advisory GHSA-2j8v-hwgc-x698: later shell-wrapper argv advisory, published 28 May 2026, patched 2026.5.18OpenClawaccessed 2026-09-21
- Primaryopenclaw package metadata: publication times of 2026.2.23 and 2026.2.24, absence of 2026.5.16npmaccessed 2026-09-21
- Primaryagno release history: dates of tested releases and of untested 3.0.10Python Package Indexaccessed 2026-09-21
- Primarylanggraph-api release history: dates of 0.7.4, 0.7.5, 0.13.4, 0.14.0 and later untested releasesPython Package Indexaccessed 2026-09-21
- Primaryopenai-agents release history: dates of 0.22.0 and 0.22.2Python Package Indexaccessed 2026-09-21
- PrimaryOpenAI Agents SDK human-in-the-loop documentation: per-call approvals scoped to call ID, sticky always_approve decisionsOpenAIaccessed 2026-09-21
- PrimaryA2A issue 2080 on in-task authorisation scope, opened 28 July 2026A2A Projectaccessed 2026-09-21
- PrimaryA2A pull request 2081 adding section 7.6.4, merged 30 July 2026A2A Projectaccessed 2026-09-21
- PrimaryLangGraph SDK advisory GHSA-fvww-7h3r-vfhp: custom auth actions= ignored in langgraph-sdk 0.1.45 to 0.4.3, fixed 0.4.4LangChainaccessed 2026-09-21
- PrimaryAgno repository security advisories: checked for any approval-binding advisory (none found)Agnoaccessed 2026-09-21
- PrimaryHITL Dialog Forging (Lies in the Loop): OWASP recommends HITL for LLM01 and LLM06; Checkmarx Claude Code dialog caseOWASP Foundationaccessed 2026-09-21
- PrimaryCursor advisory for CVE-2025-54136: approved MCP entry changed without re-approvalCursoraccessed 2026-09-21


