Skip to content

system messages (hook_*, task_notification, compact_boundary, status, files_persisted) are dropped without any client-visible signal #1030

Description

@babayuhei

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:

  1. 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.
  2. Forward them as session/update notifications with an _meta field, letting clients opt in.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions