External agents (Agent Client Protocol)
Caret can hand a chat session to an external coding agent that speaks the Agent Client Protocol (ACP): Claude Code, Codex, Gemini CLI, OpenCode, Pi, GLM, or any program you add. The agent runs as a child process on your machine with its own login and billing. Caret is the ACP client: it owns the UI, the review bar, approvals, checkpoints and terminals; the agent owns the model loop.
Using it
- Settings → Agents. Builtin agents show Not installed until you click Install
(runs
npm install <adapter>@latestinto~/.caret/acp, never your global prefix) or install them yourself with the shown command. Connect starts the agent and runs theinitializehandshake so you can see its version, capabilities, modes and sign-in state. - Composer → agent picker (left of the mode selector). Pick Caret (built-in) or an external agent per chat. The choice is stored on the session; the agent starts on the first message of that session.
- Chat as usual. Streaming text, thoughts, tool cards, plans/todos, permission prompts, file diffs and terminal output all render in the normal chat UI. Pending changes appear in the review bar with accept/reject and are checkpointed for restore.
Confirmed launch commands (October 2026):
| Agent | Command | Install |
|---|---|---|
| Claude Code | claude-agent-acp |
npm i -g @agentclientprotocol/claude-agent-acp (uses your Claude Code login or ANTHROPIC_API_KEY) |
| Codex | codex-acp |
npm i -g @agentclientprotocol/codex-acp (bundles codex; ChatGPT login or OPENAI_API_KEY) |
| Gemini CLI | gemini --experimental-acp |
npm i -g @google/gemini-cli |
| OpenCode | opencode acp |
brew install anomalyco/tap/opencode or npm i -g opencode-ai |
| Pi | pi-acp |
npm i -g pi-acp |
| GLM (Z.AI) | glm-acp-agent |
npm i -g glm-acp-agent (ZAI_API_KEY) |
Custom agents: any executable + args + env. Caret resolves commands against PATH plus
~/.caret/acp/node_modules/.bin, Homebrew, nvm/fnm/volta/bun locations (GUI apps on macOS
inherit a minimal PATH).
Model, effort and permission mode
External agents expose their own knobs through ACP session config options
(session/new → configOptions, changed with session/set_config_option, refreshed by
config_option_update). As soon as you pick an external agent in the composer, Caret
connects it and creates the session (acp_prepare_session) so the pickers appear before the
first message:
| Agent | Options it advertises (Oct 2026) |
|---|---|
| Claude Code | Model (default / sonnet / opus / haiku / …), Effort (low → max), Mode (Manual, Accept edits, Plan, Auto, Bypass) |
| OpenCode | Model — every model of every provider you configured in OpenCode (searchable) |
| Others | whatever they publish; agents that only publish modes get a Mode picker via session/set_mode |
Your pick is sent to the agent immediately and remembered per agent in
settings.acp.sessionDefaults ({ "claude-code": { "model": "haiku", "effort": "low" } }), then
re-applied to every new session with that agent. Caret's own model / mode / thinking pickers are
hidden while an external agent drives the chat — they only affect Caret's built-in agent.
Subscriptions, sign-in and accounts
Every builtin agent runs the vendor's own CLI, so it uses the plan you already pay for and never needs an API key in
Caret: Claude Code (Claude Pro/Max or API), Codex (ChatGPT Plus/Pro/Team), Cursor (paid Cursor plan), Gemini CLI,
OpenCode, Pi, GLM and Grok Build (grok agent stdio, native ACP with image prompts, session resume and MCP
passthrough). Caret drives the official
CLI or ACP adapter and never replays subscription OAuth tokens against vendor APIs; that is what keeps use inside each
vendor's terms.
Settings → Agents shows, per agent, the sign-in state read from the vendor CLI (codex login status, claude auth status, cursor-agent status, opencode auth list) or from its credentials file when there is no status command.
Sign in runs the vendor's login (codex login, cursor-agent login, claude auth login, grok login,
opencode auth login) in a Caret terminal, because those flows are interactive (browser hand-off, device codes); press
Check afterwards. Agents that handle their own auth over ACP (authenticate) still show their ACP auth methods.
Extra accounts. Codex, Claude Code and Grok read an environment variable for an alternate config home (CODEX_HOME,
CLAUDE_CONFIG_DIR, GROK_HOME). Add account on such an agent creates a second entry (codex-work, "Codex (Work)")
with that variable set to its own directory; Sign in and Check run with the same environment, so a work plan and a
personal plan can coexist. Entries record basedOn so the builtin's login and status commands apply.
| Agent | Command | Sign in | Status | Extra accounts |
|---|---|---|---|---|
| Claude Code | claude-agent-acp |
claude auth login |
claude auth status (JSON) |
CLAUDE_CONFIG_DIR |
| Codex | codex-acp |
codex login |
codex login status |
CODEX_HOME |
| Cursor | cursor-agent acp |
cursor-agent login |
cursor-agent status |
— |
| Grok Build | grok agent stdio (native ACP) |
Sign in with Grok (ACP) or grok login |
~/.grok/auth.json |
GROK_HOME |
| Gemini CLI | gemini --experimental-acp |
gemini (then /auth) |
~/.gemini/oauth_creds.json |
— |
| OpenCode | opencode acp |
opencode auth login |
opencode auth list |
— |
Terminal safety for external agents
When an agent runs a command through Caret (ACP terminal/create), the command goes through the same shell
classifier as the built-in agent before anything spawns (acp.terminalSafety, on by default):
- blocked or denylisted → never runs; the agent gets an error telling it to ask the user instead.
- destructive → an approval card in the chat (Allow / Allow for session / Deny) unless the user already allowed that exact command for the session through an ACP permission request.
- everything else runs, and when the sandbox is available (macOS Seatbelt, Linux bubblewrap) the process is wrapped
with the workspace policy exactly like built-in shell tools; the agent log records
risk … sandboxed.
This closes the gap where an agent in its own "bypass permissions" mode could run anything on the machine: the agent's permission mode decides whether it asks, Caret's classifier decides what may run at all.
Mapping (ACP ↔ Caret)
| ACP | Caret |
|---|---|
initialize (protocol v1, client caps fs.read/write, terminal) |
AcpAgentStatus (version, capabilities, auth methods) |
session/new {cwd, mcpServers} |
one ACP session per Caret session (SessionLink), created lazily on the first prompt; your enabled MCP servers are passed when Share MCP servers is on |
session/prompt |
one Caret turn: user message + checkpoint, assistant message, MessageEnd with status from stopReason (end_turn→done, cancelled→cancelled, refusal→error) |
session/cancel |
Stop button (chat_stop) |
agent_message_chunk / agent_thought_chunk |
TextDelta / ThinkingDelta on the streaming assistant message |
tool_call / tool_call_update |
ToolCallRecord (acp_<kind>, title, status, locations, rawInput→input, rawOutput→output) with ToolStart/ToolUpdate |
diff content block |
checkpoint snapshot + PendingChange (review bar, proof panel). Folded into the fs/write_text_file change of the same file when the agent wrote through Caret; when the agent wrote the file itself, snippet-style oldText/newText (Claude Code's Edit tool) are expanded to whole-file before/after so accept/reject is always safe |
terminal content block |
command + output + exit code on the tool card |
plan |
session todos (Todos event) |
usage_update |
Usage event (contextTokens, cost) |
current_mode_update, available_commands_update, session_info_update |
AgentStatus event (modes), slash command list, session title |
session/request_permission |
ApprovalRequest through the same approval UI as the built-in agent. ACP option kinds map to Allow / Allow always / Deny / Deny always; Caret adds Allow for session, remembered per command (execute) or per tool kind |
fs/read_text_file, fs/write_text_file |
direct file IO relative to the session cwd; writes are checkpointed and tracked as pending changes |
| `terminal/create | output |
Sessions that already have history (e.g. after restarting Caret) get a short
<previous_conversation> block on their first prompt so the agent has continuity.
After a turn that changed files, the Verified Diff engine auto-runs exactly as it does for the built-in agent
(verify.autoRunAfterTurn), so the proof panel and risk tier cover external agents too.
Code
src-tauri/src/acp/protocol.rs— newline-delimited JSON-RPC 2.0 over stdio. Notifications are applied in arrival order before any later response is delivered; agent→client requests run concurrently.src-tauri/src/acp/process.rs— spawning, command resolution, stderr → per-agent log ring (acp_logs).src-tauri/src/acp/client.rs—AgentConn(one per agent per workspace): initialize, session/new, session/load, prompt, cancel, set_mode, authenticate.src-tauri/src/acp/bridge.rs— the mapping above (run_turn,Bridge: IncomingHandler).src-tauri/src/acp/registry.rs— builtin catalog, user overrides (settings.acp.agents), npm install.src-tauri/src/acp/commands.rs—acp_*Tauri commands;chat_create_session/chat_send/chat_stoproute to the bridge when the session has anagentIdother thancaret.- UI:
src/components/chat/composer/AgentPicker.tsx,src/components/settings/pages/Agents.tsx.
Tests
cargo test --lib acp::— protocol/registry unit tests plus integration tests that drive the whole bridge againsttests/fixtures/fake_acp_agent.mjs(streaming, permission allow/deny, fs write + diff dedupe, terminal round trip, plan → todos, cancel, refusal, auth-required → authenticate, set_mode).CARET_LIVE_TEST=1 cargo test --lib acp::tests::live -- --nocapture— real Claude Code round trip throughclaude-agent-acpusing your own login (verified 2026-10-26: "pong" in ~23 s, 5 modes reported).
Gaps
session/load(resuming the agent's own context after a restart) is not used yet; Caret re-creates the ACP session and sends a transcript summary instead.session/set_modeis exposed viaacp_set_modeand the status event, but the composer's mode selector still shows Caret modes; an ACP-mode picker is a follow-up.- Agent terminals (
terminal/create) run unsandboxed and are not classified; the approval card shows an unsandboxed chip. Wrapping them withsandbox::wrap+sandbox::classifyis the next step. - Token counts are not mapped from
usage_update(only context size and cost), so cards show0 in · 0 out. - Audio prompt blocks and
resource_linkfor non-file mentions are not sent. - The agent's slash commands (
available_commands_update) are captured (acp_session_info) but not yet surfaced in the slash command picker.