Handmade Claude Code 7/7 — ACP
Part seven of the Handmade Claude Code campaign: the agent moves into the editor. The Agent Client Protocol is how editors — Zed, Neovim, JetBrains, anything that speaks it — drive a coding agent: they start it, open sessions, send prompts, watch the answer stream in, see every tool call, answer its permission questions, cancel it, and lend it their own unsaved files and MCP servers. This part makes yours a citizen of that world — the same loop, one more front end.
This session continues the agent you built in parts one to six, in the same repository. The protocol client is yours to write, like the MCP client was: JSON-RPC 2.0 over stdio, one object per line, in both directions.
Starting
<command> --acp
reads JSON-RPC from stdin and writes it to stdout — nothing else goes to
stdout — until stdin closes. --yes applies here too. The working
directory comes from the session, not from -C.
The methods
Client → agent, requests:
initialize{protocolVersion: 1, clientCapabilities: {fs: {readTextFile, writeTextFile}}, clientInfo}→{protocolVersion: 1, agentCapabilities: {...}, agentInfo, authMethods: []}.session/new{cwd, mcpServers: [{name, command, args, env: [{name, value}]}]}→{sessionId}.cwdis the working directory of that session — instructions, tools and paths resolve against it, exactly as-Cdid. ThemcpServersare started for the session like.mcp.jsonentries.session/prompt{sessionId, prompt: [{type: "text", text}]}→ runs a turn; the response{stopReason}comes only when the turn is over:end_turn, orcancelledafter asession/cancel.
Client → agent, notification: session/cancel {sessionId} — stop the
turn: kill a running tool, make no further model call, answer the pending
prompt with stopReason: "cancelled".
Agent → client, notifications session/update {sessionId, update},
where update.sessionUpdate is:
agent_message_chunk{content: {type: "text", text}}— the answer, as it streams (everytext_delta, or the final text when the model did not stream), before the prompt response.tool_call{toolCallId, title, kind, status: "pending" | "in_progress", rawInput}— when a tool starts; forbashthe title carries the command andkindisexecute.tool_call_update{toolCallId, status: "completed" | "failed", content: [{type: "content", content: {type: "text", text}}], rawOutput}— when it ends, with its output.
Agent → client, requests:
session/request_permission{sessionId, toolCall: {toolCallId, title, kind, rawInput}, options: [{optionId: "allow", name, kind: "allow_once"}, {optionId: "reject", name, kind: "reject_once"}]}— for a call the rules do not permit, instead of the TUI's question. The reply{outcome: {outcome: "selected", optionId}}decides;{outcome: {outcome: "cancelled"}}denies.fs/read_text_file{sessionId, path, line?, limit?}→{content}andfs/write_text_file{sessionId, path, content}→null— only when the client declared the capability: then thereadandwritetools go through the editor (which has the unsaved buffers) instead of the disk. Paths are absolute.
Ids: the client picks ids for its requests and the agent for its own; a reply carries the id of the request it answers. The checks drive the protocol from a shell script, so line order in the output is what they read: a chunk is streamed when it is written before the response.
The judges
The quality panel — architecture, performance, code quality, test quality, technical governance and DX review — sits on the rungs where its subject is decided, one verdict per judge per rung, on top of the rung's own points. There is no closing review: what you build is judged as you build it, and a rung you never reach is a verdict you never get.
The ladder
- Set up and carry parts one to six forward (10)
- Initialize (20)
- A session has a working directory (20)
- The answer streams as chunks (30)
- Tool calls are reported (30)
- Permission is asked (40)
- Cancel (30)
- The editor's files (20)
- The client's MCP servers (20)
0
Public
Reinvent the Wheel
handmade-claude-code-7-acp
30 min
~13 per session
No
10–40
- ai-agent
- harness
- acp
- handmade-claude-code
- campaign
1
Set up and carry parts one to six forward
+10 pts per passing check · +10 for completing the task
10
pts / check
+10 pts per passing check · +10 for completing the task
This part continues your agent. Same repository, same command, same
loop — what grows is one more front end:--acpturns the agent into an
Agent Client Protocol server on stdio, so an editor can drive it.agent: started as: --acp
Your AGENTS.md (or README.md) must still carry the two lines the
platform captures into session memory:agent:andtest:. If you are
starting in an empty folder, your earlier work is fetched for you —
check that the lines are there and that the agent still builds and
runs.The first rung re-checks the headless contract the earlier parts
earned. Everything after it is protocol work, driven by a scripted
client the checks write for you. The protocol client is yours; an ACP
SDK is out of contract.Wrapping
claude,codex,gemini,aideror any other coding agent,
or building on an agent SDK that owns the loop, is not building one.Judged by
The Debrief 2
Initialize
+20 pts per passing check · +10 for completing the task
20
pts / check
+20 pts per passing check · +10 for completing the task
--acpmakes the agent a JSON-RPC 2.0 server on stdio: one object per
line in, one per line out, nothing else on stdout. The first request isinitialize; the reply carriesprotocolVersion: 1andagentCapabilities. When stdin closes, the agent exits.Judged by
Architecture 3
A session has a working directory
+20 pts per passing check · +10 for completing the task
20
pts / check
+20 pts per passing check · +10 for completing the task
session/newopens a conversation and names it with asessionId. Itscwdis the working directory of everything that follows — the
instructions read, the tools' paths, the system prompt's "where" — just
as-Cwas.session/promptwith a text block runs a turn and answers{stopReason: "end_turn"}when it is over.Judged by
Code Quality 4
The answer streams as chunks
+30 pts per passing check · +10 for completing the task
30
pts / check
+30 pts per passing check · +10 for completing the task
The editor shows the answer as it is written. Every
text_deltathe
model streams — or the final text, when it did not stream — goes out as
asession/updatewithsessionUpdate: "agent_message_chunk"and a
text content block, before the prompt's response. The check reads
line order.Judged by
Performance 5
Tool calls are reported
+30 pts per passing check · +10 for completing the task
30
pts / check
+30 pts per passing check · +10 for completing the task
The editor sees what the agent does. When a tool starts, a
session/updatewithsessionUpdate: "tool_call"carries atoolCallId, atitle(forbash, the command),kind: "execute"and
astatusofpendingorin_progress. When it ends, atool_call_updatewith the sametoolCallIdcarriesstatus: "completed"and the output as a text content block.6
Permission is asked
+40 pts per passing check · +10 for completing the task
40
pts / check
+40 pts per passing check · +10 for completing the task
Without
--yes, a call the rules do not permit becomes asession/request_permissionrequest to the editor:toolCallwith the
id, title and input, and two options —optionId: "allow"(kind: "allow_once") andoptionId: "reject"(kind: "reject_once"). The
turn waits for the reply.{outcome: {outcome: "selected", optionId: "allow"}}runs the tool;reject— or acancelledoutcome — denies
it with the usualpermission deniedresult.7
Cancel
+30 pts per passing check · +10 for completing the task
30
pts / check
+30 pts per passing check · +10 for completing the task
session/cancelis Esc for the editor. A notification with the
session id stops the turn: a running tool is killed, no further model
call is made, and the pendingsession/promptis answered with{stopReason: "cancelled"}— promptly. A later prompt starts a fresh
turn.8
The editor's files
+20 pts per passing check · +10 for completing the task
20
pts / check
+20 pts per passing check · +10 for completing the task
The editor knows things the disk does not — the buffer that is not
saved yet. When the client declaredfs.readTextFile, thereadtool
asks the editor withfs/read_text_file(an absolutepath) and uses
thecontentit returns instead of the disk. When it declaredfs.writeTextFile, thewritetool sendsfs/write_text_fileand
lets the editor do the writing. Without the capability, the disk is the
truth as before.Judged by
Technical Governance 9
The client's MCP servers
+20 pts per passing check · +10 for completing the task
20
pts / check
+20 pts per passing check · +10 for completing the task
The editor brings its own servers. The
mcpServersofsession/new—name,command,args,envas a list of{name, value}— are
started for that session exactly like.mcp.jsonentries: greeted,
their tools advertised to the model asmcp__<name>__<tool>, their
environment set, and shut down with the agent.