Skip to content

Prompt injection and untrusted content

Securing Paddock defends against someone reaching your instance. What your agents can do describes the authority a turn holds. This page is the case where those combine badly: nobody breaks in, your agent works exactly as designed, and the instructions it follows did not come from you.

A Claude Code agent reads text and acts on it. It has no reliable mechanism for distinguishing “this is my operator’s instruction” from “this is text I happened to read while doing my job.” When both arrive in the same context window, an instruction embedded in the content can be followed.

This is prompt injection, and it is a property of tool-using agents generally, not a Paddock defect. Paddock’s relevance is that it puts such an agent on a persistent host, gives it Bash and your credentials, and then arranges for it to read things — on a schedule, unattended, sometimes at your invitation.

The useful mental model is the confused deputy: the agent is not compromised, it is being used, with its own legitimate authority, by whoever wrote the text it read.

Every one of these is a documented, intended feature. That is the point — you cannot spot the risky ones by looking for something that seems wrong.

WebFetch and WebSearch are on the default toolset for every project agent. “Read this page and summarise it” puts a third party’s text into a context that also holds Bash and your Anthropic token.

PADDOCK_BROWSER_MCP widens this: a headless Chromium the agent drives, pulling arbitrary rendered content — including text a page shows only to a browser — into the same context. It is off by default, and that default is worth keeping.

This is the sharpest one, because a repo supplies more than prose.

For a repo-backed project, the checkout is a Claude Code settings source. That means the repo can carry, and your agent will load on every turn:

  • its own CLAUDE.md, which Paddock’s sweeper deliberately leaves alone as upstream-owned — so it is standing instruction on every turn;
  • a .claude/ directory: settings, skills, slash commands, subagents — and settings.json can declare hooks that execute shell commands on tool events.

That last one is not prompt injection at all. It is direct code execution from the repository, and it does not require the model to be fooled by anything.

Whoever can merge to that repository can run commands on your box.

For most people the upstream is their own repo and this is fine. It stops being fine for a fork you don’t control, a vendored dependency, or a repo an agent chose itself — which is why create_project (gated by selfMcpProjectsEnabled, off by default) runs git clone on an agent-supplied URL with no allow-list. Paddock’s Management-API policy layer classes the same operation as high-risk.

Transcripts are JSONL files in each project’s .chats/ directory. So read_chat (gated by self-MCP read) is the tidy route, not the only one: any agent can Grep its own project’s entire history — one level above the checkout, for a repo-backed project — and the root workspace agent, whose working directory is projectsRoot, can read every chat on the instance with one command, regardless of any flag. On a shared instance, treat every chat as readable by every agent.

Worth tracing carefully, because it is the least obvious.

After each turn the sweeper reads a digest of recent chat activity and rewrites the project’s OVERVIEW.md, CHANGELOG.md, and — for non-repo-backed projects — the Curated notes section of CLAUDE.md, wholesale. CLAUDE.md is auto-loaded by Claude Code on every turn in that project.

So there is a path from content in one chat → the sweeper’s prompt → CLAUDE.mdthe standing instructions of every subsequent chat in that project. The sweeper is itself the narrowest agent Paddock runs, but it is a writer, and what it writes is read by agents that do have tools. Its prompt also absorbs a project-controlled .paddock/hooks/sweep.md, capped at 8,000 characters, and a curator trigger’s prompt file — and that one resolves inside the checkout for a repo-backed project, so upstream can steer the sweeper too.

Nothing commits these edits for you. If your projects root is a git repo and you commit regularly, git log on CLAUDE.md is a real audit trail; otherwise the previous content is simply gone, since the section is replaced rather than merged.

  • Issue and PR bodies — an agent running gh issue view is reading text written by anyone who can open an issue. On a public repo, that is anyone.
  • Scheduled work that fetches something — a nightly trigger running gh pr list or WebFetch reads content nobody looked at first, at a time nobody is watching.
  • Attachments and uploaded filesallowedTypes is described in Paddock’s own source as “a hygiene/UX guardrail, NOT a security boundary”, and invalid input falls back to the instance default (allow-all out of the box).

The instinct is to scope the agent’s tools. That instinct is right, and you should — but be clear about how much it buys. The project agent’s toolset isn’t configurable at all, and where you can scope, the grant isn’t yet enforced — including the trap that an empty tool list is passed to the runtime as no list.

And the deeper problem survives both: the agent needs the dangerous tools to do the job. An agent that reviews PRs needs to read PRs and run tests. You cannot separate “read the untrusted thing” from “have the capability” by wishing.

So tool scoping is a good default and a bad last line of defence.

Ordered by how much they buy you.

Don’t let untrusted content and privileged credentials meet in the same instance. The strongest available separation is a second Paddock instance — its own data directory, port, OS user and credentials — not a second project. A project is not a boundary: project agents have Bash and no confinement, so they can read each other’s files and each other’s transcripts. A home-lab setup describes running more than one.

Scope the credentials — and know which one you can’t. A fine-grained GitHub PAT limited to the repos this instance should touch binds regardless of what the agent decides: an injected instruction to push elsewhere simply fails. The Anthropic credential is different — the agent needs it, so it cannot be scoped away. Give the instance its own API key so you can revoke it without collateral, and set a spend limit on that key, since Paddock enforces none.

Trust the upstream of any repo-backed project the way you trust a CI runner. Its CLAUDE.md is standing instruction and its .claude/settings.json can run commands. Review what a new checkout brings with it before pointing an agent at it.

Make it recoverable. Branch protection plus required PRs on every repo the token can reach means the worst an agent’s push can do is open a branch you delete. Back up the data directory and test a restore. Rehearse revocation once — Anthropic key, GitHub PAT, PADDOCK_MCP_TOKEN_*, stop the service — so you can do it from your phone.

Keep the risky capability flags off. selfMcpProjectsEnabled, hooksMcpEnabled and browserMcp all default off; set them via environment variables if you want them to stay that way. Each one turns “the agent read something bad” into a larger event.

Know how you’d notice. See How you would know — especially the CLAUDE.md diff and your Anthropic usage graph.

Consider docker: true — but read the constraint first. It only applies on the batch path, and it isolates the filesystem, not the network or your Anthropic credential.

  • No sandbox. Per-agent filesystem confinement is #7, open.
  • No enforced tool boundary. #319, open.
  • No environment filtering. Agents inherit the server’s credentials.
  • No content provenance. Nothing marks a span of context as “this came from the web”.
  • No egress control. If you need it, it belongs at your network layer.
  • Repo-backed projects point at repositories whose merge access you trust — their CLAUDE.md is standing instruction and their .claude/settings.json can execute commands.
  • Sweeper-written changes to CLAUDE.md get reviewed (non-repo-backed projects only — the sweeper leaves a repo’s own file alone), and your projects root is actually committed so there’s a diff to review.
  • Work that reads third-party content is separated at the instance level, with its own credentials — not merely a different project on the same box.
  • browserMcp, selfMcpProjectsEnabled and hooksMcpEnabled are off unless deliberately needed.
  • You have written down, somewhere you’ll find it again, what this instance’s credentials could destroy — and confirmed you could revoke them all in five minutes.

The capability-side checklist — toolset, permission mode, flags, spend, backups — is on What your agents can do.