Summary
system messages with subtypes hook_started, hook_progress, hook_response, task_notification, compact_boundary, status, and files_persisted are silently dropped by the adapter, so ACP clients cannot surface them at all.
The break is annotated // Todo: process via status api, so this looks like known-unimplemented rather than an intentional design choice — filing this to share the client-side impact we measured, in case it helps prioritize.
Version: @zed-industries/claude-code-acp 0.16.2
Where
dist/acp-agent.js (built from src/acp-agent.ts):
case "system":
switch (message.subtype) {
case "init":
break;
case "compact_boundary":
case "hook_started":
case "task_notification":
case "hook_progress":
case "hook_response":
case "status":
case "files_persisted":
// Todo: process via status api: https://docs.claude.com/en/docs/claude-code/hooks#hook-output
break;
default:
unreachable(message, this.logger);
break;
}
Why this is harder to notice than it looks
Unknown subtypes reach unreachable(), which logs Unexpected case: {...} to stderr. A client can (and we do) scrape that line — we recover thinking_tokens that way today.
But once a subtype is added to this switch, it stops producing that line. So from a client's perspective:
The more subtypes the adapter learns about, the less visible they become.
files_persisted appears to have made exactly this transition — an older version emitted it as Unexpected case, which is still visible in a comment in our own test fixtures. In the current version it is completely silent.
This is counterintuitive because "the adapter handles more cases" normally reads as progress. Concretely: no error means no delivery here, which makes the gap easy to miss.
Observed impact
The failure mode is not "something breaks" — it is "the explanation disappears", which is why it went unreported for a while on our side.
Our client runs Claude Code through this adapter as a chat UI. A Stop hook that blocks and re-prompts renders in Claude Code's own terminal UI with the hook name and reason. Through ACP, the user sees the turn end and then a new turn begin, with no indication that a hook stopped it or why. Same for subagent/background task completion (task_notification) and auto-compaction (compact_boundary).
We worked around three of these individually before realizing they shared one cause:
| subtype |
our workaround |
compact_boundary |
a PreCompact hook posts a notification out-of-band |
task_notification |
read the parent transcript JSONL directly |
hook_started / hook_progress / hook_response |
the hook launcher detects exit 2 and posts out-of-band |
Each was invented independently in a different part of our codebase, because there was no signal tying them to a common cause.
Suggestion
Even a minimal passthrough would remove the need for out-of-band workarounds. Some options, roughly in increasing order of effort:
- Log these via
this.logger (like unreachable does) so clients can at least observe them on stderr — this alone restores parity with the pre-case behavior.
- Forward them as
session/update notifications with an _meta field, letting clients opt in.
- The
status api the TODO refers to, if that design is already settled.
Happy to send a PR for (1) or (2) if that would be useful — just let me know which direction you'd prefer.
Repro
Configure a Stop hook that exits 2, run the agent through this adapter, and observe that the client receives no notification about the hook — while Claude Code's own UI displays it.
Summary
systemmessages with subtypeshook_started,hook_progress,hook_response,task_notification,compact_boundary,status, andfiles_persistedare silently dropped by the adapter, so ACP clients cannot surface them at all.The
breakis annotated// Todo: process via status api, so this looks like known-unimplemented rather than an intentional design choice — filing this to share the client-side impact we measured, in case it helps prioritize.Version:
@zed-industries/claude-code-acp0.16.2Where
dist/acp-agent.js(built fromsrc/acp-agent.ts):Why this is harder to notice than it looks
Unknown subtypes reach
unreachable(), which logsUnexpected case: {...}to stderr. A client can (and we do) scrape that line — we recoverthinking_tokensthat way today.But once a subtype is added to this switch, it stops producing that line. So from a client's perspective:
files_persistedappears to have made exactly this transition — an older version emitted it asUnexpected case, which is still visible in a comment in our own test fixtures. In the current version it is completely silent.This is counterintuitive because "the adapter handles more cases" normally reads as progress. Concretely: no error means no delivery here, which makes the gap easy to miss.
Observed impact
The failure mode is not "something breaks" — it is "the explanation disappears", which is why it went unreported for a while on our side.
Our client runs Claude Code through this adapter as a chat UI. A
Stophook that blocks and re-prompts renders in Claude Code's own terminal UI with the hook name and reason. Through ACP, the user sees the turn end and then a new turn begin, with no indication that a hook stopped it or why. Same for subagent/background task completion (task_notification) and auto-compaction (compact_boundary).We worked around three of these individually before realizing they shared one cause:
compact_boundaryPreCompacthook posts a notification out-of-bandtask_notificationhook_started/hook_progress/hook_responseexit 2and posts out-of-bandEach was invented independently in a different part of our codebase, because there was no signal tying them to a common cause.
Suggestion
Even a minimal passthrough would remove the need for out-of-band workarounds. Some options, roughly in increasing order of effort:
this.logger(likeunreachabledoes) so clients can at least observe them on stderr — this alone restores parity with the pre-casebehavior.session/updatenotifications with an_metafield, letting clients opt in.status apithe TODO refers to, if that design is already settled.Happy to send a PR for (1) or (2) if that would be useful — just let me know which direction you'd prefer.
Repro
Configure a
Stophook that exits 2, run the agent through this adapter, and observe that the client receives no notification about the hook — while Claude Code's own UI displays it.