The Agent Client Protocol (ACP) is an open standard that lets a code editor run a coding agent as a subprocess and talk to it in JSON-RPC 2.0 over standard input and output. Zed introduced it on 27 August 2025. To secure it, treat the agent as code running with your privileges and limit its files, commands, environment and network.
ACP gives the editor a clean way to show what an agent is doing and to ask you before it acts. It does not, by itself, stop an agent from doing anything. The agent is a program on your machine, started by your editor, running as you.
This post goes one level deeper than our protocol explainers for the Model Context Protocol and A2A versus MCP on the wire. Where those cover how an agent reaches tools and other agents, this one covers the hop between you, in your editor, and the agent itself. If you are writing policy for a team rather than wiring up a protocol, read AI coding agent governance next; if you are sandboxing agents that drive a whole screen, see computer use agents explained.
Protocol details come from the ACP specification (read on 24 October 2026) and the JSON-RPC 2.0 specification, editor behaviour from Zed's documentation, Claude Code facts only from Anthropic's documentation and Codex facts only from OpenAI's. Version 1 is stable and version 2 has been a draft since 20 July 2026, so we say which version each detail belongs to.
Key takeaways
- ACP does for coding agents what LSP did for language servers: any ACP agent works in any ACP editor.
- Zed started it, announcing it on 27 August 2025 with Gemini CLI as the first reference agent. It is Apache 2.0 licensed.
- The wire format is JSON-RPC 2.0, one message per line on stdio. The editor sends
initialize,session/newandsession/prompt; the agent streamssession/updatenotifications and calls back for files, terminals and permission. - Permission is cooperative. The agent may call
session/request_permission; an agent using its own built-in tools never has to ask the editor. - ACP carries MCP. The editor passes MCP server configurations into each session, so every MCP risk rides along.
- The v2 draft removes the client file and terminal methods in favour of MCP servers. Gate v2 behind flags until it stabilises.
- Real enforcement lives elsewhere: the agent's own rules, an OS sandbox or container, a scrubbed environment and a network allow-list.
What is the Agent Client Protocol, and what problem does it solve?
ACP's introduction page says it standardises communication between code editors and coding agents. The problem it describes is an integration matrix. Without a shared protocol, every editor has to build a custom integration for every agent it wants to support, and every agent has to implement each editor's API to reach users. That produces integration overhead, agents that only work in some editors, and developers who pick an agent and then have to accept whatever interface it ships with.
The project compares this to the Language Server Protocol. Write one ACP agent and it works in Zed, JetBrains IDEs, Neovim and Emacs plugins and the other clients on the ACP site; write one ACP client and you can host any agent in the registry. The protocol "assumes that the user is primarily in their editor": the editor is home, and the agent is a guest that renders inside it.
Who started ACP, and when?
Zed Industries started it. Nathan Sobo's post on Zed's blog, dated 27 August 2025, announced that Zed users could now run third party agents directly in the editor, that Zed had created the Agent Client Protocol to make that possible, and that Zed had partnered with Google to integrate Gemini CLI as the initial reference implementation. An update in the same post says Zed added beta support for Claude Code on 3 September 2025.
The post explains the design. Zed was already running Gemini CLI in its built in terminal and needed something more structured than ANSI escape codes, so it defined "a minimal set of JSON-RPC endpoints" to relay requests and render responses. It also says that with third party agents nothing touches Zed's servers. Your code still goes wherever the agent sends it, usually a model provider.
The project now has its own governance: contributors, maintainers, core maintainers and two lead core maintainers form an ACP steering group, and changes go through a Request for Dialog (RFD) process.
Timeline
| Date | Event | Source |
|---|---|---|
| 23 June 2025 | agentclientprotocol/agent-client-protocol repository created on GitHub, Apache 2.0 license | GitHub repository metadata |
| 27 August 2025 | Zed announces ACP, with Gemini CLI as the first reference agent | Zed blog |
| 3 September 2025 | Zed adds beta support for Claude Code over ACP | Zed blog, update note |
| 9 March 2026 | ACP Registry released and stabilised; session list stabilised | ACP announcement and updates page |
| 22 April 2026 | Transports Working Group announced to stabilise new transports | ACP announcement |
| 25 June 2026 | Rust and TypeScript SDKs reach 1.0.0 | ACP announcement |
| 20 July 2026 | ACP v2 published as a draft | ACP announcement |
| July 2026 | Elicitation (structured user input) stabilised | ACP updates page and announcement |
| 17 September 2026 | Tool call names stabilised | ACP announcement |
| 16 October 2026 | Latest tagged schema release, schema-v1.26.0 | GitHub releases |
The SDK packages named in the 1.0 announcement are the Rust crate agent-client-protocol and the npm package @agentclientprotocol/sdk. The documentation also lists libraries for Kotlin, Java and Python, and a test compatibility kit for checking an agent against the protocol.
How does ACP work on the wire?
ACP is built on JSON-RPC 2.0, a specification dated 2010 that describes itself as "a stateless, light-weight remote procedure call (RPC) protocol". JSON-RPC gives ACP two message types. A request has an id and expects a response carrying either a result or an error with a numeric code and a message. A notification is a request without an id; the JSON-RPC spec says the receiver must not reply to it. ACP uses both heavily, and in both directions: the editor calls the agent, and the agent calls the editor.
The stdio transport
The transport page defines stdio as the standard transport and says agents and clients should support it whenever possible:
- The client launches the agent as a subprocess.
- The agent reads JSON-RPC messages from standard input and writes them to standard output.
- Messages are delimited by newlines and must not contain embedded newlines.
- The agent may write UTF-8 log text to standard error, which the client may capture, forward or ignore.
- Neither side may write anything to the other's stream that is not a valid ACP message.
Streamable HTTP is listed as a draft proposal in progress, and the introduction notes that full support for remote agents is still a work in progress. Custom transports are allowed if they preserve the JSON-RPC message format and lifecycle. In practice, almost every ACP deployment today is a local subprocess, which matters for security: the agent inherits your user account, your environment variables and your network.
ACP also fixes some conventions. All file paths in the protocol must be absolute. Line numbers are 1-based. Object keys are camelCase and discriminator values are snake_case.
The message flow
The overview page lays out a typical connection in three phases.
| Phase | Direction | Method | Type |
|---|---|---|---|
| Initialisation | Client to agent | initialize | Request |
| Initialisation | Client to agent | authenticate (if the agent requires it) | Request |
| Session setup | Client to agent | session/new, or session/load / session/resume | Request |
| Prompt turn | Client to agent | session/prompt | Request |
| Prompt turn | Agent to client | session/update | Notification |
| Prompt turn | Agent to client | fs/read_text_file, fs/write_text_file, terminal/* | Request |
| Prompt turn | Agent to client | session/request_permission | Request |
| Prompt turn | Client to agent | session/cancel | Notification |
| End of turn | Agent to client | Response to session/prompt with a stopReason | Response |
Initialisation and capability negotiation
Every connection starts with initialize. The client sends the latest protocol version it supports (a single integer, currently 1 for the stable protocol), the capabilities it offers, and optionally its name and version:
{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":1,"clientCapabilities":{"fs":{"readTextFile":true,"writeTextFile":true},"terminal":true},"clientInfo":{"name":"my-client","title":"My Client","version":"1.0.0"}}}
The agent answers with the version it chose, its own capabilities and any authentication methods:
{"jsonrpc":"2.0","id":0,"result":{"protocolVersion":1,"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":true,"audio":true,"embeddedContext":true},"mcpCapabilities":{"http":true,"sse":true}},"agentInfo":{"name":"my-agent","title":"My Agent","version":"1.0.0"},"authMethods":[]}}
Capabilities omitted from initialize must be treated as unsupported, and if the client does not offer readTextFile or terminal, the agent must not call those methods. The capability list is the first place you can say no, but saying no to the editor's file methods does not stop the agent from opening files itself.
Sessions
A session is one conversation thread with its own context and history. One connection can hold several sessions at once. The client creates one with session/new, passing a working directory and a list of MCP servers:
{"jsonrpc":"2.0","id":1,"method":"session/new","params":{"cwd":"/home/user/project","mcpServers":[{"name":"filesystem","command":"/path/to/mcp-server","args":["--stdio"],"env":[]}]}}
The agent replies with a sessionId. The cwd must be absolute and forms the session's root set, which the spec says "SHOULD serve as a boundary for tool operations on the file system". SHOULD, not MUST. The stabilised additionalDirectories capability adds more roots.
If the agent advertises loadSession, session/load reopens an old session and the agent must replay the whole conversation as session/update notifications. session/resume reconnects without replay, and session/close cancels work and frees the session. Replay means the agent stores the conversation, including file contents and command output, somewhere.
A prompt turn, message by message
The core loop is the prompt turn. The client sends the user's message as content blocks. Every agent must accept plain text and resource links; images, audio and embedded resources are optional and advertised in promptCapabilities.
{"jsonrpc":"2.0","id":2,"method":"session/prompt","params":{"sessionId":"sess_abc123def456","prompt":[{"type":"text","text":"Why does the config test fail?"},{"type":"resource_link","uri":"file:///home/user/project/test/config.test.js","name":"config.test.js"}]}}
The agent passes that to its model and then streams progress back as notifications. A plan, a chunk of reply text, a new tool call and an update to that tool call all arrive as session/update with a different sessionUpdate discriminator:
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"sess_abc123def456","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"Reading the test first."}}}}
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"sess_abc123def456","update":{"sessionUpdate":"tool_call","toolCallId":"call_001","title":"Reading configuration file","kind":"read","status":"pending"}}}
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"sess_abc123def456","update":{"sessionUpdate":"tool_call_update","toolCallId":"call_001","status":"completed"}}}
When the model stops asking for tools, the agent answers the original session/prompt request with a stop reason:
{"jsonrpc":"2.0","id":2,"result":{"stopReason":"end_turn"}}
The five v1 stop reasons are end_turn (the model finished without requesting more tools), max_tokens, max_turn_requests (too many model requests in one turn), refusal (the agent refuses to continue) and cancelled (the client cancelled).
Cancellation
The client can send session/cancel, a notification, at any time. It must answer every pending permission request with the cancelled outcome; the agent should stop model requests and tool calls as soon as possible and must end the turn with the cancelled stop reason rather than an error. Cancel is a request, not a kill switch. If you need a guarantee, the client must be ready to terminate the subprocess, the last step the stdio transport page shows.
Extending the protocol
ACP allows custom data in _meta fields, custom methods whose names start with an underscore, and custom capabilities. Features are prototyped this way before they become RFDs, which also means an editor and agent pair can exchange behaviour the specification does not describe. Read the actual traffic.
What does the client expose to the agent, and how are tool calls reported?
The interesting direction in ACP is agent to client. An LSP server only answers questions. An ACP agent asks the editor to do things.
File system methods
With fs.readTextFile, the agent can call fs/read_text_file with a session ID, an absolute path, and optionally a starting line and a line limit. The client returns the content, including unsaved changes in the editor. With fs.writeTextFile, the agent can call fs/write_text_file with a path and the full new content, and the client must create the file if it does not exist. The spec gives the reason these methods exist: they let agents see unsaved editor state and let clients track the changes an agent makes.
Every edit routed this way passes through the editor, which can show, diff and undo it. But the method definitions include no root check, no denied paths and no size limit. A client has to enforce its own boundary, for example refusing paths outside the session's root set and writes to its own settings files.
Terminal methods
With terminal: true, the agent gets five methods. terminal/create starts a command with a command string, an args array, an optional env list of name and value pairs, an absolute cwd and an outputByteLimit. The client returns a terminal ID immediately and the command runs in the background. terminal/output returns what has been captured so far, whether it was truncated, and the exit status if the command has finished. terminal/wait_for_exit blocks until it exits. terminal/kill stops the command but keeps the terminal readable. terminal/release kills it if still running and frees it, and the agent must call it when done.
Terminals can be embedded in a tool call so the user sees live output. Two security details stand out. The agent chooses the environment variables it passes, so a client should decide which variables are allowed rather than merging the agent's list into the user's full environment. And the output the client returns goes straight back into the model's context, so anything a command prints, including a secret, becomes part of the conversation.
Permission requests
session/request_permission is the only client method every client must support. The agent sends the tool call it wants to run and a list of options, each with an ID, a label and a kind of allow_once, allow_always, reject_once or reject_always. The client shows the choice and answers with selected plus the chosen option ID, or cancelled if the turn was cancelled.
{"jsonrpc":"2.0","id":5,"method":"session/request_permission","params":{"sessionId":"sess_abc123def456","toolCall":{"toolCallId":"call_001"},"options":[{"optionId":"allow-once","name":"Allow once","kind":"allow_once"},{"optionId":"reject-once","name":"Reject","kind":"reject_once"}]}}
{"jsonrpc":"2.0","id":5,"result":{"outcome":{"outcome":"selected","optionId":"allow-once"}}}
Two rules govern this method. The spec says the agent MAY request permission before executing a tool call, so the agent decides when to ask. It also says clients MAY automatically allow or reject requests according to user settings, so the client decides whether a human sees the question. Neither side is obliged to involve you.
Elicitation
Stabilised in July 2026, elicitation/create lets the agent ask the user for structured input through a form or send them to a URL. A URL elicitation is an agent asking your editor to send you somewhere, so show the full destination and treat it as an untrusted link.
How tool calls are reported
Tool calls are how a client learns what the agent is doing. When the model asks for a tool, the agent should send a tool_call update with:
toolCallId, unique within the session;title, a human readable description;kind, one ofread,edit,delete,move,search,execute,think,fetch,switch_modeorother;status, which moves throughpending,in_progress,completedorfailed;content, which can be ordinary content blocks, a diff with a path plus old and new text, or an embedded terminal;locations, file paths and lines so the editor can follow the agent around the code;rawInputandrawOutput, the raw tool arguments and results;name, an optional programmatic tool name, stabilised on 17 September 2026.
The spec says tool names "do not advertise a capability or grant authorization". The same goes for kind and title, which exist for icons and layout. They are labels written by the agent, so a client that auto-approves anything labelled read trusts the agent to label honestly, exactly the trust a manipulated agent would abuse.
Plans, modes and configuration options
Agents can also send plans, advertise slash commands and offer session modes. The spec's example modes are Ask, Architect and Code, and it says modes often affect "whether they request permission before running". A mode is a promise the agent makes about itself: switching to "Ask" in the editor is only as strong as the agent's implementation of it.
How does ACP relate to MCP, A2A and AG-UI?
These four protocols are often mentioned together because each covers one hop in an agent system. They are complements, not competitors.
ACP and MCP
The relationship is the closest. ACP's architecture page lists "MCP-friendly" as its first design principle: ACP is built on JSON-RPC and reuses MCP's JSON types where possible so implementers do not need yet another representation of common data. Content blocks, for example, follow MCP's shapes.
The practical link is the mcpServers field on session/new, session/load and session/resume. The editor usually holds the user's MCP configuration. When it starts a session, it passes those server definitions to the agent, and the agent connects to them directly. All agents must support MCP's stdio transport; HTTP and SSE are optional capabilities (the ACP initialisation page notes that SSE has been deprecated by MCP). The editor can even expose its own tools this way by handing the agent a small MCP proxy that tunnels calls back to the editor.
So when you approve an ACP session, you are also approving every MCP server in that list to be launched or contacted by the agent, with the agent's privileges. The MCP specification's security best practices page has a section on exactly this, "Local MCP Server Compromise", which describes local servers as binaries downloaded and executed on the same machine as the client, lists malicious startup commands and malicious payloads among the risks, and says a client that supports one click local server configuration "MUST implement proper consent mechanisms prior to executing commands".
The ACP v2 draft deepens the link. It removes the client file and terminal methods and says clients that want to expose file access, unsaved editor state or command execution should do so by providing an MCP server to the session.
ACP and A2A
A2A connects one agent to another, typically across organisations. Its specification, at version 1.0.0, defines protocol bindings for JSON-RPC, gRPC and HTTP plus JSON/REST, and agents advertise themselves with an Agent Card that can be signed. ACP is the opposite case: a human, an editor and one agent on one machine.
ACP and AG-UI
AG-UI describes itself as an event-based protocol "that standardizes how AI agents connect to user-facing applications". Both it and ACP connect an agent to a human's interface, but AG-UI streams events from a server-side agent to a web or app frontend, while ACP runs the agent as a local subprocess that calls back into the editor for files, terminals and approval.
Comparison table
| ACP | MCP | A2A | AG-UI | |
|---|---|---|---|---|
| Who talks to whom | Editor (client) and coding agent | Agent host (client) and tool or data server | Agent and agent | Agent backend and user-facing app |
| Message format | JSON-RPC 2.0 | JSON-RPC 2.0 | JSON-RPC 2.0, gRPC or HTTP+JSON bindings | Typed JSON events |
| Main transport | stdio subprocess; Streamable HTTP in draft | stdio or Streamable HTTP | HTTPS | HTTP with server-sent events; WebSockets |
| Who calls whom | Both directions | Mostly client to server; server can request sampling and elicitation | Client agent to remote agent | Agent streams to frontend; frontend sends runs and tool results |
| Approval primitive | session/request_permission (agent may ask) | Tool annotations and host consent; elicitation | Task states such as TASK_STATE_INPUT_REQUIRED | Interrupts and frontend tools |
| Main risks | Agent binary with user privileges; over-broad files, terminal and environment; forwarded MCP servers; prompt injection from repo content | Malicious or compromised local servers; token passthrough; confused deputy; SSRF; session hijacking | Spoofed or over-trusted remote agents; Agent Card trust; data leaving the organisation | Untrusted agent output rendered in the UI; state injection; spoofed tool calls |
ACP v1 compared with the v2 draft
Because v2 changes the security surface, it is worth seeing the differences side by side. The migration guide is explicit that v2 as a whole is still labelled draft and that implementers should gate it behind version negotiation and feature flags.
| Area | ACP v1 (stable) | ACP v2 (draft, July 2026) |
|---|---|---|
| Client file access | fs/read_text_file, fs/write_text_file | Removed; expose through an MCP server passed to the session |
| Client terminals | terminal/create, output, wait_for_exit, kill, release | Removed; terminal content only displays agent-supplied output |
| Prompt lifecycle | The session/prompt response ends the turn | The response acknowledges insertion of the user message; updates can arrive at any time |
| Permission requests | Bare toolCall plus options | Required title, optional description, extensible subject |
| Session reopen | session/load replays; session/resume does not | session/load gone; session/resume covers both |
| MCP config | stdio entries have no type; SSE allowed | Every entry has a type; SSE removed |
| Diffs | Old text and new text | Structured file changes (add, delete, modify, move, copy, binary) and optional git patch |
| Unknown enum values | Can break older peers | Accepted with an underscore prefix |
The migration guide says the client execution surface "was inconsistently implemented outside of a few IDEs". In v2, an editor sees file and command activity through MCP tools it hosts or through what the agent chooses to report.
How are permissions handled, and where does trust sit?
The three parties
Every local ACP session has three parties. The editor launches the agent, chooses its command line and environment, decides which capabilities to offer and which MCP servers to pass, and answers permission requests by asking you or by applying stored rules. The agent process holds the model credentials, decides which tools exist and when to call back into the editor, and runs as your operating system user. The model provider receives the context the agent assembles (file contents, command output, MCP results) and returns text and tool requests; it never talks to the editor directly.
ACP's architecture page is candid about this. Its third design principle, "Trusted", says the protocol works when you use an editor to talk to a model you trust: you keep controls over tool calls, but the editor gives the agent access to local files and MCP servers. ACP is designed for a cooperative agent. It is not a sandbox protocol and does not claim to be one.
What the editor can enforce, and what it cannot
The editor can enforce three things with certainty: which client methods it implements, what it does when they are called (for example refusing paths outside the root set), and how it launches the subprocess. Everything else depends on the agent. Zed's External Agents documentation shows the boundary in a table: model configuration, authentication and API keys are usually owned by the external agent, Zed's own profiles and skills do not apply, and for tool permissions "native tool permissions depend on the agent". If the agent edits files and runs shell commands with its own code, the editor learns about it only through the tool_call notifications the agent chooses to send.
Native permissions in the agents themselves
Because the agent holds the real power, its own permission system matters more than the editor's. Two examples, each from the vendor's own documentation:
- Claude Code. Anthropic's permissions page says rules are evaluated "deny, then ask, then allow", first match wins. A Read deny rule such as
Read(./.env)covers Claude's file tools and the file commands Claude Code recognises in Bash, but not scripts that open files themselves. For enforcement across every process the docs point to the sandbox, which uses Seatbelt on macOS and bubblewrap with socat on Linux and WSL2, and sends network traffic through a proxy whose allowed domains start empty. Anthropic's security page adds that the Manual mode working directory boundary is a permission prompt, "so a Bash command you approve can still write anywhere your user account can". - Codex. OpenAI's agent approvals and security page says the agent runs with network access off by default and, locally, inside an OS-enforced sandbox plus an approval policy.
sandbox_mode(for exampleread-onlyorworkspace-write) sets what is technically possible;approval_policy(for exampleon-request) sets when Codex must ask. Inworkspace-writemode the repository's.gitdirectory stays read-only.
Which native settings apply behind an ACP adapter depends on the adapter. Zed's documentation says the Claude Agent may use Claude Code authentication and native config, and Codex may use its native config. Check your adapter's documentation, then verify with a test, as in the walkthrough below.
The approval pattern that holds up
The pattern that works is layered. ACP permission prompts are the user experience layer, good for showing intent and catching mistakes. The agent's native deny rules are the policy layer. An operating system sandbox or container is the enforcement layer, so a mistaken approval or a mislabelled tool call still cannot reach your SSH keys or the open internet. OWASP's Top 10 for LLM Applications calls the failure this prevents "Excessive Agency" and splits it into excessive functionality, permissions and autonomy.
What are the security risks?
1. An agent binary running with your privileges
With the stdio transport the editor starts the agent as a child process. That process can do anything your user account can: read your home directory, use your SSH agent socket, read cloud credential files, open network connections, and spawn more processes. The ACP spec does not change that, and the agent does not need to call a single ACP method to do it.
The agent also has a supply chain. Many agents and adapters ship as npm packages, Python packages or binaries, and the editor runs whatever the configured command resolves to. A package runner command can fetch a new release on every launch unless pinned. The ACP Registry is curated and, per its page, lists only agents that support authentication, which is a quality bar, not a code review. Pin versions, install agents like build dependencies, and keep a list of approved agents.
2. Prompt injection from repository content
An agent's context is built from whatever it reads: source files, READMEs, issue text, test fixtures, dependency code, web pages and MCP results. OWASP calls instructions arriving this way indirect prompt injection, where the model takes input from external sources such as websites or files. A comment in a vendored dependency or a crafted pull request description can carry instructions the model may follow.
The damage is bounded by what the agent can do. With unrestricted commands and network, an injected instruction becomes an action; with an allow-list and limited egress, it stalls at a prompt or fails. Among OWASP's mitigations for LLM01 is to "enforce privilege control and least privilege access", the part an ACP deployment controls. Note too that fs/read_text_file returns unsaved buffers, and session/load can bring old injected text back into a new session.
3. Over-broad file and terminal access
The session root set is only a SHOULD-level boundary, and the file methods take any absolute path. A client without a root check will return ~/.ssh/config; one without write restrictions lets the agent rewrite the editor's own agent_servers entry, which decides what runs next time, or a project's CI configuration. terminal/create runs any command in any absolute cwd. Zed's terminal rules show what a client can add (regex allow, deny and confirm lists, with chained commands parsed), and its only built-in rules block recursive deletion of root, home, current and parent directories: "There are no other built-in rules."
4. Secrets in context and in the environment
Secrets reach an ACP agent by four routes.
- Environment inheritance. A subprocess inherits its parent's environment unless scrubbed. Anthropic's sandboxing page notes sandboxed commands inherit Claude Code's environment, "including any secrets in its environment".
- Credentials passed on purpose. Zed documents that, if you configured a key for its Google AI provider, it passes that key to Gemini CLI as
GEMINI_API_KEY. Reasonable, but the agent now holds a live key. - File reads.
.envfiles and fixtures with real values sit in the project. - Command output. Whatever
terminal/outputreturns enters the model context and, withsession/load, stored history.
Once a secret is in the context it goes to the model provider with the next request, so deny secret file reads, scrub the agent's environment and keep long-lived credentials out of the project.
5. Forwarded MCP servers
Every mcpServers entry is a command the agent launches or an endpoint it calls. Forwarding all of them into every session gives each agent each server's reach, and some agents also read native MCP configuration the editor never shows. Pass only what a project needs.
6. Protocol integrity and logging
ACP over stdio has no message authentication; it relies on the process boundary. Debug logs of the traffic contain everything the agent read and wrote, so store them like secrets. Remote transports are still a draft, so do not expose a stdio agent on a network socket with a homemade bridge.
7. Approval fatigue and sticky approvals
An "always allow" granted for one harmless command can become a standing grant for a pattern. Review stored approvals and prefer narrow, anchored patterns.
Step by step: connect an agent, inspect the messages, restrict permissions
This walkthrough uses Zed as the client because its ACP documentation is the most complete, then shows a tiny standalone client you can use with any ACP agent. It assumes macOS or Linux, a project directory you are willing to experiment in, and an ACP agent you have already installed and authenticated. Run it in a throwaway clone first.
Step 1: install an agent the documented way
In Zed, open the registry with the zed: acp registry command, or open Agent Settings with agent: open settings, go to External Agents, click Add Agent and choose Install from Registry. The agent then appears in the new-thread menu. For an agent that is not in the registry, choose Add Custom Agent, which opens your settings file with an entry like this:
{
"agent_servers": {
"my-agent": {
"type": "custom",
"command": "/usr/local/bin/my-agent",
"args": ["--acp"],
"env": {}
}
}
}
Prefer an absolute path to a pinned binary over a package runner command that resolves the latest version on each launch. Write down the exact version you installed.
Step 2: read the traffic with the built-in log
Start a thread with the agent, ask a harmless question such as "list the files in this project", then run dev: open acp logs from the Command Palette. Zed's documentation describes this as the way to inspect messages between Zed and an external agent. Look for four things:
- The
initializerequest: whichclientCapabilitiesthe editor offered (fs,terminal,elicitation). - The
initializeresponse: the agent'sprotocolVersion,agentCapabilities,mcpCapabilitiesandauthMethods. - The
session/newrequest: thecwdand the fullmcpServerslist the editor forwarded. - During the prompt:
tool_callupdates and whether any arrive without a matchingsession/request_permission. Tool calls of kindexecuteoreditthat never trigger a permission request tell you the agent is acting on its own policy.
Step 3: capture raw stdio with a logging wrapper
For a full record, put a small wrapper between the editor and the agent. It copies each direction of the stdio stream to a log file and passes everything through unchanged. It also starts the agent with a cleaned environment.
#!/usr/bin/env bash
# acp-tap.sh: log ACP JSON-RPC traffic and start the agent with a minimal environment.
# The log contains file contents and command output. Keep it private and delete it after use.
set -euo pipefail
LOG_DIR="${HOME}/.acp-tap"
mkdir -p "$LOG_DIR" && chmod 700 "$LOG_DIR"
STAMP="$(date +%Y%m%d-%H%M%S)"
AGENT="/usr/local/bin/my-agent" # absolute path to the pinned agent binary
exec env -i \
HOME="$HOME" PATH="/usr/local/bin:/usr/bin:/bin" LANG="${LANG:-C.UTF-8}" TERM=dumb \
bash -c 'tee -a "$1/in-$2.jsonl" | "$3" --acp | tee -a "$1/out-$2.jsonl"' \
_ "$LOG_DIR" "$STAMP" "$AGENT"
Point the Zed entry at the wrapper ("command": "/path/to/acp-tap.sh", "args": []). Agent logs on standard error still reach the editor. in-*.jsonl holds what the editor sent; out-*.jsonl holds what the agent sent. Because ACP messages are one JSON object per line, standard tools read them directly:
# Which methods were called, in order, in each direction
jq -r '.method // "response:\(.id)"' ~/.acp-tap/in-*.jsonl | head -50
jq -r '.method // "response:\(.id)"' ~/.acp-tap/out-*.jsonl | head -50
# Every tool call the agent reported, with kind and title
jq -c 'select(.params.update.sessionUpdate? == "tool_call") | .params.update | {kind, title, status}' ~/.acp-tap/out-*.jsonl
# Every permission request and what the editor answered
jq -c 'select(.method == "session/request_permission") | {id, tool: .params.toolCall.toolCallId}' ~/.acp-tap/out-*.jsonl
jq -c 'select(.result.outcome?) | {id, outcome: .result.outcome}' ~/.acp-tap/in-*.jsonl
If the agent needs a model key, add only that variable to the env -i line, or have the agent read it from its own credential store. The point of env -i is that nothing else from your shell, such as cloud tokens, follows the agent in.
Step 4: drive an agent from a minimal client that offers nothing
To see what an agent does when the editor offers no file or terminal methods and refuses every permission request, use a short client. This one uses only the Python standard library.
#!/usr/bin/env python3
"""acp_probe.py: start an ACP agent, offer no fs or terminal capability,
reject every permission request, print every message. For inspection only."""
import json, subprocess, sys, threading, os
agent_cmd = sys.argv[1:] # e.g. /usr/local/bin/my-agent --acp
project = os.path.abspath(".")
proc = subprocess.Popen(agent_cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
text=True, bufsize=1, env={"PATH": "/usr/bin:/bin", "HOME": os.environ["HOME"]})
def send(msg):
line = json.dumps(msg)
print(">>", line[:300])
proc.stdin.write(line + "\n"); proc.stdin.flush()
def reader():
for line in proc.stdout:
msg = json.loads(line)
print("<<", line.strip()[:300])
# The agent is asking us something: refuse it.
if "method" in msg and "id" in msg:
if msg["method"] == "session/request_permission":
reject = next((o["optionId"] for o in msg["params"]["options"]
if o["kind"].startswith("reject")), None)
outcome = {"outcome": "selected", "optionId": reject} if reject else {"outcome": "cancelled"}
send({"jsonrpc": "2.0", "id": msg["id"], "result": {"outcome": outcome}})
else:
send({"jsonrpc": "2.0", "id": msg["id"],
"error": {"code": -32601, "message": "Method not found"}})
threading.Thread(target=reader, daemon=True).start()
send({"jsonrpc": "2.0", "id": 0, "method": "initialize",
"params": {"protocolVersion": 1, "clientCapabilities": {},
"clientInfo": {"name": "acp-probe", "version": "0.1"}}})
input("Press Enter after the initialize response arrives...\n")
send({"jsonrpc": "2.0", "id": 1, "method": "session/new",
"params": {"cwd": project, "mcpServers": []}})
sid = input("Paste the sessionId: ").strip()
send({"jsonrpc": "2.0", "id": 2, "method": "session/prompt",
"params": {"sessionId": sid, "prompt": [{"type": "text",
"text": "List the files in the project root. Do not modify anything."}]}})
input("Press Enter to cancel and quit...\n")
send({"jsonrpc": "2.0", "method": "session/cancel", "params": {"sessionId": sid}})
proc.stdin.close(); proc.terminate()
Run it from a scratch project, passing the agent's command line as arguments. Some agents require authenticate before session/new; if so, run the agent's own login first. If tool_call updates of kind read or execute complete while your client offered no capabilities and rejected every permission request, the agent is using native tools, and only native rules or a sandbox will constrain it.
Step 5: restrict what the editor allows
For Zed's own agent tools, and for ACP forwarding where it applies, set tool permissions in your settings. This example confirms by default, allows a few build commands, always confirms network fetches and blocks reads of obvious secret paths through the file tools:
{
"agent": {
"tool_permissions": {
"default": "confirm",
"tools": {
"terminal": {
"default": "confirm",
"always_allow": [
{ "pattern": "^npm\\s+(test|run\\s+lint)$" },
{ "pattern": "^cargo\\s+(build|test|check)$" }
],
"always_deny": [
{ "pattern": "\\b(curl|wget|nc|ssh|scp)\\b" },
{ "pattern": "\\.ssh/|\\.aws/|\\.env\\b" }
]
},
"edit_file": {
"always_deny": [{ "pattern": "(^|/)\\.(env|git/|github/workflows/)" }]
},
"fetch": { "default": "confirm" }
}
}
}
}
Zed documents the precedence as built-in rules, then always_deny, then always_confirm, then always_allow, then the tool default, then the global default, and recommends testing patterns with its "Test Your Rules" checker. Remember the boundary from Zed's own table: for external agents, these rules cover what flows through Zed, while native tool permissions depend on the agent.
Step 6: restrict what the agent itself allows
Configure the agent's native policy. If your agent is Claude Code or is built on it, Anthropic documents project settings in .claude/settings.json with deny, ask and allow rules. A starting point:
{
"permissions": {
"deny": [
"Read(./.env)",
"Read(./.env.*)",
"Read(./secrets/**)",
"Bash(curl *)",
"Bash(wget *)"
],
"ask": [
"Bash(git push *)"
],
"disableBypassPermissionsMode": "disable"
}
}
Anthropic's documentation explains the limits of such rules: a Bash deny rule matches the command as written, not the same program invoked by path or inside a sub-shell, and Read deny rules do not cover scripts that open files themselves. For that, it recommends turning on the sandbox with /sandbox and configuring network allowed domains.
If your agent is Codex, OpenAI documents the equivalent split between a sandbox mode and an approval policy. A conservative interactive setup in its config.toml is:
sandbox_mode = "read-only"
approval_policy = "on-request"
Move to workspace-write when you want edits, and leave sandbox_workspace_write.network_access off unless a task needs it.
For any other agent, find its equivalent controls in its own documentation. If it has none, the next step is not optional.
Step 7: contain the process
The strongest control is the one the agent cannot opt out of. Run the agent inside a container or VM that mounts only the project, with no host credentials and outbound traffic limited to the model provider's API. A minimal Docker invocation, used as the command in a wrapper script, looks like this:
#!/usr/bin/env bash
# acp-contained.sh: run an ACP agent in a container that sees only the current project.
set -euo pipefail
exec docker run --rm -i \
--network acp-egress \
--read-only --tmpfs /tmp \
--cap-drop ALL --security-opt no-new-privileges \
-v "$PWD":/workspace -w /workspace \
-e MODEL_API_KEY \
registry.example.internal/agents/my-agent@sha256:REPLACE_WITH_PINNED_DIGEST --acp
Here acp-egress stands for a Docker network you have attached to an egress proxy that allows only your model provider's API host; build that once and reuse it. The -i flag keeps standard input open, which the stdio transport needs. Pin the image by digest. One caveat matters for ACP: the session cwd the editor sends is a host path, while the agent inside the container sees /workspace. Either mount the project at the same absolute path inside the container, or use a dev container setup in which the editor and agent agree on paths.
Step 8: verify with a test
Repeat Step 2 or Step 4 with your restrictions in place and ask the agent, in plain words, to show the first line of a file outside the project and to fetch a public web page. Both should fail or stop at a prompt you can see. Then check the logs from Step 3 for any tool_call that completed without a permission request. Delete the logs when you are done.
Which control to use for which risk
| You want to stop | Editor capability off | Editor permission rules | Agent native rules | OS sandbox or container |
|---|---|---|---|---|
| Agent reading files outside the project | Partial (only fs methods) | Partial | Partial (scripts can bypass) | Yes |
| Agent running arbitrary commands | Partial (only terminal methods) | Partial | Yes, with gaps for indirect calls | Yes |
| Network exfiltration | No | Partial (command patterns) | Partial | Yes, with an egress allow-list |
| Secrets in environment | No | No | Partial (scrub settings where offered) | Yes, if the container gets no host env |
| Over-broad MCP servers | Yes (do not forward them) | Per-tool rules | Native MCP config | Network policy |
| A malicious agent build | No | No | No | Limits the damage |
Common mistakes
- Treating the permission prompt as a security boundary. ACP lets the agent decide when to ask. Native tool calls can complete without any prompt reaching the editor.
- Turning off
fsandterminalcapabilities and assuming the agent cannot read or run anything. It only means the agent cannot ask the editor to do it. - Auto-approving by
kindortitle. Both are labels written by the agent. A manipulated agent can describe a command as a harmless read. - Launching the editor from a shell full of credentials. The agent inherits that environment. Start the agent with a scrubbed one.
- Forwarding every MCP server into every session. Pass only what each project needs, and check the agent's native MCP configuration as well.
- Running agents with an unpinned package runner command. Each launch can pull a new release. Pin versions or digests.
- Granting "always allow" broadly. Use narrow, anchored patterns and review stored approvals.
- Shipping ACP v2 support by default. The project says v2 is a draft; gate it behind negotiation and flags.
- Assuming the root set is enforced. The spec says the session roots SHOULD bound file tool operations. Verify what your client and agent actually do.
FAQ
What is the Agent Client Protocol in simple terms?
It is a shared language that lets a code editor run a coding agent and show its work. The editor starts the agent, sends your prompt and receives a stream of messages, plans, tool calls and diffs in JSON-RPC over standard input and output.
Who created ACP?
Zed Industries created it and announced it on 27 August 2025, with Google's Gemini CLI as the first agent to implement it. It is open source under the Apache 2.0 license and is now run by a steering group of maintainers through a public RFD process.
Is ACP the same as MCP?
No. MCP connects an agent to tools and data sources. ACP connects an editor to the agent. They fit together: ACP reuses MCP's JSON types, and the editor passes MCP server configurations into each ACP session so the agent can connect to them.
Can an ACP agent run commands on my machine?
Yes, in two ways. If the editor offers the terminal capability, the agent can ask the editor to run commands through terminal/create. Independently of that, a local agent is a program running as your user and can spawn processes itself. Limit both with the agent's own rules and an operating system sandbox or container.
How do I see what my editor and agent are sending each other?
In Zed, run dev: open acp logs from the Command Palette. For any editor, wrap the agent command in a small script that copies standard input and output to log files, then read the JSON lines with jq. Treat those logs as sensitive.
Does ACP work with remote agents?
The specification says local agents use JSON-RPC over stdio and remote agents can use HTTP or WebSocket, but notes that full remote support is a work in progress. Streamable HTTP is a draft proposal and a Transports Working Group was formed in April 2026 to stabilise new transports.
What changed in ACP v2?
The draft of 20 July 2026 removes the client file and terminal methods in favour of MCP servers, lets updates flow outside a prompt turn, gives permission requests a title, description and subject, and merges session load into resume. Implementers are told to support v1 and v2 side by side.
Is it safe to use ACP agents on a work codebase?
It can be, with the care you would give any program that reads your code and runs commands: pin the version, scrub the environment, deny secret file reads, require approval for network and destructive commands, and use a container for sensitive repositories.
Where Safeguard fits
Safeguard does not implement ACP, does not sit between your editor and an agent, and does not detect prompt injection. The overlap is narrower and practical. ACP agents and adapters are themselves software with dependency trees, shipped as npm packages, Python packages, binaries and container images, and the projects agents work on pull in more dependencies every time an agent runs npm install or pip install. Safeguard can scan those repositories and images from the Safeguard CLI, match components against vulnerability databases, apply reachability analysis, show which projects carry a given version, and gate builds with policies. The Safeguard CLI is also a coding assistant, and it follows the pattern this post recommends: it asks before each command, and its /permissions modes (Ask, Accept edits, Plan, Auto and Strict) set what runs without a prompt. None of that replaces native agent rules, a scrubbed environment or a sandbox.
Software supply chain hygiene still matters for ACP because every agent session ends in dependency changes: the agent edits a manifest, runs an install, and the lockfile it leaves behind is what ships. The fit is indirect, since none of this governs how the agent behaves in your editor, but it does govern what the agent's work pulls into your builds.
Fix it without waiting for upstream
Whatever the vulnerability, most of the risk sits in two gaps. The first is the time between a fix being published and that fix actually running in your production builds. The second, and worse, is the time before any fix exists at all, while a maintainer works out a patch and a release schedule. Waiting through both gaps is a habit, not a requirement. Safeguard gives teams three ways to close them.
1. Autonomous remediation: pull requests, not tickets
When a fixed version exists, Safeguard's Griffin models open a tested, policy gated pull request that moves the affected dependency to the safe version. You can run it on one repository or in bulk across every project that carries the vulnerable version. Remediation covers eight categories, including dependencies, licenses, security posture and findings. Reachability analysis ranks the vulnerable code paths your application actually calls first, so the first pull requests you merge are the ones that matter most. Autonomous remediation ships switched off, with every category set to Manual, so your team decides how much runs on its own and what still needs a human approval.
2. Gold Open Source: the safe version in seconds, free
gold.safeguard.sh is free and needs no login. Search any CVE or package to see affected and fixed versions, CVSS, EPSS, CISA KEV status and a recommended safe version, or use the "Am I affected?" checker. The same data is available through a free read API, README status badges and RSS threat feeds. The safeguard-sh/gold-scan-action GitHub Action can fail a build on a known exploited vulnerability, on any known vulnerability, or never, so a version you have just removed cannot quietly come back in next week's merge.
3. Gold Registry: start from components that are already fixed
registry.safeguard.sh provides hardened, drop in replacements for upstream packages and container images. Every Gold artifact is held continuously to zero exploitable critical or high CVEs, zero malware and a completed license check, with SLSA L3 or higher provenance, a Sigstore signature and a public evidence page you can hand to an auditor. An artifact loses its Gold badge if it regresses. Teams can point their own package directory at the registry so hardened equivalents resolve without changing how developers install anything.
When upstream has no fix yet, or will not ship one, Safeguard first works to get the fix accepted upstream. If the issue is severe or upstream will not move, Safeguard builds a fork, fixes the vulnerability there and publishes a hardened Gold build, so you are not stuck on a maintainer's release schedule. You can also request a hardened, compatibility tested build of a specific package. This fork and patch flow is in early access today, so ask which packages it covers before you plan around it.
| Your situation | What to use |
|---|---|
| A fixed version exists | Autonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version |
| You need to know today whether you are affected | Gold Open Source lookup and "Am I affected?" checker, free and without login |
| You want vulnerable versions to stop shipping | The Gold scan GitHub Action in CI, plus Safeguard policies and gates |
| Upstream has no fix yet, or will not ship one | A custom Gold request; fork and patch (early access) |
| You want to start clean on new projects | Pull hardened components and images from Gold Registry |
One honest limit: when the vulnerable code lives in a vendor appliance, a hosted service or an operating system runtime you do not build yourself, the fix still has to come from that vendor or distribution. The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.
See how Safeguard would help
None of these demos shows an ACP editor. Each shows a Safeguard surface that uses the controls this post recommends for ACP agents: explicit approval before each action, permission modes you choose, and a person deciding at the point of risk. You can download Safeguard and follow the CLI documentation to try them.
This is a real CLI session. Asked to fix a lodash CVE, the CLI reads the finding and advisory, asks before each command, edits package.json to lodash 4.17.21, runs npm install and npm test with approval, prints a summary and pushes nothing, then shows /permissions, /model and /scan .. It is the same propose, approve, act loop that session/request_permission is meant to give an ACP client.
Click through it at your own pace, or open it full screen.
This real Android session sends work to a computer running the CLI and approves the command on the phone with Allow once, Allow for turn or Deny. Those choices map closely to ACP's allow_once and reject_once options, and the demo shows why scoped, short-lived approvals are easier to reason about than standing ones.
Click through it at your own pace, or open it full screen.
This demo connects Claude to the Safeguard MCP server with OAuth and lets you choose which tools Claude can use. It is the MCP half of an ACP session: the servers an editor forwards into a session should be chosen and scoped by a person, tool by tool, in the same way.
Click through it at your own pace, or open it full screen.
Quick check
Your editor offers no fs or terminal capability to an ACP agent. Can the agent still read files outside the project?
Yes, if it is a local agent. Turning off the client capabilities only stops the agent from asking the editor to read files or run commands. The agent is a process running as your user and can use its own tools. Use the agent's native deny rules and an operating system sandbox or container to stop it.
Which side decides whether a permission prompt is shown in ACP?
Both, in sequence. The agent decides whether to send session/request_permission at all (the spec says it MAY), and the client decides whether to ask the user or answer automatically from stored settings (the spec says it MAY auto-allow or auto-reject).
What does the ACP v2 draft replace the client file and terminal methods with?
An MCP server that the client passes into the session through the mcpServers list. The v2 migration guide says the v1 client execution surface was inconsistently implemented and removes fs/* and terminal/* entirely.
Sources
- Agent Client Protocol, Introduction
- Agent Client Protocol, Architecture
- Agent Client Protocol, Protocol overview (v1)
- Agent Client Protocol, Initialization (v1)
- Agent Client Protocol, Authentication (v1)
- Agent Client Protocol, Session Setup (v1)
- Agent Client Protocol, Prompt Turn (v1)
- Agent Client Protocol, Tool Calls (v1)
- Agent Client Protocol, File System (v1)
- Agent Client Protocol, Terminals (v1)
- Agent Client Protocol, Transports (v1)
- Agent Client Protocol, Session Modes (v1)
- Agent Client Protocol, Extensibility (v1)
- Agent Client Protocol, Migrating from v1 to v2
- Agent Client Protocol, ACP v2 is available in Draft (20 July 2026)
- Agent Client Protocol, Rust and TypeScript SDKs reach 1.0 (25 June 2026)
- Agent Client Protocol, Updates
- Agent Client Protocol, Governance
- Agent Client Protocol, ACP Registry
- Agent Client Protocol, Agents
- Agent Client Protocol, Clients
- agentclientprotocol/agent-client-protocol on GitHub
- Zed blog, Bring Your Own Agent to Zed (27 August 2025)
- Zed documentation, External Agents
- Zed documentation, Tool Permissions
- JSON-RPC 2.0 Specification
- Model Context Protocol specification, 2025-11-25
- Model Context Protocol, Transports
- Model Context Protocol, Security Best Practices
- A2A Protocol specification
- AG-UI documentation, Introduction
- Anthropic, Claude Code security
- Anthropic, Claude Code permissions
- Anthropic, Claude Code sandboxing
- OpenAI, Codex agent approvals and security
- OWASP GenAI Security Project, LLM01:2025 Prompt Injection
- OWASP GenAI Security Project, LLM06:2025 Excessive Agency
Last reviewed: 24 October 2026.