DEV Community

Mike Dabydeen
Mike Dabydeen

Posted on AI-assisted

A Weekend Project With Docker Agent: Four Agents, Two Config Files, One Permissions Trap

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
Enter fullscreen mode Exit fullscreen mode
  • 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/Dockerfile and target/compose.yaml, and may run grep -n.
  • The pipeline auditor reads the CI workflow in target/ci/.
  • The scribe writes reports/gate-report.md and 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
Enter fullscreen mode Exit fullscreen mode

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=*|*"]
}
Enter fullscreen mode Exit fullscreen mode

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 }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 *"]
Enter fullscreen mode Exit fullscreen mode

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:

  1. Deny shell operators outright: *&&*, *;*, *|*, backticks, $(.
  2. Write deny rules as *curl*, not curl *.
  3. Allow one narrow command per agent that needs a shell at all.
  4. 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. Set allow_list on every filesystem toolset.
  • Pulled configs can set their own safety mode. An agent from a registry may declare runtime.safety: autonomous. Your own --safety flag 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 cagent also shows: the config and data directories are still ~/.config/cagent and ~/.cagent.
  • Telemetry is on by default. The first run says so. Set TELEMETRY_ENABLED=false in CI.
  • Evals have prerequisites. docker agent eval runs 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
Enter fullscreen mode Exit fullscreen mode

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)