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}. cwd is the working directory of that session — instructions, tools and paths resolve against it, exactly as -C did. The mcpServers are started for the session like .mcp.json entries.
  • session/prompt {sessionId, prompt: [{type: "text", text}]} → runs a turn; the response {stopReason} comes only when the turn is over: end_turn, or cancelled after a session/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 (every text_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; for bash the title carries the command and kind is execute.
  • 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} and fs/write_text_file {sessionId, path, content}nullonly when the client declared the capability: then the read and write tools 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)
Sessions

0

Visibility

Public

Category

Reinvent the Wheel

Slug

handmade-claude-code-7-acp

Duration

30 min

Judge reviews

~13 per session

Active session

No

Points

10–40

Tags
  • ai-agent
  • harness
  • acp
  • handmade-claude-code
  • campaign
  • 1

    Set up and carry parts one to six forward

    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: --acp turns 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: and test:. 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, aider or any other coding agent,
    or building on an agent SDK that owns the loop, is not building one.

    Judged by
  • 2

    Initialize

    20

    pts / check

    +20 pts per passing check · +10 for completing the task

    --acp makes 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 is
    initialize; the reply carries protocolVersion: 1 and
    agentCapabilities. When stdin closes, the agent exits.

    Judged by
  • 3

    A session has a working directory

    20

    pts / check

    +20 pts per passing check · +10 for completing the task

    session/new opens a conversation and names it with a sessionId. Its
    cwd is the working directory of everything that follows — the
    instructions read, the tools' paths, the system prompt's "where" — just
    as -C was. session/prompt with a text block runs a turn and answers
    {stopReason: "end_turn"} when it is over.

    Judged by
  • 4

    The answer streams as chunks

    30

    pts / check

    +30 pts per passing check · +10 for completing the task

    The editor shows the answer as it is written. Every text_delta the
    model streams — or the final text, when it did not stream — goes out as
    a session/update with sessionUpdate: "agent_message_chunk" and a
    text content block, before the prompt's response. The check reads
    line order.

    Judged by
  • 5

    Tool calls are reported

    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/update with sessionUpdate: "tool_call" carries a
    toolCallId, a title (for bash, the command), kind: "execute" and
    a status of pending or in_progress. When it ends, a
    tool_call_update with the same toolCallId carries status: "completed" and the output as a text content block.

  • 6

    Permission is asked

    40

    pts / check

    +40 pts per passing check · +10 for completing the task

    Without --yes, a call the rules do not permit becomes a
    session/request_permission request to the editor: toolCall with the
    id, title and input, and two options — optionId: "allow" (kind: "allow_once") and optionId: "reject" (kind: "reject_once"). The
    turn waits for the reply. {outcome: {outcome: "selected", optionId: "allow"}} runs the tool; reject — or a cancelled outcome — denies
    it with the usual permission denied result.

  • 7

    Cancel

    30

    pts / check

    +30 pts per passing check · +10 for completing the task

    session/cancel is 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 pending session/prompt is answered with
    {stopReason: "cancelled"} — promptly. A later prompt starts a fresh
    turn.

  • 8

    The editor's files

    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 declared fs.readTextFile, the read tool
    asks the editor with fs/read_text_file (an absolute path) and uses
    the content it returns instead of the disk. When it declared
    fs.writeTextFile, the write tool sends fs/write_text_file and
    lets the editor do the writing. Without the capability, the disk is the
    truth as before.

  • 9

    The client's MCP servers

    20

    pts / check

    +20 pts per passing check · +10 for completing the task

    The editor brings its own servers. The mcpServers of session/new
    name, command, args, env as a list of {name, value} — are
    started for that session exactly like .mcp.json entries: greeted,
    their tools advertised to the model as mcp__<name>__<tool>, their
    environment set, and shut down with the agent.