Headless and CI
Run the agent without the terminal app, from scripts, CI, GitHub Actions or your own code.
| Tool | Use it for |
|---|---|
zidane run --json | Scripts and CI with the installed zidane command |
bun run headless | Evaluation harnesses in a repository checkout: local models, every event, transcripts, time limits |
runHeadless() | Many runs in one JS or TS process |
Approvals#
Headless runs never ask for approval. Every tool call runs, shell commands and edits included. Run them in a container, a CI runner or a throwaway checkout. --context docker (or e2b, daytona) runs the tools in a container or a remote sandbox (execution contexts).
zidane run --json#
zidane run --json --prompt "Fix the failing test in src/limits.test.ts" > result.json
jq -r .finalText result.jsonIt needs no TTY, never loads the terminal UI and skips the update check. Credentials come from provider environment variables (ANTHROPIC_API_KEY and others) or the machine's credentials file. Flags, the result object and exit codes: CLI.
Stream turns#
--stream-json prints one JSON line per finished turn, then the result object:
{"type":"turn","index":0,"turn":{"role":"assistant","content":[…]}}
{"type":"turn","index":1,"turn":{"role":"user","content":[…]}}
{"status":"completed","finalText":"…","turns":2,…}For live text deltas and tool calls, use bun run headless --output-format stream-json.
GitHub Actions#
name: zidane
on:
workflow_dispatch:
inputs:
task:
description: What the agent should do
required: true
jobs:
agent:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm install -g zidane-tui
- name: Run the agent
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
TASK: ${{ inputs.task }}
run: zidane run --json --prompt "$TASK" --model claude-sonnet-4-5 > result.json
- name: Summary
if: always()
run: jq -r '.finalText // .error.message' result.json >> "$GITHUB_STEP_SUMMARY"
- run: git diff --stat| Detail | Why |
|---|---|
ubuntu-latest | zidane-tui has Linux x64 and arm64 binaries (glibc). Alpine runners do not work. |
TASK | An env variable keeps the input out of the shell command |
ANTHROPIC_API_KEY | From secrets: the runner has no credentials file |
if: always() | A failed run exits non-zero. The summary still prints the error |
The job leaves its changes in the checkout for later steps to commit or open a pull request.
bun run headless#
bun run headless runs src/headless-cli.ts in a checkout, on the local provider by default (LOCAL_LLM_* variables). The prompt comes from an argument, --prompt, --prompt-file or stdin. --transcript writes the trajectory to a file.
echo "Add a /health route" | bun run headless --output-format stream-json --max-turns 30 --transcript run.jsonl
bun run headless print-tools --json # the exact tool schema the agent sees| Flag | Default | Description |
|---|---|---|
-p, --prompt <text> | DefaultNone | DescriptionPrompt to run. Also the first positional argument, or stdin. |
--prompt-file | DefaultNone | DescriptionRead the prompt from a file. |
-m, --model <id> | DefaultNone | DescriptionModel id (or LOCAL_LLM_DEFAULT_MODEL). |
-s, --system <text> | DefaultNone | DescriptionSystem prompt override. |
--thinking | Defaultoff | DescriptionThinking (reasoning effort) level. |
--provider | Defaultlocal | DescriptionModel provider. |
--base-url <url> | DefaultNone | DescriptionOpenAI-compatible base URL (or LOCAL_LLM_BASE_URL). |
--api-key-env <NAME> | DefaultNone | DescriptionEnv var holding the API key (or LOCAL_LLM_API_KEY). |
--vision | DefaultNone | DescriptionLocal/openai-compat model accepts image inputs. |
--image-in-tool-result | DefaultNone | DescriptionLocal/openai-compat accepts images in tool messages. |
--temperature <f> | DefaultNone | DescriptionSampling temperature (local provider only). |
--seed <n> | DefaultNone | DescriptionSampling seed (local provider only). |
--max-turns <n> | Default50 | DescriptionStop after this many agent turns. |
--max-tokens <n> | DefaultNone | DescriptionOutput token limit for each model call. |
--timeout <sec> | DefaultNone | DescriptionAbort the run after this many seconds (exit code 124). |
--output-format | Defaultjson | DescriptionStream-json emits the FULL event union as JSONL (start/text/thinking/tool_call/tool_result/turn/result); bun start --stream-json emits complete turns only. |
--transcript <path> | DefaultNone | DescriptionWrite the trajectory as JSONL. |
--transcript-format | Defaultturns | DescriptionShape of the transcript lines: zidane turns, or OpenAI chat messages. |
--no-thinking-transcript | DefaultNone | DescriptionDrop thinking blocks from the transcript. |
--schema <file.json> | DefaultNone | DescriptionEnforce structured output → result.output. |
--cwd <dir> | DefaultNone | DescriptionWorking directory for file tools. |
--import-foreign-configs | DefaultNone | DescriptionAlso load .claude/agents (user scope; project scope too with --project-agents). |
--no-markdown-agents | DefaultNone | DescriptionSkip markdown agents and the builtin Explore/Plan; spawn schema stays unfolded. |
--project-agents | DefaultNone | DescriptionAlso load project agents (<cwd>/.agents/agents, <cwd>/.zidane/agents); default: user-scope (~/) agent files and builtins only. |
-c, --context | Defaultprocess | DescriptionExecution context. |
--image <img> | DefaultNone | DescriptionDocker image for --context docker. |
--env KEY=VALUE | DefaultNone | DescriptionSet an env var in the context (repeatable). |
--pass-env VARNAME | DefaultNone | DescriptionForward a shell env var into the context (repeatable). |
--pregame <file> | DefaultNone | Descriptione2b/daytona: upload + run this setup script once the sandbox is ready, before prompting (shebang picks the interpreter; a non-zero exit aborts the run). |
--sandbox <id> | DefaultNone | Descriptione2b/daytona: connect to an existing sandbox by id instead of creating one (left running on exit). |
--sandbox-on-destroy | Defaultkill | Descriptione2b/daytona: what to do with a created sandbox at the end of the run (pause preserves state for reconnect via --sandbox; leave relies on provider TTL). |
--mcp <json|file> | DefaultNone | DescriptionMCP server config(s). |
--session-db <path> | DefaultNone | DescriptionPersist/resume a session. |
--session-id <id> | DefaultNone | DescriptionPersist/resume a session. |
--durability | DefaultNone | DescriptionWith --session-db: checkpoint runs; on resumes an interrupted run first (resume-ask never does here). |
--json | DefaultNone | DescriptionWith print-tools, print the tool schemas as JSON. |
-h, --help | DefaultNone | DescriptionPrint help and exit. |
--output-format | stdout |
|---|---|
json (default) | The result object at the end |
text | The agent's text as it streams |
stream-json | Every event as a JSON line |
--durability checkpoints the runs of a --session-db session. Headless never asks, so under resume-ask it does not resume an interrupted run. on resumes it first (crash recovery).
| Exit code | Means |
|---|---|
0 | Completed |
1 | Error |
2 | Bad usage |
124 | Timeout (--timeout) |
130 | Aborted |
zidane run or bun run headless#
Both print the same result object.
| What | zidane run --json | bun run headless |
|---|---|---|
| Needs | The installed zidane | A repository checkout and Bun |
| Default provider | anthropic | local |
--provider names | anthropic, openai, openrouter, cerebras, xai, google, google-vertex, arcee, baseten, neuralwatt, local, openai-compat | local, openai-compat, anthropic, openai, openrouter, cerebras, baseten, neuralwatt |
| Preset | --preset is ignored | No flag |
| Extra headers | --header, --headers-env | No flag |
| Output schema | --json-schema <JSON>, --schema <file> | --schema <file> |
| Provider message shape | --format zidane|provider | No flag |
| Codemode tool | --codemode | No flag |
| Turn limit and time limit | None | --max-turns (default 50), --timeout <sec> |
| Prompt from a file | No | --prompt-file |
| Streaming | Finished turns (--stream-json) | Every event (--output-format stream-json) |
| Transcript file | No | --transcript, --transcript-format openai, --no-thinking-transcript |
| Agents | Default | --project-agents, --import-foreign-configs, --no-markdown-agents |
| Tool schema dump | No | print-tools |
| Exit codes | 0, 1 | 0, 1, 2, 124, 130 |
Events#
type | Fields |
|---|---|
start | runId, provider |
text, thinking | delta |
tool_call | callId, name, input |
tool_result | callId, name, output, isError |
turn | index, turn |
spawn | event (before, complete, error), id, info |
error | message, errorType |
result | result: the result object |
Debug the prompt cache#
With ZIDANE_DEBUG set, zidane run, bun run headless and runHeadless() log prompt-cache breaks to stderr. Put cache in the value (ZIDANE_DEBUG=cache) to get a line for every turn.
In your own process#
For parallel runs, call runHeadless in one process instead of spawning one per task:
import { runHeadless } from 'zidane/headless'
import { local } from 'zidane/providers'
const result = await runHeadless({
prompt: 'Add a /health route that returns 200.',
provider: local({ baseURL: 'http://localhost:8000/v1', defaultModel: 'my-model' }),
cwd: '/tmp/rollout-42',
maxTurns: 30,
timeoutMs: 600_000,
onEvent: e => process.stderr.write(`${e.type}\n`),
})
console.log(result.status) // 'completed' | 'aborted' | 'error' | 'timeout'| Option or helper | Does |
|---|---|
onEvent | Receives the events |
schema | A JSON Schema. The run puts the object that matches it in result.output |
exitCodeForHeadlessResult(result) | Maps the status to the bun run headless exit codes |
setupTerminalOutput (zidane/output/terminal), setupStreamJsonOutput (zidane/output/stream-json) | The renderers bun start uses, for your own CLI |