English documentation · runtime 7.24.4 · SDK 2.6.7. Content is maintained with runtime development; see each guide's scope and review date.
Headless CLI (ax-code run)
Status: Active Scope: current-state Last reviewed: 2026-09-22 Owner: AX Code runtime maintainers
ax-code run is the one-shot, non-interactive entry point for AI coding agents and scripts. It submits a single
prompt, prints the assistant’s final reply, and exits — no terminal UI, no interactive prompts. This guide covers the
option surface, output contract, exit codes, and copy-paste recipes for scripting and CI.
Invoking a run
There are four ways to supply the prompt. They compose in order: --prompt-file, then -p/--prompt, then the positional
message, then piped stdin.
# Positional after -- (safest; flags after -- are never consumed as options)
ax-code run --model qwen -- "Review this change"
# Explicit prompt flag
ax-code run --model qwen --prompt "Review this change"
ax-code run --model qwen -p "Review this change"
# Prompt read from a file (not an attachment)
ax-code run --model qwen --prompt-file ./prompt.txt
# Piped stdin (used as the prompt when no positional/--prompt is given)
printf 'Review this change' | ax-code run --model qwen
When stdin is not a TTY, its contents are appended to the composed prompt, so it is the whole prompt when nothing else
is supplied. The stdin reader waits for a 300 ms quiet window before giving up on an open pipe, so write the prompt
promptly and close stdin — or use --prompt-file for large or slowly produced prompts. --prompt-file - reads the
prompt from stdin explicitly (usage error when stdin is a TTY); the implicit piped-stdin append never runs a second
time for that invocation.
-f/--file attaches files to the message and never replaces the prompt; it is repeatable:
ax-code run --model qwen --file README.md --file src/main.ts -- "Summarize these"
An attachment must live inside the project directory: the server refuses attachments outside it regardless of
permission rules, so the CLI rejects them up front with a usage error. Copy the file into the project or pass its content
with --prompt-file. --add-dir grants tools access to extra directories but does not change this rule.
Attachment mime types are inferred from the extension: png, jpg, jpeg, gif, and webp map to their image/*
type and pdf to application/pdf, so binary attachments reach the server as their real type instead of text/plain.
Anything else — including a file with no recognized extension — is sent as text/plain, and a directory attachment is
classified application/x-directory.
Steering the run
Three flags steer a run without changing the prompt text. All are long-only and kebab-case.
--append-system-prompt TEXT / --append-system-prompt-file PATH
Appends extra text to the system prompt after the agent and environment system prompts — it appends and never replaces the built-in system prompt. The two forms are mutually exclusive. A file variant is convenient for longer instructions; exactly one trailing newline is trimmed, and an empty text (or unreadable file) is a usage error before anything is submitted.
ax-code run --model qwen --append-system-prompt "Answer in English only" -- "Review this change"
--disallowed-tools a,b
Disables tools by id for the run (comma-separated, repeatable). It is applied through both server mechanisms: deny
rules on the created session cover new runs, and a per-request tools map also covers resumed --session/--continue
runs. Denying bash also denies the monitor tool’s command, which runs through the same shell launcher; a call that
hits a deny rule fails as a tool error and counts as a denial for the blocked-run status. Unknown ids are not an error — MCP tool ids are dynamic — but under the default format each id outside the
built-in tool set prints one stderr warning (suppressed by --quiet).
ax-code run --model qwen --disallowed-tools bash,write -- "Audit this module without mutating anything"
--add-dir PATH
Grants the agent access to one additional directory (repeatable): an external_directory allow rule for
<resolved-path>/* is added to the new session’s permission rules, covering the directory and everything beneath it.
Each path must exist and be a directory (resolved against the caller cwd like --file). The rule is applied when the
session is created, so under --session/--continue it cannot take effect — the CLI prints
--add-dir applies only to new sessions on stderr and continues. It does not change --file containment: attachments
must still live inside the project directory. It covers the file tools (read, glob, grep, list, edit, write); a shell
command that reaches into the directory through a dynamic path still triggers the interactive-only path-access
prompt, which a headless run auto-rejects, so prefer the file tools or pass the file content explicitly.
ax-code run --model qwen --add-dir ../design-docs -- "Read ../design-docs/spec.md and summarize it"
Choosing a model
List usable IDs with ax-code models. Pass the resulting provider/model value to --model (-m):
ax-code models # one "provider/model" ID per line
ax-code models --json # one JSON document
ax-code models --json prints a single document of the form
{"models":[{"id":"provider/model","provider":"...","model":"...","connected":true}]}.
The family names deepseek, glm, and qwen resolve to their Flash defaults, so
ax-code run --model qwen -- "..." works without spelling out a full provider/model ID. Omitting --model uses the
configured default; the effective agent and model are printed to stderr as > Agent · model.
Output formats
--format accepts default (the default), json, jsonl, or ndjson (jsonl and ndjson are aliases for json).
Default (text)
The default format prints only the final assistant text on stdout. All progress, tool activity, and diagnostics go to
stderr, starting with a > Agent · model · ses_... header that includes the session id, so a multi-turn caller can
resume with --session without switching to --format json (--quiet suppresses the header). ANSI colors are
disabled when stderr is not a TTY or when NO_COLOR is set (any value), so a piped run never emits escape codes.
JSON stream (--format json)
--format json prints a newline-delimited JSON (NDJSON) event stream — one JSON object per line, not a single JSON
document. Events include step_start, text, tool_use, reasoning (only with --thinking), permission_denied,
error, and step_finish. The stream always ends with exactly one result line:
{
"type": "result",
"timestamp": 1727000000000,
"sessionID": "ses_...",
"status": "completed",
"text": "...",
"permissionDenials": 0,
"usage": { "input": 1200, "output": 80, "reasoning": 0, "cacheRead": 0, "cacheWrite": 0 }
}
status is completed, blocked, or error. usage carries token counts only — input, output, reasoning,
cacheRead, cacheWrite — and is omitted entirely when the counts are unknown; there is no cost field. When a run
fails before submitting (bad flag, unknown model, etc.), the stream format prints one line before exiting:
{ "type": "error", "error": { "code": "usage", "message": "..." } }
The error.code is one of:
| Code | When it fires |
|---|---|
usage |
Bad or contradictory flags, a missing prompt, or an unreadable --output-schema. |
provider |
Unknown provider id, or a known provider that is not connected. |
model |
Unknown model id on a known provider, or an unparseable --model value. |
session |
A missing or rejected --session id (preflighted before the run submits). |
attach |
The attached server could not be reached, rejected the credentials (401/403), or no managed runtime is running for --runtime. |
internal |
Any otherwise-unhandled rejection. |
Exit codes
| Code | Meaning |
|---|---|
| 0 | Completed. |
| 1 | Usage error, provider/model error, stream error, or --output-schema validation failure. |
| 3 | Blocked — at least one permission denial and no successful mutating tool call (result.status is blocked). |
| 124 | Timed out (--timeout elapsed; result.status is timeout). |
| 130 | Cancelled by SIGINT or SIGTERM (session aborted on the server; result.status is cancelled). |
Sandbox
--sandbox read-only|workspace-write|full-access selects the isolation mode (default full-access). In headless runs
permission asks are auto-rejected and reported as permission_denied events, so a run that needs a write it was not
allowed to make reports blocked and exits 3. This covers subagents too: asks raised in child sessions created by the
task tool are rejected the same way, and their permission_denied events carry the child sessionID. Tool calls
refused by a permission deny rule (for example from --disallowed-tools) or by the read-only sandbox count as
denials as well. The interactive question and plan_exit tools are always disabled in a headless run, including on
resumed sessions. Use read-only only when no mutation is expected; workspace-write keeps writes inside the project.
Under --attach the flag is also sent as a per-request isolation policy in every prompt body; the server applies the
stricter of its own mode and the requested policy, so it can only tighten. The same per-request policy is sent for
locally owned servers, keeping the behavior uniform.
Structured output
-o/--output-file <path> writes the final assistant text to a file. --output-schema <file> validates the final text
as JSON against a JSON Schema file; a mismatch is reported as an error event with result.status error and exit
- The schema file is preflighted before the model runs — an
unreadable, unparseable, or non-object schema is a usage error before anything is submitted. On success the parsed
schema is also sent to the model as the run’s
json_schemaoutput format, and the server retries an invalid reply up to twice before the CLI’s own final validation runs as the backstop. The final output is the serialized structured object (one line of JSON): it is what stdout,--output-file, andresult.textcarry:
ax-code run --model qwen --output-schema ./answer.schema.json -- "Return a JSON object with a summary field"
Sessions and resuming
-c/--continue— continue the most recent session.-s/--session <id>— continue a specific session by ID.--fork— fork the session before continuing (requires--continueor--session).--show-history— print visible session history when resuming (requires--continueor--session).--attach <url>— attach to an already-running server instead of starting one; combine with--dirto target a project directory on that server. A server protected withAX_CODE_SERVER_PASSWORDtakes--password(or the same variable on the caller side); a managed runtime takes its token fromAX_CODE_RUNTIME_TOKEN.--runtime— attach to the managed runtime of the project directory (--diror the caller cwd) started withax-code runtime start, resolving its URL and token from the private runtime record. With no running runtime the run fails before any request with error codeattachand a message that names the start command.--runtimeand--attachare mutually exclusive.
Recipes
One-shot
ax-code run --model qwen -- "Fix the failing test in src/parser.ts"
Bound a run with a timeout
ax-code run --timeout 120 --model qwen -- "Fix the failing test in src/parser.ts"
--timeout <seconds> aborts the run on the server and exits 124 (result.status is timeout) when the run outlives
the bound, so a stuck agent cannot hold a CI job open indefinitely. The bound covers the whole invocation — it is
armed before the first server call, so even a black-holed --attach host or a hung startup terminates on time (in
that early case the result line carries an empty sessionID).
Wait for background subagents
ax-code run --await-background 300 --timeout 360 --model qwen -- \
"Delegate the independent checks, then integrate their results"
--await-background <seconds> keeps this invocation open for background task children created by its session and
the parent follow-up turns triggered by their results. It is opt-in and capped at 3600 seconds. The final reply and
JSON result.text come from the last completed parent turn. If the children or their follow-up do not settle within
the wait bound, the run reports an error and exits 1. --timeout remains the overall bound. Project scheduled tasks
run in separate sessions and are not part of this wait. This one-shot command does not claim due project schedules;
a persistent backend owns their dispatch.
Parse the JSON stream
result=$(ax-code run --format json --model qwen -- "..." | tail -n 1)
echo "$result" | jq -r '.status'
echo "$result" | jq -r '.text'
The result line is always the last line, so tail -n 1 isolates it even when the stream ends early.
Read-only review
ax-code run --sandbox read-only --model qwen -- "Review this diff for bugs"
Structured JSON output with a schema
cat > answer.schema.json <<'JSON'
{"type":"object","properties":{"summary":{"type":"string"}},"required":["summary"]}
JSON
ax-code run --model qwen --output-schema answer.schema.json --output-file answer.json \
-- "Return JSON with a one-sentence summary"
Resume a session
# Start a session and note the sessionID from the result line
ax-code run --format json --model qwen -- "Draft the outline" | tail -n 1
# Continue it
ax-code run --model qwen --session ses_... -- "Now write section 2"
Attach a file
ax-code run --model qwen --file docs/spec.md -- "Summarize the attached spec"
Machine-readable commands
ax-code run --format json emits a newline-delimited event stream (see JSON stream); it is
the only --json surface that streams. Every other read-only command that carries a --json flag follows a stricter
contract: on success it writes exactly one JSON document to stdout, and on failure stdout stays empty while a single
{"error":{"code","message"}} document goes to stderr and the exit code is 1.
Commands with --json:
ax-code session list --jsonax-code models --jsonax-code providers list --jsonax-code agent list --jsonax-code mcp list --jsonax-code mcp auth list --jsonax-code stats --jsonax-code context --jsonax-code memory status --jsonax-code memory list --jsonax-code task list --json/ax-code task show <taskID> --jsonax-code schedule list --json/ax-code schedule show <taskID> --jsonax-code runtime list --json/ax-code runtime status --jsonax-code doctor --jsonax-code risk <sessionID> --jsonax-code wiki status --jsonax-code workflow list --json/ax-code workflow status <runID> --json
ax-code runtime status prints its JSON document unconditionally — the --json flag is accepted for consistency but
the status action always emits JSON on stdout.
Calling ax-code from another agent
When wrapping ax-code run from a script, CI step, or another agent:
- Use
--format jsonand read the last line — theresultrecord is always the final line, even when the stream ends early. - Capture
sessionIDfrom that result line for any follow-up turn; pass it back with--session. - Never use
--continuefrom concurrent callers — it resumes the most recent session, which races when multiple callers are active; pass an explicit--sessionid instead. - Pass an explicit
--modelso the run does not depend on a mutable configured default. - Always pass
--timeoutso a stuck agent cannot hold the caller open. - Close stdin, or use
--prompt-file/--prompt-file -: the implicit pipe reader gives up after a 300 ms quiet window and truncates a slow pipe silently. - Set
NO_COLOR=1, or rely on the non-TTY detection, so a piped run never emits ANSI escape codes. - Run from the project directory: a home or multi-repo parent directory is refused in non-interactive mode. Set
AX_CODE_ALLOW_BROAD_DIR=1to override that guard. - Usage failures print nothing on stdout (help and the one-line error go to stderr), so a mistyped command leaves
stdout empty with exit 1. Under
--format jsonthe usage failure is also written as oneerrorline on stdout. - A signal that arrives while the process is still loading, before the
runcommand is active, ends the process with exit 130 and no output; once the command is active, SIGINT and SIGTERM always produce the single terminalresultline with statuscancelled.