Agents
Wherever you set agent: <name> — in .goga/config.yml (build.agent, build.review.agent, build.review.additional.agent, pipeline.agent) or in a workflow-file (workflow.stages.<name>.agent, workflow.extend.<name>.agent) — goga resolves that name into a wrapper script inside the Docker container. The wrapper is what actually runs the AI agent during goga build and goga pipeline; it presents the agent's CLI in a uniform shape so goga does not care which concrete agent is underneath.
Resolution is pure string concatenation — there is no whitelist and no validation. A missing wrapper surfaces as a runtime error when the container tries to invoke it, not from goga itself. The full mechanic, baseline wrappers, per-agent env variables, and the custom-agent path are covered below.
How agent resolves to a wrapper
Resolution invariant:
The agent field is optional in build, build.review, build.review.additional, and pipeline: at config load, an absent / YAML-null / empty / whitespace-only value resolves to None (it is not an error). resolve_wrapper_path is invoked only for a non-None value — config-file agent values are whitespace-stripped at load, and the routine then forwards the value verbatim (no stripping or case-folding of its own; workflow-file agents reach it as authored), so an empty value never reaches resolution. What None means differs by consumer: goga build stops in-container with exit 1 when the effective build.agent is None (the guard reads the post-amendment configuration before any state write or pass), whereas goga pipeline carries None through and lets a per-stage workflow agent or the pipeline's own default cover the absent global agent. A None build.review.agent inherits build.agent — the review pass always runs as its own pass on the review agent's wrapper (its existence is validated in-container before the pass), with the review env (build.review.env) composed around the pass subprocess only; build.review.additional.agent inherits the review agent the same way and carries the external review (under strategy: short the review pass itself runs on its wrapper).
Edge cases:
| Edge case | What happens | Where it surfaces |
|---|---|---|
agent: "" / whitespace-only |
Resolves to None at config load (the field is optional — absent/empty/whitespace all collapse to None). goga build then stops at the in-container agent guard when the effective value is still None (an amendment can supply it); goga pipeline does not require it (a per-stage workflow agent or the pipeline's default covers the absent global agent). |
goga config prints null; goga build exits non-zero inside the container (before any state write or pass); goga pipeline proceeds. |
agent: "CodEx" (case mismatch) |
Resolves to /home/goga/bin/CodEx-as-claude.sh. Case-sensitive filesystem → file not found. |
Runtime error inside the container. goga does no case folding. |
Wrapper file missing in image (custom Dockerfile forgot COPY) |
Path resolves but the file is absent. | Runtime error inside the container. No upfront validation by goga. |
Wrapper present but not executable (forgot chmod +x) |
Permission denied. | Runtime error inside the container. |
cursor configured without CURSOR_API_KEY |
The wrapper is env-based, not credential-file-based — there is no credential mount to fall back on. | Wrapper exits with error (CURSOR_API_KEY is required). See cursor. |
workflow.stages.<name>.agent: <unknown> |
Wrapper path is composed verbatim; no validation against a known agent set. | Runtime error inside the container. See workflows. |
Baseline wrappers
The image ships the baseline wrappers:
agent value |
Wrapper file | Wrapper class |
|---|---|---|
claude |
claude-as-claude.sh |
Invocation-shape |
codex |
codex-as-claude.sh |
Format-converter (jq) |
cursor |
cursor-as-claude.sh |
Format-converter (jq) |
opencode |
opencode-as-claude.sh |
Format-converter (jq) |
qwen |
qwen-as-claude.sh |
Format-converter (jq) |
The wrapper class describes how each wrapper produces the Claude Code stream-json output: invocation-shape wrappers forward arguments nearly verbatim to an underlying CLI binary that owns its own agent loop, and format-converter wrappers translate the agent's JSONL into stream-json via jq.
Environment variables per agent
Env variables are forwarded into the container through the standard env layering (home.env → git identity → CLI -e / extra_env → engine variables in the env-file; the task env (pipeline.env, build.env, build.review.env) applies in-container around the launched binary only, with the CLI -e entries winning over it) — see Home configuration.
claude
| Variable | Required | Default | Purpose |
|---|---|---|---|
ANTHROPIC_API_KEY |
yes | — | API key for the Claude API. Absence surfaces inside the wrapper as an auth error. |
ANTHROPIC_DEFAULT_HAIKU_MODEL |
no | Claude default | Override for the Haiku-class model slot. goga init suggests this when claude is the agent. |
ANTHROPIC_DEFAULT_SONNET_MODEL |
no | Claude default | Override for the Sonnet-class model slot. Suggested by goga init. |
ANTHROPIC_DEFAULT_OPUS_MODEL |
no | Claude default | Override for the Opus-class model slot. Suggested by goga init. |
ANTHROPIC_MODEL |
no | Claude default | Override for the main model slot. Suggested by goga init. |
ANTHROPIC_BASE_URL |
no | Claude default | Base URL for an Anthropic-compatible gateway or proxy. Suggested by goga init. |
codex
| Variable | Required | Default | Purpose |
|---|---|---|---|
CODEX_MODEL |
no | codex default | Model selector — the full model id, for example gpt-6-astra, gpt-6-sol or gpt-6-luna; a bare family name like astra is not a valid id. goga init suggests this when codex is the agent. |
CODEX_REASONING |
no | codex default | Reasoning effort passed through as model_reasoning_effort — none, minimal, low, medium, high, xhigh or max. |
CODEX_SANDBOX |
no | danger-full-access |
Sandbox mode. Default disables codex sandboxing so the agent can run builds and modify the workspace without restrictions. |
CODEX_VERBOSE |
no | 0 |
Set to 1 to include command execution output in the codex response — useful for debugging pipeline/build failures. |
cursor
The cursor agent runs through the cursor CLI bundled in the goga image: the wrapper forwards the prompt and environment to the cursor agent and streams its --output-format stream-json events through a jq translator — assistant text and thinking blocks reach the feed in real time (thinking surfaced as narrative text), terminal tool calls are shown as Bash actions, file-writing tools (edit_file/write_file) as Edit actions, every other tool passes through under its native name. The agent loop itself — tool use, multi-turn, file writes — runs inside the cursor agent, exactly as it runs inside the claude agent for claude.
| Variable | Required | Default | Purpose |
|---|---|---|---|
CURSOR_API_KEY |
yes | — | Authorization token, read from the environment. The wrapper exits with an error when unset. |
CURSOR_MODEL |
no | (unset — Cursor default) | Model selector. An empty value or "auto" uses the cursor default; any other value selects that model. |
The cursor wrapper is env-based, not credential-file-based — there is no host credential file to bind-mount. Both variables are forwarded exclusively through the env layering.
The cursor agent runs non-interactively with every tool call auto-approved — an unanswered interactive approval prompt would hang the stage — and the prompt is always taken from stdin and forwarded to cursor-agent as a positional argument (the CLI does not read stdin itself). These launch settings are fixed by the wrapper.
opencode
The opencode agent runs through the OpenCode CLI bundled in the goga image: the wrapper streams its run --format json JSONL through a jq translator — assistant text and reasoning blocks reach the feed in real time (reasoning surfaced as narrative text), bash tool calls are shown as Bash actions, edit/write as Edit actions, every other tool passes through under its native name, tool actions appear once per call on its terminal state (completed/error), and the session ends with exactly one result event on the final step finish.
| Variable | Required | Default | Purpose |
|---|---|---|---|
OPENCODE_MODEL |
no | opencode default | Model in provider/model format, e.g. openai/gpt-4o. |
OPENCODE_VARIANT |
no | opencode default | Model variant / reasoning effort, e.g. high, medium, low. |
OPENCODE_EFFORT |
no | — | Alias for OPENCODE_VARIANT when OPENCODE_VARIANT is unset. |
OPENCODE_REASONING |
no | — | Alias for OPENCODE_VARIANT when both OPENCODE_VARIANT and OPENCODE_EFFORT are unset. |
OPENCODE_VERBOSE |
no | 0 |
Set to 1 to include [step started] markers for each step. |
OPENCODE_CONFIG_CONTENT |
no | {"permission":{"*":"allow"}} |
Inline opencode config as JSON. The wrapper deep-merges the auto-approve permission set into it and appends its output-rules instruction file, so custom settings survive. Invalid JSON fails the wrapper. |
Variant precedence: OPENCODE_VARIANT > OPENCODE_EFFORT > OPENCODE_REASONING. The first set value wins; the rest are ignored.
OPENCODE_CONFIG_CONTENT is how the agent is pointed at a custom OpenAI-compatible endpoint: declare a provider on @ai-sdk/openai-compatible and select it via OPENCODE_MODEL — the built-in openai provider speaks the OpenAI Responses API, which many gateways reject on the first tool result:
{
"provider": {
"mygateway": {
"npm": "@ai-sdk/openai-compatible",
"name": "My Gateway",
"options": {
"baseURL": "https://llm.example.com/api/v1",
"apiKey": "{env:OPENAI_API_KEY}"
},
"models": { "my-model": {} }
}
}
}
with OPENCODE_MODEL=mygateway/my-model (and OPENAI_API_KEY supplied through the same env layering).
qwen
The qwen agent runs through the qwen CLI bundled in the goga image: the wrapper forwards the prompt and environment to the qwen agent and streams its --output-format stream-json events through a jq translator — assistant text and thinking blocks reach the feed in real time (thinking surfaced as narrative text), shell tool calls (run_shell_command/shell depending on the qwen-code release) are shown as Bash actions, file-writing tools (edit/write/write_file) as Edit actions, every other tool passes through under its native name, and messages from qwen subagents are filtered out so only the main agent's stream is shown. The agent loop itself — tool use, multi-turn, file writes — runs inside the qwen agent, exactly as it runs inside the claude agent for claude.
Because the qwen agent speaks the OpenAI Chat Completions protocol, one agent setting serves any OpenAI-compatible endpoint: Qwen Cloud, DeepSeek, OpenRouter, OpenAI direct, or a local vLLM/ollama instance. The env vars are named OPENAI_*, not QWEN_* — the agent label is just a goga name; the protocol is OpenAI.
| Variable | Required | Default | Purpose |
|---|---|---|---|
OPENAI_MODEL |
yes | — | Model selector. No default — a server-side default would be unpredictable. The wrapper exits non-zero when unset. |
OPENAI_BASE_URL |
no | qwen default | Endpoint base URL of the OpenAI-compatible server. Used only when set. |
OPENAI_API_KEY |
no | (unset) | API key for the endpoint. The wrapper works without it (local servers often need none). |
The qwen wrapper is env-based, not credential-file-based — there is no host credential file to bind-mount. All three variables are forwarded exclusively through the env layering.
The qwen agent runs non-interactively with every tool call auto-approved — an unanswered interactive approval prompt would hang the stage — and the prompt is always taken from stdin. These launch settings are fixed by the wrapper.
Custom agents
Any name works as agent: <name> as long as /home/goga/bin/<name>-as-claude.sh exists in the Docker image and is executable. There is no registration step.
Two paths, both through a custom Dockerfile:
Path A — via the dockerfile: field. When .goga/config.yml declares a top-level dockerfile (see Top-level and Example configuration), goga build --update / goga pipeline --update build the image from that Dockerfile:
FROM qarium/goga-python-<python-version>:<goga-version> # or any baseline language image
COPY myname-as-claude.sh /home/goga/bin/myname-as-claude.sh
RUN chmod +x /home/goga/bin/myname-as-claude.sh
Path B — extend a baseline image. If dockerfile: is not yet set, create .goga/Dockerfile (via the goga init "Custom Dockerfile" step, or manually) with the same FROM + COPY + chmod shape, then register its path in the dockerfile: field.
Wrapper contract. The script must:
- Read the prompt from stdin (the way the
claudeCLI consumes a piped prompt); - Ignore or carefully parse CLI flags that goga passes through (
--model,--effort,--dangerously-skip-permissions, etc.); - Emit Claude Code stream-json on stdout: an
assistantenvelope followed by aresultevent.
The simplest baseline wrapper (claude-as-claude.sh) is a near-no-op that just forwards arguments; the codex/qwen/opencode/cursor wrappers are format-converters that translate JSONL into stream-json via jq. Use them as reference shapes when designing your own.
If agent: myname is set but the wrapper is not COPY'd into the image or is not executable, the container fails at runtime (file not found / permission denied). goga does not validate this up front.
Relationship to goga connect
Two different
agentconcepts. The runtimeagent(this section) picks which CLI binary runs inside the goga Docker container duringgoga build/goga pipeline.goga connectis a separate, host-side mechanism that installs goga skills and commands into an AI agent (claude/codex/cursor/opencode/qwen) as a target. They are orthogonal: you can rungoga connect claude codexto get goga skills inside both of your host-installed CLIs, and still setbuild.agent: codex— in that case the codex wrapper runs inside the container, not your host-side CLI.