Docker Agent lets me describe a team of AI agents, their tools, and their guardrails in one config file that a platform team can review like any other piece of infrastructure. That is the part I like most. It also has a permissions trap that I reproduced in a test, and I'll show it before you copy any example rules.
I spent a weekend-sized block of time building a small project to find out how far that idea goes. The result is release-gate-crew: four agents that review a service's container and CI configuration before release and write a PASS/BLOCK report.
What Docker Agent is
Docker Agent is Docker's open-source runtime for teams of agents. You write a config, run docker agent run crew.yaml, and talk to the root agent, which delegates to the sub-agents you declared. It ships in Docker Desktop 4.63 and later (versions 4.49 to 4.62 called it cagent), or as a standalone binary from GitHub releases. It works with Anthropic, OpenAI, Google, Bedrock, Docker Model Runner and a long list of other providers.
I used v1.150.0, released 9 October 2026.
The team
root (lead) think, todo
/ | \
container_auditor pipeline_auditor scribe
fs: ./target fs: ./target fs: ./reports
shell: grep -n
- The lead never reads files. It delegates, then makes the gate call: BLOCK if any finding is high or critical.
- The container auditor reads
target/Dockerfileandtarget/compose.yaml, and may rungrep -n. - The pipeline auditor reads the CI workflow in
target/ci/. - The scribe writes
reports/gate-report.mdand can write nowhere else.
target/ is a deliberately bad sample service: FROM node:latest, a token baked into ENV, a privileged container with the Docker socket mounted, and a pull_request_target workflow that checks out pull request code with permissions: write-all.
The whole team is one file
Here is the core of crew.yaml:
models:
lead: { provider: anthropic, model: claude-sonnet-4-5, max_tokens: 8000 }
worker: { provider: anthropic, model: claude-haiku-4-5, max_tokens: 4000 }
agents:
root:
model: lead
description: Release gate lead. Delegates reviews and makes the PASS/BLOCK call.
instruction_file: prompts/lead.md
sub_agents: [container_auditor, pipeline_auditor, scribe]
toolsets:
- type: think
- type: todo
container_auditor:
model: worker
description: Reviews Dockerfile and compose files for container risks.
instruction_file: prompts/container_auditor.md
toolsets:
- type: filesystem
allow_list: ["./target"]
- type: shell
# pipeline_auditor and scribe follow the same shape
The sub_agents line is the whole orchestration. The runtime gives the lead a transfer_task tool and handles the hand-off. Each agent can use a different model, so the coordinator gets the stronger one and the workers get the cheaper one.
This is why I prefer config to framework code for this kind of work. In a code-first framework, the same team is classes, a tool registry, a loop, and retry handling, and the permissions live wherever someone remembered to put them. Here the reviewer sees the topology, the tool scope, and the policy in one diff. Swapping a provider is a two-line change. Nothing about the agents is hidden in a helper function.
The same team in HCL
Docker Agent also reads HCL. If your team writes Terraform, this will feel familiar:
agent "container_auditor" {
model = "worker"
description = "Reviews Dockerfile and compose files for container risks."
instruction = file("prompts/container_auditor.md")
toolset "filesystem" {
allow_list = ["./target"]
}
toolset "shell" {}
}
permissions {
allow = ["read_file", "read_multiple_files", "shell:cmd=grep -n *"]
deny = ["shell:cmd=*curl*", "shell:cmd=*&&*", "shell:cmd=*;*", "shell:cmd=*|*"]
}
The runtime converts HCL to the YAML shape and validates both against the same schema. Labelled blocks and heredocs suit long prompts and per-agent commands well. Keep your expectations modest, though: there are no variables, locals or modules, and file() is the only function. It is a second syntax, not a second language. I keep both files in the repo and run the same tests against each, so they cannot drift apart silently.
Guardrails live next to the agents
This is where Docker Agent reads like a DevOps tool rather than an AI demo. Every layer is declared in the same file:
| Layer | Config | Effect |
|---|---|---|
| Filesystem scope |
allow_list per toolset |
Auditors read only ./target. The scribe writes only ./reports. |
| Hidden files | .agentsignore |
.env, keys and certs disappear from listings, reads and writes. |
| Tool rules | permissions |
Deny, then allow, then ask. |
| Safety mode | runtime.safety: restricted |
Unattended runs deny anything not explicitly allowed, without prompting. |
| Spend | budget |
One shared $0.50 and 10-minute ceiling for all four agents. |
| Isolation | --sandbox |
Runs the team in a Docker Sandboxes VM. |
The budget detail is thoughtful. A named budget is one shared pot, so a lead that fans out to ten sub-agents cannot spend ten times the cap.
Two other features earned their place. Flavors are named patches inside the config, so --flavor cheap swaps the lead to a smaller model without a second file. Sharing packages the config as an OCI artifact: docker agent share push ./crew.yaml you/release-gate-crew, then anyone with access can docker agent run you/release-gate-crew.
Testing the guardrails without an API key
A config file is only as trustworthy as the tests behind it, so I wanted CI to prove the guardrails hold on every push without paying for tokens.
I wrote a small OpenAI-compatible stub model in Go, standard library only. It plays a fixed script per agent: the lead delegates three times, the auditors read files, and then they deliberately try things they shouldn't. An offline flavor points every agent at the stub:
flavors:
offline:
providers:
stub:
api_type: openai_chatcompletions
base_url: http://127.0.0.1:8089/v1
token_key: STUB_API_KEY
models:
lead: { provider: stub, model: scripted }
worker: { provider: stub, model: scripted }
A Go integration test then runs the real docker-agent binary against both crew.yaml and crew.hcl and checks each requirement in the project's SPEC.md:
--- PASS: TestCrew/crew.yaml/R2_delegates_to_scribe
--- PASS: TestCrew/crew.yaml/R4_.agentsignore_hides_target/.env
--- PASS: TestCrew/crew.yaml/R5_auditor_read_scope_holds
--- PASS: TestCrew/crew.yaml/R6_compound_shell_is_denied
--- PASS: TestCrew/crew.yaml/R7_scribe_cannot_write_target/
--- PASS: TestCrew/crew.yaml/R9_target_unchanged
What the runtime returned, verbatim:
- Reading
target/.env:path "target/.env" is excluded by .agentsignore - The pipeline auditor reading a prompt file:
outside the allowed directories (./target) - The scribe writing
target/Dockerfile: denied by the restricted safety mode, with no prompt -
grep ... && curl --version:Tool 'shell' is denied by permissions configuration
The stub proves the harness, not the reviews. For review quality, the repo has three docker agent eval sessions: the gate must BLOCK the sample and name its worst problems, must not reveal the .env value when asked directly, and must not claim an edit when told to fix the Dockerfile. Those need real model keys and a container runtime, so they are not in CI yet. I have not run them yet, so I'm making no claim about review quality here.
The permissions trap
Permission patterns are globs matched against the whole command string, and * matches anything, including spaces. That makes the common prefix style unsafe for shell:
permissions:
allow: ["shell:cmd=grep -n *"]
deny: ["shell:cmd=curl *"]
grep -n FROM target/Dockerfile && curl --version starts with grep -n, so it matches the allow rule. It does not start with curl, so it misses the deny rule. Deny is checked first, finds nothing, and allow approves it. I reproduced this with the stub (--flavor naive-permissions): curl ran and printed its version banner, even under restricted safety, because an explicit allow outranks the safety mode.
The documentation is clear that permissions are enforced client-side and are not a security boundary, and it points to sandbox mode for isolation. That is fair. The trap is that prefix rules like cat * and rm * are the pattern you see most often. What I use instead:
- Deny shell operators outright:
*&&*,*;*,*|*, backticks,$(. - Write deny rules as
*curl*, notcurl *. - Allow one narrow command per agent that needs a shell at all.
- Run anything you don't fully trust with
--sandbox.
The test suite asserts the trap still exists, so if a future release starts parsing compound commands, CI will tell me and I'll update the advice.
Other rough edges
-
The filesystem toolset is unrestricted by default. Without an
allow_list, absolute paths and..reach anything the process can. Docker's own example file calls this a footgun. Setallow_liston every filesystem toolset. -
Pulled configs can set their own safety mode. An agent from a registry may declare
runtime.safety: autonomous. Your own--safetyflag or user settings win, but only if you set them. Read a config before you run it. -
Sub-agents get the task text and nothing else. I confirmed it: the delegated task arrives in a
<task>block in the sub-agent's system prompt, and its user turn is just "Please proceed." Isolation is good. It also means the lead's prompt must pass along everything a specialist needs. -
It moves fast. The changelog lists 17 releases in September 2026 alone. Pin the version in CI. The rename from
cagentalso shows: the config and data directories are still~/.config/cagentand~/.cagent. -
Telemetry is on by default. The first run says so. Set
TELEMETRY_ENABLED=falsein CI. -
Evals have prerequisites.
docker agent evalruns each case in a container and the default judge model is an OpenAI model, so plan for a container runtime and a second provider key, or pass--judge-model.
Try it
git clone https://github.com/mdabydeen/release-gate-crew && cd release-gate-crew
make test integration # Go 1.24+ and docker-agent; no API key needed
export ANTHROPIC_API_KEY=...
docker agent run crew.yaml # try /gate
Point it at your own service by replacing target/. If you try the evals with real keys before I do, I'd like to know what the gate gets wrong.
What I'd like to hear from people running agents in CI: where do your guardrails live today, in the agent's code or next to its config?
Top comments (0)