Most prompt-injection papers on my morning arxiv skim follow the same arc: a new way to trick a coding agent, a scary success rate, good luck out there. One on today's list runs the other way. It doesn't try to make the model better at spotting malicious text. It assumes the model will fall for it, and makes falling for it harmless. My thesis for this post: in a multi-agent coding harness, the permission table is a better place to stop prompt injection than the prompt.
What it does#
Authority Is Not a String: A Capability-Scoped Harness for Prompt-Injection-Resistant Coding Agents, by Dimitrios Stamatios Bouras, Yihan Dai and Sergey Mechtaev at Peking University, opens with a diagnosis I think is exactly right. Inside a coding agent's sandbox, tools carry ambient authority: if the agent can name a path or type a command, it can act on it. Indirect prompt injection simply borrows that authority. Someone plants an instruction in a README, a docstring or the output of a trojaned test fixture, the agent reads it, and the resulting tool call looks exactly like a legitimate one.
Their system, CapScope, takes the defense out of the model's hands. Before the agent reads a single byte of repository content, a preflight LLM call sees only trusted input (the user's request and the project's file tree, names not contents) and predicts which files and commands the task needs. That prediction becomes a frozen, task-wide authority ceiling held by the host, outside the model's context, so nothing the agent reads later can widen it. Capabilities are typed: Read and Write over canonicalized path prefixes, Exec over allowed argument-vector prefixes. What separates it from earlier policy work like Progent is delegation. When the orchestrator spawns a sub-agent, it proposes a grant, and the host installs only the part contained in the ceiling. Scopes can narrow (src/** down to src/parser.py) but never widen. The test runner gets Read on tests and Exec on pytest; the patcher gets Write on src. Every tool call is checked against the store of the agent that issued it. It's classic object-capability thinking (no ambient authority, attenuation on delegation), and it ships as an extension to the Pi coding agent that hooks the pre-dispatch tool call, with no changes to Pi's internals.
The key result#
The setup is a four-agent repair workflow (orchestrator, runner, patcher, verifier) on five small one-bug Python projects, attacked through five surfaces (README, AGENTS.md, skill files, source comments and tool output) under four authorization conditions, three trials each: 300 runs on qwen3.5-flash. With no policy, the injected action executed in 47 of 75 runs. A static global denylist, the kind most of us actually ship (block .env, .ssh, rm -rf, sudo, curl), managed 46 of 75. A tighter global policy derived from the trusted task got to 33 of 75. CapScope: 3 of 75. And it didn't get there by being useless: it completed 68 of 75 repairs, tied with the task-specific policy and four short of the unrestricted baseline's 72. The number I keep staring at is a different one, though. Under CapScope, the model still proposed the injected action in 34 of 75 runs. The model fell for the injection almost half the time. The harness just refused to carry it out.
Why it matters#
That 46-of-75 row should make anyone running a coding agent uncomfortable, because it describes the configuration most of us have: a denylist for the obvious hazards, an allowlist for the test command, applied to the whole session. It barely helps because the injected actions were deliberately plausible. The paper's motivating example is a fake CI note in test output telling the agent to write an attacker's signing key into src/auth/keys.py. Plausible actions don't trip denylists. What worked was two ideas together: derive authority before reading anything untrusted, and give every sub-agent its own slice. The drop from 33 to 3 is what the per-agent split buys. A global policy, however tight, has to be the union of what every role needs, so the test runner inherits the patcher's write access. That makes the security win a dividend of decomposition. If you've already split a Claude Code-style workflow into explorer, implementer and verifier sub-agents for context hygiene, you've done the hard part. You just haven't given them different keys.
Here's what I'd change in practice. First, compute each sub-agent's grant from the task and the file tree before it reads anything, and make it strictly narrower than its parent's. A verifier needs Exec on the test command and nothing else. You don't need to fork your harness for this: CapScope is a pre-tool-call hook that checks the issuing agent's grant and blocks, which is the same shape as a PreToolUse hook. Second, move AGENTS.md and CLAUDE.md to the untrusted column. AGENTS.md was the most effective surface against the task-specific policy (11 of 15) and the only one that got anything past CapScope. Third, log proposals, not just executions. Blocked attack proposals in 31 of 75 runs is a free, high-signal intrusion detector that most harnesses currently throw away.
The caveats#
Tiny and single-model. Five one-bug Python repos, one backbone, 15 runs per surface cell. Refactors and migrations need wide ceilings, which is exactly where leaks and false denials would grow. The authors say as much.
The preflight is the new single point of failure. It's an LLM call too. Two of CapScope's seven failed repairs came from a preflight that forgot the write the patcher needed. All three leaks came through AGENTS.md, via grants that sat inside the ceiling but were too broad (a directory instead of a file) or were shaped by injected text in the orchestrator's subtask.
It's slow. Mean run time went from 145 to 316 seconds, largely because a blocked model keeps retrying: 27 of the 34 runs with an attack proposal tried repeatedly. Token cost isn't reported. You'd want a circuit breaker on repeated denials.
It scopes calls, not consequences. The authors are explicit that CapScope "does not stop the model from following an injected instruction." An allowed Exec on pytest still runs whatever conftest.py the attacker committed, the same trap as the scoped-looking Bash(python:*) grants in yesterday's harness-config audit. Sandboxing is still your job.
The takeaway#
I'm filing this next to SABER from June, whose lesson was that safety lives in the harness, not the weights. What's new here is a concrete pattern with a number attached: capabilities derived from trusted input, held outside the context and narrowed on every delegation. My change after reading it: per-role tool and path scopes on every sub-agent definition, and repo instruction files treated as untrusted input by default.