Skip to content

What your agents can do

Securing Paddock answers one question: who can start a turn. This page answers the other one: what a turn can do once it has started.

A correctly-authenticated user, a scheduled trigger firing at 3am, and an attacker who got past your proxy all reach the same agent, with the same tools and the same credentials.

Every project agent is registered with a hard-coded tool list. Paddock generates herdctl.yaml at boot with a header reading “GENERATED by paddock-server. Do NOT hand-edit”, and rewrites it on every start:

Read Edit Write Bash Glob Grep WebFetch WebSearch Task
TodoWrite Skill NotebookEdit ToolSearch ScheduleWakeup Monitor
CronCreate CronList CronDelete mcp__playwright__*

The generated config also carries a denied_tools list — 23 Bash patterns covering sudo, chmod 777, and bare-root wipes like rm -rf / and rm -rf ~. It is worth having. It is not a security boundary, and Paddock’s own source says so:

a best-effort, defence-in-depth denylist, NOT a sandbox … these string patterns are trivially bypassable (a relative path, a $VAR, a find -delete) and are here only to make the obvious catastrophic footguns require deliberate rephrasing.

The gaps are deliberate and documented: rm -rf /usr/local/… is not caught, because the pattern that would catch it also blocks the legitimate absolute-path cleanup agents do all day. Narrow and honest beat broad and leaky. Real isolation is #7, open.

An agent’s working directory is where it starts, not where it is confined. Bash and Write are on the allowlist and nothing constrains the paths they touch. There is no chroot, no seccomp profile, and no per-agent uid.

  • A project is not a boundary. cd ../other-project works, as does reading that project’s .chats/ transcripts, its project.yaml, and the paddock.config.yaml above them all. If two projects need genuinely different trust levels, that needs to be two Paddock instances with separate data directories and credentials — not two projects.
  • The root workspace agent’s working directory is projectsRoot — the parent of every project. One grep -r reads every chat on the instance. See Workspaces.
  • An agent can edit paddock.config.yaml, where every instance capability flag lives.
  • send_file takes an absolute path. The attachment tool is injected into every keeper turn behind no flag, and a relative path resolves against the project directory while an absolute one is used as-is — “Because we copy rather than reference, there’s no sandbox.” Not an escalation for an agent that already has Bash, but it means on-box file contents reach the chat UI, and it applies to scoped trigger agents that have no other tools. See Sending files & images.

Paddock does not construct a filtered environment for agent subprocesses — they inherit the server process’s environment as-is. There is no allow-list and no redaction. An agent with Bash can read, out of /proc/self/environ:

  • CLAUDE_CODE_OAUTH_TOKEN / ANTHROPIC_API_KEY — your Anthropic credential
  • every env var a managementApi.clients[*].auth.ref points at (conventionally PADDOCK_MCP_TOKEN_*) — including write-scoped Management API tokens
  • whatever else your deployment exports into the server’s environment

Also on disk: if the instance is logged in with a Claude subscription, $CLAUDE_HOME/.credentials.json (default ~/.claude/) holds the OAuth refresh token, not just a short-lived access token. If you ever have to assume it leaked, rotating locally is not enough — revoke at Anthropic.

There is currently no control for this, so plan around it: give the instance the narrowest credentials that let it do its job, and don’t co-locate secrets belonging to other systems in the same environment.

Persistence: an agent can schedule its own future

Section titled “Persistence: an agent can schedule its own future”

ScheduleWakeup, CronCreate, CronList and CronDelete are on the default toolset, gated by no capability flag. A turn can register a durable wake that resumes its own session with a prompt of its choosing, persisted under the herdctl state directory (<PADDOCK_DATA_DIR>/.herdctl/ by default, or PADDOCK_STATE_DIR) and surviving a restart.

Unlike a trigger — which lives in project.yaml or .paddock/triggers/, shows up in the Triggers tab, and is visible in a diff — an agent-created wake has no operator-facing surface. If you want to know what your instance has scheduled for itself, you have to read herdctl’s state file directly.

The one capability dial that is per-project. Set it in project.yaml or the project’s Settings tab.

ModeBehaviour
defaultClaude asks before consequential tool calls
acceptEditsPaddock’s default. File edits proceed unprompted; other prompts stand
planRead-only planning; no mutations
bypassPermissionsEvery tool runs unprompted, with no gate at all

There is no instance-level default — every project independently starts at acceptEdits, including projects an agent creates via create_project.

A trigger or hook runs as its own agent whose run.tools list becomes that agent’s allowed_tools, so its capability is set by configuration rather than by instruction. This is the closest thing Paddock has to a per-agent allow-list.

project.yaml
triggers:
nightly-notes:
trigger: { type: schedule, cron: "0 3 * * *" }
run:
tools: [Read, Glob, Grep] # the capability IS this list
maxTurns: 30
permissionMode: default

Grant the narrowest set that does the job. Bash is described in Paddock’s own tool picker as “the broadest grant” — granting it to an unattended trigger means arbitrary shell on a schedule. Full schema in the hooks reference.

Limits that do hold: new triggers and hooks are disabled until you arm them, they default to maxTurns: 30, and a trigger’s prompt file must live under .paddock/triggers/ and end in .md.

The post-turn sweeper is the narrowest agent Paddock runs: no injected MCP tools, max_turns: 4, a cheap model, a working directory outside projectsRoot, and a system prompt whose first instruction is “You DO NOT use any tools — you only return text.” It decides what to write; Paddock does the writing, so a sweep can never mutate your code or trigger another sweep.

