Skip to content

Headless and CI

Run the agent without the terminal app, from scripts, CI, GitHub Actions or your own code.

ToolUse it for
zidane run --jsonScripts and CI with the installed zidane command
bun run headlessEvaluation 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#

Terminal
zidane run --json --prompt "Fix the failing test in src/limits.test.ts" > result.json
jq -r .finalText result.json

It 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:

JSON
{"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#

.github/workflows/zidane.yml
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
DetailWhy
ubuntu-latestzidane-tui has Linux x64 and arm64 binaries (glibc). Alpine runners do not work.
TASKAn env variable keeps the input out of the shell command
ANTHROPIC_API_KEYFrom 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.

Terminal
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
bun run headless flags
FlagDefaultDescription
-p, --prompt <text>NonePrompt to run. Also the first positional argument, or stdin.
--prompt-fileNoneRead the prompt from a file.
-m, --model <id>NoneModel id (or LOCAL_LLM_DEFAULT_MODEL).
-s, --system <text>NoneSystem prompt override.
--thinkingoffThinking (reasoning effort) level.One of off, minimal, low, medium, high, xhigh, max, adaptive.
--providerlocalModel provider.One of local, openai-compat, anthropic, openai, openrouter, cerebras, baseten, neuralwatt.
--base-url <url>NoneOpenAI-compatible base URL (or LOCAL_LLM_BASE_URL).
--api-key-env <NAME>NoneEnv var holding the API key (or LOCAL_LLM_API_KEY).
--visionNoneLocal/openai-compat model accepts image inputs.
--image-in-tool-resultNoneLocal/openai-compat accepts images in tool messages.
--temperature <f>NoneSampling temperature (local provider only).
--seed <n>NoneSampling seed (local provider only).
--max-turns <n>50Stop after this many agent turns.
--max-tokens <n>NoneOutput token limit for each model call.
--timeout <sec>NoneAbort the run after this many seconds (exit code 124).
--output-formatjsonStream-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.One of text, json, stream-json.
--transcript <path>NoneWrite the trajectory as JSONL.
--transcript-formatturnsShape of the transcript lines: zidane turns, or OpenAI chat messages.One of turns, openai.
--no-thinking-transcriptNoneDrop thinking blocks from the transcript.
--schema <file.json>NoneEnforce structured output → result.output.
--cwd <dir>NoneWorking directory for file tools.
--import-foreign-configsNoneAlso load .claude/agents (user scope; project scope too with --project-agents).
--no-markdown-agentsNoneSkip markdown agents and the builtin Explore/Plan; spawn schema stays unfolded.
--project-agentsNoneAlso load project agents (<cwd>/.agents/agents, <cwd>/.zidane/agents); default: user-scope (~/) agent files and builtins only.
-c, --contextprocessExecution context.One of process, docker, e2b, daytona.
--image <img>NoneDocker image for --context docker.
--env KEY=VALUENoneSet an env var in the context (repeatable).
--pass-env VARNAMENoneForward a shell env var into the context (repeatable).
--pregame <file>Nonee2b/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>Nonee2b/daytona: connect to an existing sandbox by id instead of creating one (left running on exit).
--sandbox-on-destroykille2b/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).One of kill, pause, leave.
--mcp <json|file>NoneMCP server config(s).
--session-db <path>NonePersist/resume a session.
--session-id <id>NonePersist/resume a session.
--durabilityNoneWith --session-db: checkpoint runs; on resumes an interrupted run first (resume-ask never does here).One of on, resume-ask, off.
--jsonNoneWith print-tools, print the tool schemas as JSON.
-h, --helpNonePrint help and exit.
--output-formatstdout
json (default)The result object at the end
textThe agent's text as it streams
stream-jsonEvery 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 codeMeans
0Completed
1Error
2Bad usage
124Timeout (--timeout)
130Aborted

zidane run or bun run headless#

Both print the same result object.

Whatzidane run --jsonbun run headless
NeedsThe installed zidaneA repository checkout and Bun
Default provideranthropiclocal
--provider namesanthropic, openai, openrouter, cerebras, xai, google, google-vertex, arcee, baseten, neuralwatt, local, openai-compatlocal, openai-compat, anthropic, openai, openrouter, cerebras, baseten, neuralwatt
Preset--preset is ignoredNo flag
Extra headers--header, --headers-envNo flag
Output schema--json-schema <JSON>, --schema <file>--schema <file>
Provider message shape--format zidane|providerNo flag
Codemode tool--codemodeNo flag
Turn limit and time limitNone--max-turns (default 50), --timeout <sec>
Prompt from a fileNo--prompt-file
StreamingFinished turns (--stream-json)Every event (--output-format stream-json)
Transcript fileNo--transcript, --transcript-format openai, --no-thinking-transcript
AgentsDefault--project-agents, --import-foreign-configs, --no-markdown-agents
Tool schema dumpNoprint-tools
Exit codes0, 10, 1, 2, 124, 130

Events#

typeFields
startrunId, provider
text, thinkingdelta
tool_callcallId, name, input
tool_resultcallId, name, output, isError
turnindex, turn
spawnevent (before, complete, error), id, info
errormessage, errorType
resultresult: 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:

TypeScript
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 helperDoes
onEventReceives the events
schemaA 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

Reference#