# acp-cli
> Headless CLI client for the Agent Client Protocol (ACP). Rust port of ACPX. Talk to coding agents (Claude, Codex, Gemini, etc.) over structured JSON-RPC instead of terminal scraping.
- Version 0.2.2
- GitHub: https://github.com/motosan-dev/acp-cli
- crates.io: https://crates.io/crates/acp-cli
## Install
```bash
cargo install acp-cli
```
## Setup
```bash
acp-cli init
```
Detects Claude Code, finds existing auth tokens (env var → config → ~/.claude.json → Keychain), writes `~/.acp-cli/config.json`.
## Quick Start
```bash
# Prompt Claude
acp-cli claude "fix the auth bug" --approve-all
# One-shot (no session persistence)
acp-cli claude exec "explain this function"
# JSON output for automation
acp-cli claude "list TODOs" --format json
# Quiet (final text only)
acp-cli claude "what is 2+2?" --format quiet
# From file
acp-cli claude -f prompt.md --approve-all
# Stdin pipe
echo "fix the bug" | acp-cli claude --approve-all
```
## Supported Agents
| Name | Command | Type |
|------|---------|------|
| claude | `npx @zed-industries/claude-agent-acp` | npm adapter |
| codex | `npx @zed-industries/codex-acp` | npm adapter |
| gemini | `gemini --acp` | native |
| copilot | `copilot --acp --stdio` | native |
| cursor | `cursor-agent acp` | native |
| goose | `goose acp` | native |
| kiro | `kiro-cli acp` | native |
| pi | `npx pi-acp` | npm adapter |
| openclaw | `openclaw acp` | native |
| opencode | `npx opencode-ai acp` | npm adapter |
| kimi | `kimi acp` | native |
| qwen | `qwen --acp` | native |
| droid | `droid exec --output-format acp` | native |
| kilocode | `npx @kilocode/cli acp` | npm adapter |
Unknown agent names are treated as raw commands.
## Commands
```bash
acp-cli init # interactive setup
acp-cli [agent] [prompt...] # persistent session prompt
acp-cli [agent] exec [prompt...] # one-shot (no persistence)
acp-cli [agent] sessions new [--name <name>] # create named session
acp-cli [agent] sessions list # list sessions
acp-cli [agent] sessions show # session details
acp-cli [agent] sessions close # soft-close session
acp-cli [agent] sessions history # conversation log
acp-cli [agent] cancel # cancel running prompt
acp-cli [agent] status # check session state
acp-cli [agent] set-mode <mode> # change agent mode
acp-cli [agent] set <key> <value> # change config option
acp-cli config show # print config
```
## Global Flags
| Flag | Default | Description |
|------|---------|-------------|
| `-s, --session <name>` | — | Named session |
| `--approve-all` | — | Auto-approve all tool calls |
| `--approve-reads` | default | Approve read-only tools only |
| `--deny-all` | — | Deny all tool calls |
| `--cwd <dir>` | `.` | Working directory |
| `--format text\|json\|quiet` | `text` | Output format |
| `--timeout <seconds>` | — | Max wait |
| `-f, --file <path>` | — | Read prompt from file (`-` for stdin) |
| `--no-wait` | — | Fire-and-forget (queue and return) |
| `--agent-override <cmd>` | — | Raw ACP command |
| `--verbose` | — | Debug to stderr |
## Permission Modes
| Mode | Behavior |
|------|----------|
| `--approve-all` | Select first allow option for every permission request |
| `--approve-reads` | Approve: Read, Glob, Grep, WebSearch, WebFetch, LSP. Deny: Edit, Write, Bash, etc. |
| `--deny-all` | Cancel all permission requests |
## Output Formats
- **text** — streaming text to stdout, tool status spinner to stderr
- **json** — NDJSON, one event per line: `{"type":"text","content":"..."}`, `{"type":"tool","name":"Read"}`, `{"type":"done"}`
- **quiet** — final text only, no status
## Session Scoping
Session key: `SHA-256(agent + "\0" + directory + "\0" + name)`.
Directory resolved by walking from `--cwd` up to git root.
Named sessions (`-s`) are independent — `None` matches only unnamed sessions.
## Queue System
First `acp-cli` process for a session becomes the **queue owner**:
- Holds the ACP agent connection
- Listens on Unix socket (`~/.acp-cli/sessions/<key>.sock`)
- Executes prompts sequentially (FIFO)
- Heartbeat every 5s, TTL 300s (configurable)
Subsequent processes connect as **queue clients** via the socket.
## Config
### Global: `~/.acp-cli/config.json`
```json
{
"default_agent": "claude",
"default_permissions": "approve_reads",
"timeout": 60,
"auth_token": "sk-ant-...",
"agents": {
"my-agent": { "command": "./custom", "args": ["--flag"] }
}
}
```
### Project: `.acp-cli.json` (in git root)
Same format. Merge order: global → project → CLI flags.
### Auth Token Resolution
Token for Claude agent resolved in order:
1. `ANTHROPIC_AUTH_TOKEN` env var
2. `~/.acp-cli/config.json` → `auth_token`
3. `~/.claude.json` → `oauthAccount.accessToken`
4. macOS Keychain (`Claude Code` service)
OAuth tokens (`sk-ant-oat01-*`) are detected but NOT injected via env var — the SDK resolves them from Keychain using the correct auth flow with `anthropic-beta: oauth-2025-04-20` header.
## Architecture
Multi-threaded tokio runtime with `spawn_blocking` + `LocalSet` bridge for `!Send` ACP futures.
```
Main thread (Send) ACP thread (!Send, LocalSet)
├── CLI parsing ├── AcpConnection (spawn_local)
├── Output rendering ├── ClientSideConnection I/O
├── Permission resolution └── BridgedAcpClient callbacks
├── Signal handling
└── Queue IPC server Channel bridge (mpsc + oneshot)
```
## Exit Codes
| Code | Meaning |
|------|---------|
| 0 | Success |
| 1 | Agent/runtime error |
| 2 | CLI usage error |
| 3 | Timeout |
| 4 | No session found |
| 5 | Permission denied |
| 130 | Interrupted (SIGINT) |
## Release
```bash
# 1. Bump version in Cargo.toml
# 2. Update CHANGELOG.md
# 3. Commit
git commit -m "chore: release v0.2.2"
# 4. Tag + push (triggers publish.yml → crates.io)
git tag -a v0.2.2 -m "v0.2.2 — summary"
git push origin main v0.2.2
```