The April 2026 Claude Code update from inside an app that ships on top of it
Most write-ups of the April 2026 Claude Code release recap the official launch post: Opus 4.7 in the CLI, Visualizations, Cowork going GA, Managed Agents in beta. The other half of the story is the agent SDK that powers all of it. @agentclientprotocol/claude-agent-acp jumped from v0.25.0 on April 7 to v0.29.2 on April 20, and downstream apps had two weeks to absorb a model alias rename, a dynamic models payload, an exposed compaction stream, and at least nine session event types the stock agent silently consumes and drops.
Fazm is a consumer Mac app that runs this exact SDK as a subprocess for every paying user. This page is the story of what we patched and why.
Two updates, one month, one SDK rev
The CLI got the headlines. The npm package did the work. Every change a downstream app had to make in April traces back to a single tarball.
Try Fazm on macOSThe package nobody talks about
When you run claude-code in a terminal, what actually starts is a Node subprocess running this package. When you embed Claude Code in another app, you import the same package. The bin script is a thin entrypoint; the real surface is ClaudeAcpAgent exported from dist/acp-agent.js.
Anthropic shipped four minor versions in April: 0.26.0 on April 8, 0.27.x in the second week, 0.28.0 around April 15, and 0.29.2 on April 20. The first one Fazm absorbed was v0.25.0 on April 7. The big jump was v0.29.2.
How the bridge sits between the agent and the app
The Mac app does not talk to Anthropic. The Mac app talks to the agent, the agent talks to Anthropic. The patch sits between them and rewrites a few of the messages on the way back.
Fazm bridges the patched agent to the consumer UI
The 267-line patch, in full
The whole thing is one file: acp-bridge/src/patched-acp-entry.mjs. It imports the agent class directly from the dist path, wraps two prototype methods, and calls runAcp() at the bottom so the rest of the SDK behaves identically.
The nine dropped events
The stock ClaudeAcpAgent reads items off the SDK query iterator. Items it does not recognize as plain text or tool calls get consumed and dropped. Here is the full list as of v0.29.2.
compact_boundary
Emitted when the SDK rolls older messages into a compacted summary to stay under the model's context window. Trigger is auto, manual, or pre_compact and pre_tokens carries the size of the rolled-up window.
status
High-level lifecycle states the agent transitions through: starting, processing, finalizing, idle. Useful for swapping a generic spinner for an actual phase indicator.
task_started
Fires when the agent decomposes a request into one or more named subtasks. Carries task_id and a human-readable description that you can surface in the UI.
task_notification
Per-task progress: status (running, complete, error) and a free-form summary. Pairs with task_started for live subtask reporting.
api_retry
Fired when the underlying Anthropic API call returns a 429, 503, or transient error and the SDK is about to retry. Carries httpStatus, attempt, max_retries, and retry_delay_ms. Without this, your app looks frozen during a retry storm.
rate_limit_event
Distinct from api_retry. Carries the structured rate-limit info from the Claude account itself: status, resetsAt, rateLimitType, utilization, overageStatus. This is how an embedding app shows "you have 12 minutes until your weekly cap resets" instead of a generic 429.
tool_progress
Long-running tools (Bash, browser_navigate, MCP tool calls) emit elapsed_time_seconds heartbeats so the client can keep its watchdog alive. Without this you cannot distinguish a slow tool from a hung one.
tool_use_summary
After a sequence of related tool calls, the SDK emits a one-line summary plus the preceding tool_use_ids. This is the data you would otherwise have to synthesize by tailing logs.
compaction stream chunks
The SDK streams the compacted summary as content_block_delta items of type compaction_delta. Forwarding these lets the UI show the in-flight summary the way it would show a normal assistant response.
A query, end to end
Here is what one query looks like across the wire when the patch is in place. Notice that the cost field, the compaction boundary, and the rate-limit event all reach the client.
One Fazm chat turn through the patched v0.29.2 agent
Cost in dollars, captured at the source
The Claude SDK puts a per-result total_cost_usd field on the result item. The stock agent reads the result for its assistant text and discards the rest. The patched session.query.next stashes the field on the session, computes the delta against the cumulative session cost, and the patched prompt() attaches it to its return value. Below is the actual log line shape Fazm writes for every query.
What this looks like in production
Numbers that come straight from the patched agent on a paying user's Mac, not from a benchmark.
Per-query cost
$0
Typical Opus 4.7 turn with cached context. Captured from item.value.total_cost_usd.
Cache read tokens
0
The accessibility tree and prior turns get read from cache, not re-billed.
Tool watchdog
0s
Default per-tool wall-clock budget. MCP tools get 120s, internal tools get 10s.
Stock agent vs the patched bridge
A practical diff of what the embedding app sees with and without the patch in v0.29.2. None of these are SDK bugs; they are just events the stock ClaudeAcpAgent chooses not to forward.
| Feature | Stock claude-agent-acp v0.29.2 | Patched (Fazm) |
|---|---|---|
| Per-query USD cost | Consumed and dropped from item.value.total_cost_usd; never reaches the client | Captured in patched session.query.next; attached as _meta.costUsd to prompt() result |
| Compaction visibility | Silent. The UI sees a long pause between assistant messages | compact_boundary + compaction_delta both forwarded as sessionUpdate so the UI can render the summary |
| API retry feedback | Logged to stderr only; the client sees no event | api_retry forwarded with httpStatus, attempt, max_retries, retry_delay_ms |
| Rate-limit metadata | rate_limit_event item type emitted by SDK but not forwarded by agent | Forwarded with utilization, overageStatus, isUsingOverage, resetsAt, surpassedThreshold |
| Long-tool progress | tool_progress dropped; client cannot tell slow from hung | Forwarded with elapsed_time_seconds so a watchdog can keep the spinner alive |
| Subtask reporting | task_started and task_notification dropped | Both forwarded with task_id, status, summary |
| Tool timeout safety | No wall-clock budget; a hung MCP tool can block the entire session | Per-tool budget (10s internal / 2 min MCP / 5 min default) with synthesized error to unblock the model |
| Opus model alias | "opus" no longer accepted in v0.29; user preferences silently break | normalizeModelId in ShortcutSettings.swift maps "opus" -> "default" on load |
Architecture decisions that survived the bump
A few choices in the bridge made the v0.25 to v0.29 jump painless. Most of them predate the SDK bump by months.
Long-lived subprocess, not request-per-turn
The agent runs as a Node child process spawned by the Mac app at launch. It stays alive across queries, owns conversation history, and never re-replays prior turns. Killing it requires kill(-pid) because the SDK creates its own process group.
JSON-RPC over stdio, not HTTP
Every message between Fazm and the agent is a single line of JSON on stdin/stdout. No localhost server, no port collisions, no auth tokens to manage. The native side parses with createInterface from readline.
Per-tool wall-clock timeouts
The patched bridge tracks every running tool and enforces a wall-clock budget: 10s for internal tools, 2 min for MCP tools, 5 min for everything else. When the budget elapses the bridge synthesizes a tool_use error so the model can recover and the UI unblocks.
9 dropped event types, re-emitted
compact_boundary, status, task_started, task_notification, api_retry, rate_limit_event, tool_progress, tool_use_summary, and compaction_delta. None of these reach the client through the stock agent. All nine are re-emitted as ACP sessionUpdate notifications by the patch.
Cost in USD, captured at item.value.total_cost_usd
Per-query USD cost is on the result item but consumed and dropped by the stock agent. The patched session.query.next stashes it on the session, the patched prompt() attaches it to the return value, and the Swift bridge writes it into APIClient.recordLlmUsage along with input/output/cache token counts.
The rollout, day by day
Anthropic's own April calendar and the Fazm release cadence overlapped almost one-for-one. Here is the SDK side of it.
April 7 - claude-agent-acp v0.25.0
First ACP bump in April. Improved error handling for credit exhaustion and rate limits. Onboarding stopped silently failing when new users ran out of built-in credits and now showed an actual prompt to connect a personal Claude account or skip. Fazm shipped this as v2.1.2 the same day.
April 8 to 19 - v0.26.x to v0.28.x
Three intermediate minor versions. Internal SDK refactors that changed how session events are forwarded. The dropped-events list grew from 4 types to 9 over this window. None of them shipped to Fazm production individually because the patch surface was still moving.
April 20 - claude-agent-acp v0.29.2
The big one. Models picker becomes dynamic via the models_available notification. The Opus alias renames from "opus" to "default". Compaction is exposed as a stream. Fazm shipped this as v2.4.0 the same day along with custom MCP server support and a new patched-acp-entry.mjs that handles all 9 dropped event types.
April 22 - downstream cleanup as v2.4.1
No new SDK release. Fixed the partial-models payload bug (Smart label drifting to Sonnet for users without Opus access during a rollout window), fixed the paywall blocking users mid-onboarding because of the new dynamic-credits state, and fixed a revoked-sign-in retry loop that came from the new auth-required notification path in v0.29.
Pinned dependencies, end of April
The full set of npm pins that ship inside the Fazm app bundle as of April 22, 2026.
Why the model alias rename matters
Before v0.29 you sent modelId: "opus" and got the latest Opus. After v0.29 you send "default" and the agent picks the latest Opus your account has access to. Any app that stored the user's preference as the literal string "opus" in UserDefaults silently broke until it migrated.
Fazm's migration is one function:
The unreleased CHANGELOG.json line for the next Fazm version is literally about this exact migration: "Fixed Smart (Opus) model preference not persisting after app update, now correctly maps stored "opus" to the new ACP model ID."
The takeaway
The April 2026 Claude Code update is two things stitched together: a model and CLI refresh that everyone wrote about, and an agent SDK rewrite that no one wrote about. For an embedding app, the second one is the one that matters.
If you embed @agentclientprotocol/claude-agent-acp in your own product, the 267-line patch in acp-bridge/src/patched-acp-entry.mjs is a copy-paste starting point. If you just want a Mac app that already runs it, install Fazm.
Embedding Claude Code in your own app?
Book 20 minutes and we'll walk through the patch, the timeouts, and what we learned absorbing four SDK bumps in two weeks.
Book a call →Frequently asked questions
What actually shipped in the Claude Code update in April 2026?
Two things, and they are easy to confuse. The first is the consumer-facing CLI: Claude Code the terminal app got the Opus 4.7 model on April 16, the new Visualizations renderer, Claude Cowork going GA on macOS and Windows, and Managed Agents in public beta. The second, and the one this page focuses on, is the agent SDK that powers all of it: @agentclientprotocol/claude-agent-acp jumped from v0.25.0 on April 7 to v0.29.2 on April 20. That is four minor version bumps in 13 days, and every one of them changed how a downstream app embeds the agent.
What is @agentclientprotocol/claude-agent-acp and why does it matter for the April 2026 update?
It is the npm package that implements the Agent Client Protocol on top of Anthropic's Claude SDK. When you run claude-code in a terminal, it spawns this package as a JSON-RPC subprocess and talks to it over stdio. Any app that wants to embed Claude Code as an agent (not just call the API) imports this package and runs it. Fazm imports it directly: see the dependencies block in /Users/matthewdi/fazm/acp-bridge/package.json, which pins "@agentclientprotocol/claude-agent-acp": "^0.29.2" and resolves to the v0.29.2 tarball in package-lock.json. The April 2026 update is mostly a rewrite of how this package surfaces session events.
What is the dropped-events problem in v0.29 and what events get dropped?
The stock @agentclientprotocol/claude-agent-acp builds a Claude SDK query iterator and pulls items off it inside ClaudeAcpAgent. Items it does not recognize as plain text or tool calls get silently consumed and dropped. In v0.29 the Claude SDK started emitting nine types of items the agent does not forward: type="system" with subtypes compact_boundary, status, task_started, task_notification, and api_retry; type="rate_limit_event"; type="tool_progress"; type="tool_use_summary"; and type="stream_event" with a content_block of type="compaction". From inside a consumer app this is invisible until your users complain that the spinner stopped moving for 90 seconds during a compaction or that they can't tell why a tool call is taking forever. The fix is to monkeypatch session.query.next and re-emit the dropped items as ACP sessionUpdate notifications.
How do you actually monkeypatch the Claude Code agent in production?
You don't import @agentclientprotocol/claude-agent-acp from the package root, because that auto-runs the agent. You import the inner module the package's bin script imports, then wrap the prototype methods. Fazm's patch lives at acp-bridge/src/patched-acp-entry.mjs (267 lines). It does: import { ClaudeAcpAgent, runAcp } from "@agentclientprotocol/claude-agent-acp/dist/acp-agent.js", saves the original createSession, replaces ClaudeAcpAgent.prototype.createSession with a wrapper that intercepts the query iterator, and finally calls runAcp() to start the agent normally. The Swift side passes -e patched-acp-entry.mjs to node instead of the default bin. The patched entrypoint is bundled into the .app at build time.
Where is the per-query USD cost in the v0.29 SDK and why isn't it surfaced by default?
It lives on the SDK result object as item.value.total_cost_usd when item.value.type === "result" and subtype === "success". The stock @agentclientprotocol/claude-agent-acp consumes the result item to get the assistant's last text but throws the cost field away. Fazm captures it inside the patched session.query.next: it reads item.value.total_cost_usd, subtracts the previous session cost (stored as session._sessionCostUsd), and stores the delta as session._lastCostUsd. The patched ClaudeAcpAgent.prototype.prompt then attaches a _meta.costUsd field to its return value. From there the Swift bridge writes it into APIClient.recordLlmUsage(...) along with input/output/cache token counts. Per-query cost down to the cent is the difference between knowing whether your built-in credits will last the trial and finding out at the paywall.
What changed about the Opus model identifier in v0.29?
Before v0.29, when you wanted Opus you sent modelId: "opus" in the session/setModel call. After v0.29 the SDK accepts "default" as the alias for the latest Opus, and "opus" is no longer a valid alias on its own. Any app that stored the user's model preference as the literal string "opus" silently broke until it migrated. Fazm's migration is one function, normalizeModelId at /Users/matthewdi/fazm/Desktop/Sources/FloatingControlBar/ShortcutSettings.swift line 174, which does: if modelId.contains("opus") { return "default" }. The unreleased CHANGELOG.json line for the next version is literally about this exact migration.
What is the dynamic models_available payload and why did it ship in this update?
Before v0.29, an embedding app shipped a hard-coded list of supported Claude models and updated it on every Anthropic release. After v0.29 the agent emits a session/update notification with method models_available containing the full list of models the user's account has access to, scoped to whatever credit pool they are using. The downstream app builds its model picker from that payload at runtime. The Fazm release notes for v2.4.0 on April 20 say it directly: "Available AI models now populate dynamically from the agent, so newly released Claude models appear without an app update." The handler is at ACPBridge.swift line 1131 (case "models_available"). When Anthropic flipped the switch on Opus 4.7 on April 16, the new model showed up in the picker the same day with no app update required.
What is the partial models_available bug that v2.4.1 fixed?
During the rollout window of a new Anthropic release, the SDK occasionally returns a subset of models in models_available, for example only Sonnet during the hour before Opus comes back online for a region. Fazm has a substring-matched family table at ShortcutSettings.swift that maps modelId.contains("opus") to the Smart label, modelId.contains("sonnet") to Fast, and modelId.contains("haiku") to Scary. The first version of the labeler walked the returned list and assigned Smart to whatever model came back ranked highest. For an Opus-less payload that briefly meant Smart pointed at Sonnet. v2.4.1 on April 22 anchored Smart to the opus substring so a payload with no opus model now leaves Smart undefined rather than reassigning the label.
How does the Agent Client Protocol differ from just calling the Anthropic API?
The Anthropic API is request and response: send messages, get a single completion back. The Agent Client Protocol is a bidirectional JSON-RPC session: the agent owns conversation history, tool calling, planning, compaction, and rate-limit handling. The client is the embedding app and it gets streaming sessionUpdate notifications as the agent works. The client never re-sends history; the agent's session is the source of truth. That is why ACPBridge.swift uses one persistent session per Fazm chat thread and never injects prior turns into the system prompt. The per-query Anthropic API calls are an internal detail of the agent and the cost is the aggregate across however many tool-use rounds happened.
Why is Fazm using accessibility APIs and not screenshots if the new Claude has Visualizations?
Visualizations is an output feature. It lets Claude render charts and diagrams inside its own response. It does not change how the agent reads what is happening on your Mac. The input side is still the agent's job, and a screenshot pipeline pays the OCR and re-segmentation cost on every turn. Fazm hands the agent the actual macOS accessibility tree (AXUIElement structures from the Accessibility API) for whatever app is in focus. That tree is structured text, not pixels. When Anthropic ships a smarter Opus, the input quality stays the same; only the model gets better at acting on it. A Visualizations chart in the response is rendered the same way regardless.
Where can I read the patched ACP entry source?
It is in the Fazm repo at acp-bridge/src/patched-acp-entry.mjs. The file is 267 lines, MIT-style, and the structure is small enough to copy into your own embedding app: import ClaudeAcpAgent and runAcp from the dist/acp-agent.js path, save originalCreateSession = ClaudeAcpAgent.prototype.createSession, replace it with a wrapper that calls the original then intercepts session.query.next, then call runAcp() at the bottom. The Swift bridge that spawns it is Desktop/Sources/Chat/ACPBridge.swift; look at the spawn logic around line 532 (acpEntry = join(__dirname, "patched-acp-entry.mjs")) for the wiring.
Adjacent guides written from the same downstream consumer-app vantage point.
More on the April 2026 Claude release wave
Anthropic Claude release notes April 2026
Twelve Fazm releases in 20 days, each one tied to a specific Anthropic-side change.
Anthropic Claude new model release April 2026
Opus 4.7 GA on April 16 and how a substring-matched family table absorbed it without an app update.
Claude AI for macOS
Why the bridge reads the macOS accessibility tree instead of screenshotting the screen.