It is a good model for scoping unattended work. Note, per the trap above, that its tool-lessness rests on its prompt, its runtime and its lack of injected tools rather than on an enforced allow-list.

Per-project, project.yamldocker: true (or the Settings toggle) asks Paddock to run that project’s agent in a container. It defaults to false.

Where it does apply, a container gives you filesystem and capability isolation — only that project’s directory is mounted, and herdctl drops all capabilities and sets no-new-privileges. What it does not give you is network isolation (the container gets a normal bridge network with full outbound access) or protection for your Anthropic credential, which has to go in for the agent to work at all.

These gate the self-management MCP — the tools with which an agent manages Paddock itself. The five booleans all default off; maxSpawnDepth is a number.

FlagEnv varGrants
selfMcpEnabledPADDOCK_SELF_MCPRead: list projects/chats, read a chat
selfMcpWriteEnabledPADDOCK_SELF_MCP_WRITEStarts real turns: create_chat, send_message, fork_chat
selfMcpProjectsEnabledPADDOCK_SELF_MCP_PROJECTScreate_projectgit clone on a URL the agent chose
hooksMcpEnabledPADDOCK_HOOKS_MCPset_trigger / remove_trigger / run_trigger
browserMcpPADDOCK_BROWSER_MCPHeadless Chromium (accepts literal 1 only)
maxSpawnDepthPADDOCK_MAX_SPAWN_DEPTHDefault 1. How deep spawned turns keep these tools

Full semantics — the nesting (projectswriteread, with the trigger tools also requiring write; browserMcp stands alone), and why these do not gate an external /mcp client — are in the self-management MCP reference. Three things matter from the capability side:

  • The hooks gate is binary. An agent with the hooks MCP can create a hook at any capability, including Bash with bypassPermissions. Note this is not the only route to self-scheduling — CronCreate is on the default toolset already.
  • maxSpawnDepth: 1 means depth-1 turns — the direct children of a human- or trigger-initiated chat — keep the self-MCP write tools; their children do not. It is an anti-fork-bomb measure, not an authority boundary.
  • These are settable over PUT /api/instance-config. See below.
  • No spend cap, budget, or turn-rate limit. Cost is estimated for display, never enforced. Set limits at your Anthropic account.
  • No egress control. WebFetch, curl via Bash, DNS, git push to any repo the token allows, and send_file into the chat UI all work unrestricted — inside a container too. If this matters, it belongs in a firewall rule on the host.
  • No rate limiting on the HTTP API or the WebSocket. Each project caps concurrent turns at 10, but nothing bounds the rate at which they start.
  • No role model. Every authenticated user is fully privileged, and there is no per-resource authorization — “authenticated” does not mean “owns this chat”. PUT /api/instance-config can rewrite every flag in the table above, and in the default auth.mode: none it is reachable by anyone who can reach the port.
  • Model allow-lists are not a capability control — they filter the picker, not the turn. See Model allow-lists.

Paddock has no audit log. Detection is assembled from surfaces built for other purposes, so it’s worth knowing which ones answer a security question.

Regularly, and quickly:

  • Diff CLAUDE.md, project.yaml and .paddock/. These are standing instructions and standing capability. If your projects root is a git repo, an unexplained edit to Curated notes, a new trigger, or a changed permissionMode shows up as a diff — treat one like an unexplained change to a CI config. Nothing commits for you, so this only works if you (or a scheduled job) actually commit.
  • Your Anthropic usage graph. Paddock enforces no budget, so a step change in spend with no matching human chats is the loudest signal available — and the only one that fires when an agent has been quiet in the UI.
  • The chat list’s scheduled and spawned badges, and run History, for turns nobody asked for.
  • herdctl’s state file in the state directory, for wakes an agent scheduled for itself. Nothing in the UI shows these.

After anything surprising: git log --all on every repo the instance’s token can reach (including branches nobody opened a PR from), paddock.config.yaml for flipped flags, and the Triggers tab for hooks you don’t recognise — before restarting, since a persisted wake fires on boot.

What you will not see: no record of what an agent read, no outbound-request log, no per-tool-call log outside the transcript, and no alerting of any kind.

  • The instance’s Anthropic and GitHub credentials are scoped to the minimum — a fine-grained PAT for just the repos this instance should touch, and an API key used by nothing else so you can revoke it without collateral.
  • No unrelated system’s secrets are exported into the Paddock server’s environment.
  • Paddock runs as a dedicated unprivileged OS user, not root and not your login — it has no per-agent uid, so the Unix user is the real boundary.
  • paddock.config.yaml is not writable by that user, if you rely on env vars to keep capability flags off — note this also disables the Config screen’s writes.
  • Branch protection on main for every repo the instance can push to.
  • permissionMode reviewed per project; bypassPermissions only where you’d accept losing the working directory.
  • Unattended work runs on scoped agents with the narrowest run.tools — and no schedule relies on tools: [] to be safe.
  • Self-MCP write / projects / hooks flags are on only if you meant them, set via env vars if you don’t want them rewritable over the API.
  • Spend limits set at your Anthropic account.
  • The data directory is backed up, and you have run a restore.
  • You have read Prompt injection and untrusted content — the capabilities above are what an injected instruction gets to use.