acpx-faq · v0.1.1 · 2026-09-09 · sha256 7701e763680dde1a

acpx-faq v0.1.1B

Immutable. This exact content is served forever at /api/v1/blob/7701e763680dde1a.

---
name: acpx-faq
description: Run coding agents (codex, claude, Antigravity/agy) headlessly through the acpx ACP CLI. Use before launching or prompting an acpx subagent, and when an acpx command fails, a session is not found, or a prompt seems lost.
metadata:
  version: "0.1.1"
  categories: "agents, operations"
  topics: "acpx, acp, agent-orchestration, troubleshooting, headless-agents"
  upstream: "acpx@0.15.1"
  openclaw:
    homepage: https://github.com/tenequm/skills/tree/main/skills/acpx-faq
    emoji: "🔌"
---

# acpx FAQ

Drive coding agents headlessly through [acpx](https://github.com/openclaw/acpx) (>= 0.15.1).
Command syntax: `acpx --skill show acpx` - the binary ships its own reference and is the
tiebreaker on syntax. That doc is strong on command shape and silent on failure semantics: it
has no exit-code table, no `status` state table, and does not mention `--mcp-config` at all.
This file is the other half.

Read the Invariants, then your agent's section in **Per agent** - each is a complete
launch-to-result recipe. After that: Sessions, Completion, MCP, Limits, Failures.

## Invariants

1. **Global flags precede the agent subcommand.** `--cwd`, `--model`, `--approve-all`,
   `--agent`, `--format`, `--timeout`, `--ttl`, `--mcp-config` are all global. The subcommand
   accepts only `-s`, `--no-wait`, `-f` (and `exec` also takes `--config-option`). Putting a
   global flag after the agent exits **2** with `error: unknown option '--cwd'`.
2. **`-s <name>` never creates a session.** It resolves one, walking from `--cwd` up to the
   git root. No match exits **4**. Create first with `sessions ensure --name <n>`.
3. **`status` is not a turn signal.** It reports the queue-owner process, not the turn, and
   keeps saying `running` after the turn has ended. Never gate automation on it.
4. **Permission flags only gate requests the adapter chooses to raise.** Neither the codex nor
   the claude adapter asks before writing files, so `--deny-all` does not stop writes.
   Isolation comes from the `--cwd` you hand it, never from a flag.
5. **`[done] end_turn` and exit 0 are not proof of success.** A content-filter kill, an MCP
   load failure, or a truncated turn all end that way. Read the stream, or require the agent
   to write a result file you can check.

## Per agent

Each section is self-contained: flags, launch, and the quirks that bite while it runs.

### agy / Antigravity - the `--agent` escape hatch

**acpx has no Antigravity adapter and never will get one.** Issue #362 was closed as
externally blocked: Antigravity ships no supported ACP stdio mode. The built-in `gemini` agent
is the *public Gemini CLI*, a different product, and it is dead for Code Assist:

```
[error] RUNTIME: This client is no longer supported for Gemini Code Assist for individuals.
To continue using Gemini, please migrate to the Antigravity suite of products
```

The only route is Google's own signed ACP server through the raw-command escape hatch:

```bash
D=/abs/real/dir                              # must exist: roots resolve through realpath
acpx --agent ~/.local/lib/antigravity-acp/agy_acp_server.par \
     --cwd "$D" --model gemini-3.7-flash-medium --timeout 1800 \
     exec 'Carry out $D/brief.md. Write your report to $D/report.md.'
```

- **Effort lives in the model id** - `gemini-3.7-flash-{low,medium,high}`,
  `gemini-3.8-flash-high`. This server exposes no separate effort option.
- **First run only:** `[error] RUNTIME: Authentication required`. Re-run once with
  `ACPX_AUTH_OAUTH_PERSONAL=1` and complete the browser sign-in; credentials persist in
  `~/.gemini` and every later run needs nothing.
- **Never pass a positional agent with `--agent`** - exit **2**,
  `Do not combine positional agent with --agent override`.
- **Check the tier before sending anything private.** The `oauth-personal` path can report
  `currentTier: {'id': 'free-tier'}`, whose terms permit human review of submitted prompts and
  code for up to 18 months. Run with `--verbose` and grep `loadCodeAssist response` to see it.
- **agy hides MCP failures inside a successful-looking turn.** A failed server appears as
  inline text - `MCP load failed for pond: ... expect initialized request, but received: ...
  "server/discover"` - in a stream that still ends `[done] end_turn` with exit 0. Grep the
  output for `MCP load failed`, do not trust the exit code.
- The CLI (`agy`) and this ACP server keep **separate state under the same home**: two
  onboardings, and authenticating one does not authenticate the other.

### codex

```bash
D=/abs/dir
acpx --cwd "$D" codex sessions ensure --name work            # idempotent; -s cannot create
acpx --cwd "$D" codex set model gpt-5.6-sol -s work          # -> model set: gpt-5.6-sol
acpx --cwd "$D" codex set reasoning_effort high -s work      # -> config set: ... (5 options)
acpx --cwd "$D" --approve-all --timeout 5400 --ttl 0 --format quiet --suppress-reads \
     codex -s work 'Carry out ./brief.md. Write your report to ./report.md.' > run.log 2>&1 &
```

For a one-shot, skip the session entirely - `exec --config-option` sets model and effort
inline (0.14.0+), applied after `--model` and before the prompt:

```bash
acpx --cwd "$D" --timeout 1800 codex exec --config-option reasoning_effort=low 'Summarize ./DESIGN.md'
```

- **The adapter floats.** `@agentclientprotocol/codex-acp@^1.1.5` is a caret on a 1.x, so it
  tracks the latest 1.x on every `npx` resolution. A codex-side change can land without you
  upgrading acpx.
- **OpenAI's content filter kills benign turns** and the turn ends looking clean
  (`[done] end_turn`, exit 0). Vocabulary like race / sweep / exploit / attack in a filename,
  comment or prompt triggers it. Read the transcript tail for the flag line before believing
  completion. Recover by re-prompting the same `-s <name>` (context survives): rename the
  artifact neutrally and list the remaining steps explicitly.
- **Queue a follow-up onto a live session** by prompting the same name again - it runs after
  the current turn rather than interrupting. This is how you course-correct a running executor
  without relaunching it.
- **Headful takeover:** `sessions show <n>` prints a `sessionId` that is the ordinary codex
  rollout id, so `codex resume <sessionId>` opens the same thread in a TUI with model and
  effort intact. **Close first** - `sessions close <n>` - or you get
  `already has an active writer (code -32600)`.
- `codex queue --thread` does **not** reach an acpx-driven session (see What acpx cannot do).

### claude

```bash
D=/abs/dir
acpx --cwd "$D" --model claude-opus-5 claude sessions ensure -s work
acpx --cwd "$D" --model claude-opus-5 --approve-all --suppress-reads --timeout 2400 \
     claude -s work -f /abs/brief.md >> run.log 2>&1 &
```

- **No effort knob.** `set reasoning_effort` returns `Internal error`. Only the model id.
- **`--model` and `set model` validate differently, and this surprises people.** The adapter
  advertises `["default","opus[1m]","claude-fable-5[1m]","sonnet","haiku"]`, and `set model`
  refuses anything outside that list:
  `Invalid value for config option model: claude-opus-5 (ACP -32603, adapter reported "Internal error")`.
  The global `--model` at session creation passes the id through to the harness instead, so
  `--model claude-opus-5` works fine. A genuinely unknown id still fails loudly:
  `RUNTIME: Internal error: There's an issue with the selected model (<id>)`.
- **Fable runs**: `--model 'claude-fable-5[1m]'` (quote it - the brackets are shell globs).
- **User-scope skills are excluded on purpose.** The adapter loads project and local settings
  but not user settings, so a user-level skill or slash command comes back
  `Unknown command: /polish`. Fix with `ACPX_CLAUDE_INCLUDE_USER_SETTINGS=1` - and settings
  bind at **session creation**, so an existing session must be recreated, not re-prompted.
- **Second account:** export `CLAUDE_CONFIG_DIR` and `CLAUDE_SECURESTORAGE_CONFIG_DIR` before
  `acpx`; the child inherits them. That is the whole mechanism for driving another lane.
- **The adapter is pinned far behind your CLI.** `claude-agent-acp@^0.60.0` resolves to exactly
  0.60.0, which pins `@anthropic-ai/claude-agent-sdk@0.3.215`, which bundles claude **2.1.215**
  - regardless of the version on your PATH. Consequences: the bundled binary predates the
  cross-session messaging socket, so these sessions are unreachable by peer messaging, and any
  newer harness feature is simply absent. Override with `--agent` pointed at a newer adapter
  build if you need one.
- A **session-creation stall** is a known adapter combination bug; acpx's own error text
  recommends `--approve-all` with `nonInteractivePermissions=deny`, upgrading both sides, or
  falling back to `claude exec` as a one-shot.

## Sessions

A session is keyed on **(agent command, absolute cwd, optional name)**. Because `cwd` is part
of the key, parallel executors each need their own directory - give every one its own git
worktree and their per-session model and effort settings cannot race.

| verb | behavior |
|---|---|
| `sessions ensure --name <n>` | returns the existing session or creates one - idempotent, safe before every prompt |
| `sessions new --name <n>` | soft-closes any current session and creates a fresh one (`(replaced <id>)`) |
| a bare prompt with `-s <n>` | never auto-creates; exits **4** with `Create one: ...` |
| `sessions show <n>` | `lastActivity`, `lastPrompt`, `historyEntries`, `sessionId`, `closed` |
| `sessions history <n> --limit N` | the actual turn content |
| `sessions close <n>` | releases the agent; required before `codex resume` and before `sessions export` |
| `sessions list --local` | local records including closed ones |
| `sessions prune` | deletes closed records - they persist indefinitely otherwise |

Resolution without `-s` walks from `--cwd` up to the git root. A session whose cwd you have
since deleted becomes an unreachable registry row: `sessions close` cannot target it without
its cwd, so prune it.

## Completion

**Never poll `status`.** It is a local `kill(pid,0)`-style check on the queue owner and never
touches the agent. Its states are `running`, `idle`, `dead`, `no-session`, where `dead` means
the owner is gone or the last exit was abnormal. A turn that finished seconds ago still reads
`running`, because the owner survives for its idle TTL (default **300s**, `--ttl <seconds>`,
`--ttl 0` to keep it forever). A loop waiting for `idle` will spin past real completion and
time out.

What is actually correct:

- the foreground stream's terminating `[done] end_turn`, plus the process exit code;
- `sessions show <n>` (`lastActivity`, `historyEntries`) and `sessions history <n>`;
- best of all, a **result file** the brief required the agent to write - poll for the file.

`--no-wait` returns as soon as the queue owner **acknowledges** the submission - `[queued]
<id>` in well under a second, even on an idle session. It is not delivery and not completion:
if a turn is already running, the prompt waits for the turn boundary. Background a normal
blocking prompt when you want the transcript; use `--no-wait` only to enqueue.

Supervising a backgrounded run: count tool lines and check mtime
(`rg -c '^\[tool\]' run.log`, `stat -f '%Sm' run.log`) rather than tailing the whole log, and
check liveness with `pgrep -fl 'acpx|codex-acp|claude-agent-acp'`.

`Ctrl+C` (and the `cancel` subcommand) sends ACP `session/cancel` first and force-kills only
if the agent does not stop in time.

## MCP

`--mcp-config <path>` **replaces** the project/global `mcpServers` for that invocation;
relative paths inside resolve from `--cwd`.

**The file shape is not the one every other tool uses.** acpx wants a JSON *array* of named
server objects. Handing it the standard object-keyed map throws an uncaught Node exception
with a stack trace, not a clean CLI error:

```
Error: Invalid mcpServers in /path/to/config.json: expected array
    at parseMcpServers (.../acpx/dist/cli.js:1393)
```

```jsonc
// WRONG - the Claude Code / standard shape
{ "mcpServers": { "pond": { "command": "pond", "args": ["mcp"] } } }

// RIGHT - an array, each entry carrying its own name
{ "mcpServers": [ { "name": "pond", "type": "http", "url": "http://127.0.0.1:9797/mcp" } ] }
```

Two more traps:

- **A live queue owner refuses a config change.** The owner carries the config path and a
  SHA-256 fingerprint; switching MCP config on a persistent session requires
  `sessions close` first.
- **stdio servers can fail against a strict client.** Antigravity's MCP client sends
  `server/discover` before `initialized`, which a compliant stdio server rejects
  (`expect initialized request`). Serve over HTTP instead when the consumer is agy.

`${...}` expansion inside an MCP config is evaluated in the **launcher's** environment,
because the acpx child inherits it - so a variable like a session id resolves to *your* id,
not the child's. Never let an identity flow in through the environment.

## Limits

- **Incoming ACP messages are capped at 64 MiB** since 0.15.1 (previously unlimited). Raise
  with `ACPX_MAX_ACP_MESSAGE_BYTES`, or `0` to disable. A previously-working large-payload
  script can start failing here.
- Terminal output retention is 64 KiB per call; `ACPX_TERMINAL_MAX_OUTPUT_BYTES` adjusts it.
- `--timeout <seconds>` is the wall clock for the whole prompt. Long unattended work wants
  `--timeout 5400 --ttl 0`; a wrapping shell `timeout` is a reasonable belt-and-braces.
- Output: `--format quiet` for many backgrounded executors, `text` for one you are watching,
  `json` (with `--json-strict`) when a script parses it. `--suppress-reads` keeps read-file
  contents out of the log and is worth setting on every long run.
- Pass long briefs with `-f <path>` or a path in the prompt, not inlined text: it keeps the
  content out of the shell command, where a driving harness's classifier may block on it.

## What acpx cannot do

Structural, not bugs. Do not design around them.

- **An ACP session does not wake.** An externally injected message lands in the transcript but
  starts no turn, because under ACP the turn loop belongs to the ACP client. The only inbound
  channel to an acpx session is acpx itself (`-s <name>`, optionally `--no-wait`).
- **`codex queue --thread` does not reach an acpx-driven codex session**, though it does wake a
  plain interactive codex. The ACP wrapper is not in the app-server's context.
- **acpx-spawned Claude sessions bind no IPC socket** (`entrypoint: sdk-cli`, no key file), so
  `ListAgents` / `SendMessage` cannot see or reach them.
- **acpx cannot attach to a session it did not start.** It owns the process it drives; a
  human-started headful session is out of reach.
- **Permission flags are not a sandbox.** See invariant 4: `--deny-all` does not prevent file
  writes, because the adapters do not ask. `--no-terminal` genuinely removes the terminal
  capability (an agent that calls it gets a hard error), but for filesystem safety the only
  real control is which `--cwd` you hand it.

## Failures

Exit codes, which the shipped doc does not list:

| code | meaning |
|---|---|
| 0 | success (also `cancel` with nothing to cancel) |
| 1 | agent / protocol / runtime error - the catch-all |
| 2 | usage error: bad or conflicting flags, malformed `--agent` |
| 3 | `--timeout` exceeded |
| 4 | no session found by the directory walk |
| 5 | every permission request denied or cancelled, none approved |
| 130 | interrupted (cooperative cancel first) |

**`⚠ No acpx session found (searched up to <cwd>).`** (exit 4) - invariant 2. Emitted by
`set`, by `-s` prompts, by anything that resolves a session. Fix:
`sessions ensure --name <n>` first. Confirm `--cwd` is the directory you think it is.

**`error: unknown option '--cwd'`** (exit 2) - a global flag placed after the agent
subcommand. Move it before.

**`Do not combine positional agent with --agent override`** (exit 2) - drop the positional
`claude`/`codex` when `--agent` supplies the adapter.

**`Invalid mcpServers in <path>: expected array`** (uncaught, stack trace) - the MCP config is
the standard object map; convert it to an array. See MCP.

**`Invalid value for config option model: <id> (ACP -32603, adapter reported "Internal error")`**
- `set model` with an id outside the adapter's advertised list. Use `--model` at session
creation, or pick an advertised id from `--format json ... status`.

**`RUNTIME: Internal error: There's an issue with the selected model (<id>)`** - the harness
itself rejects the id. This is a real typo or an entitlement problem, not the previous case.

**`Internal error`** from `claude set reasoning_effort` - there is no effort knob on the
claude adapter. Expected; ignore.

**`[error] RUNTIME: Authentication required`** - the agy ACP server has never been
authenticated. One run with `ACPX_AUTH_OAUTH_PERSONAL=1`.

**`RUNTIME: This client is no longer supported for Gemini Code Assist for individuals`** - you
used the built-in `gemini` agent. It is the public Gemini CLI, not Antigravity; use `--agent`
with the `.par` server.

**`already has an active writer (code -32600)`** - `codex resume` while acpx still owns the
thread. `sessions close <n>` first.

**`Missing --skill action.`** (exit 1) - `--skill` needs `show`, `list`, `install`, `export`
or `help`.

**`error: unknown option '--one-shot'`** (exit 2, usage on **stderr**) - not a verb; `exec` is
the one-shot form.

**Timeouts** (exit 3) mean the wall clock ran out, not that the agent is stuck. Check
`sessions show` for `lastActivity` before assuming failure, and remember an adapter-side
timeout surfaces as exit 1 instead.

Silent failures worth an explicit check: a content-filter kill and an MCP load failure both
end `[done] end_turn` with exit 0; a `--no-wait` prompt that "sent" may still be queued behind
a running turn; `status` reporting `running` long after completion; and a session record whose
cwd no longer exists lingering until pruned.