# Claude Code v2.1.228

> Claude Code v2.1.228, released 11 Aug 2026 (2026-08-11). 245 entries read out of the shipped bundle. Unofficial, and not affiliated with Anthropic.

Web version: https://changelogs.core-directive.com/v/2.1.228

Thirty-nine entries are in this build without being reachable. A diff panel lives inside the REPL, with a file list, a noise filter and an "Ask about selection" action, and `/diff` toggles it instead of opening the old dialog, but it sits behind the `tengu_willow_crate` gate whose in-source fallback is false. Skills synced from a claude.ai account are a new source alongside local and plugin skills, fetched only if you set `CLAUDE_CODE_SYNC_SKILLS`, while the new word processor and spreadsheet skills are held off by a build-time constant that always returns false, so no setting turns them on. Remote sessions can be pinned to one device, reporting every outcome as `tengu_device_bind_attach`, and per-turn file sync with a cloud session can now pull as well as push under the `tengu_violin_wood` flag. The bundled SDK also carries resources for deployments, dreams, tunnels and webhooks that no command calls yet.

Of the 95 shipped entries, the self-hosted gateway takes the largest share: a new `cri` config block lets it admit Claude Enterprise callers presenting Anthropic-minted ES256 tokens checked against an organisation allowlist, and the `oidc:` sign-in block is now optional, so a gateway can run with no identity provider at all. Managed settings accept a `policyHelpers` object keyed by `macos`, `linux`, `windows` and `wsl`, with the matching entry beating the existing single `policyHelper`. Setting `CLAUDE_CODE_COWORK_FRAME_ARTIFACTS` makes artifact permission decisions be asked fresh rather than reused across a session. Memory store requests now send the beta value `agent-memory-2026-07-22`, which matters if a proxy allow-lists the old one, and the Write-before-Read rule is now decided in the client instead of by a server flag.

The model-backed pre-flight check on shell commands is gone; the safe prefix is derived locally from the already-parsed command, removing a round-trip that could stall before anything ran. The gateway now strips inbound `authorization` and `x-api-key` headers before attaching its own credential upstream. Artifacts are disabled by `CLAUDE_CODE_DISABLE_ARTIFACT` and `disableArtifact` rather than the Workflows switch they were reading. A refusal now ends the assistant turn instead of letting the tool loop continue, and a title rename arriving over the mobile bridge is dropped rather than applied to whatever conversation happens to be open.

The bundled Anthropic client swapped its memory-version endpoints for `/v1/deployment_runs?beta=true`, gained a `threads` resource, and now strips `user_profile_id` out of message, token-count and batch-create bodies before sending. The SDK host auth token refresh hands back the whole control response and leaves the token extraction to the callback. Datadog flush interval is now read from `CLAUDE_CODE_DATADOG_FLUSH_INTERVAL_MS` at one call site while the older variable name stays live elsewhere.

## What probably matters to you

Anything you can use today, anything that visibly changes, and anything worth poking at. One line each, open for detail.

### Diff panel inside the REPL

A full uncommitted-changes side panel with filters and "Ask about selection" is built but gated off.

**What**

A side panel showing uncommitted changes is built into the REPL, with `/diff` toggling it rather than opening the existing dialog. It has a file list, a section for changes made before this session, a filter that hides test and generated-file noise, cycling of the comparison base, and an "Ask about selection" action that sends a selected hunk into your prompt. It sits behind the `tengu_willow_crate` gate, whose fallback in this build is false, so nothing changes without a remote value enabling it.

**Details**

- New keybindings: `app:toggleReplTab`, `app:cycleDiffBase`, `app:toggleDiffNoiseFilter`, `app:toggleDiffPreSession`, `app:diffFileListUp` and `app:diffFileListDown`.

**Evidence**

`Toggle the diff panel showing uncommitted changes`

- Flag `tengu_willow_crate`: Off in both readings (read for one account on one subscription tier against v2.1.228; this account: off, anonymous baseline: off, compiled default: on) These values were read against a different version of Claude Code, so treat them as the nearest reading available instead of one taken on this release.
- Area: Diff Panel
- Names: `/diff`
- Tier: Not switched on
- Useful: 4/5
- Signal: 5/5
- Present in the build but not switched on

### Gateway can admit Claude Enterprise callers without its own login (customer-routed inference)

Your gateway can accept Enterprise callers with Anthropic-minted tokens and run without any identity provider.

**What**

The bundled `claude gateway` gains a `cri` config block. With `cri.enabled: true` it accepts Claude Enterprise callers presenting Anthropic-minted CRI tokens, checked against an organization allowlist you supply, and the `oidc:` sign-in block becomes optional so a gateway can run with no identity provider at all.

**Details**

- `cri.org_allowlist` must list at least one Claude Enterprise organization UUID when `cri.enabled` is true, and entries are validated as UUID-shaped; startup fails otherwise.
- The `cri` section also carries a policy webhook setting.
- Discovery advertises `cri_enabled`, and responses are tagged with an `x-cri-upstream-kind` header.
- Omitting `oidc:` is only permitted for a CRI-only deployment; otherwise config validation rejects it.
- With no identity provider, the landing page prints `Identity provider (none — customer-routed inference only)`, `/oauth/callback` returns 404, discovery reports `grant_types_supported: []`, and group and scope checks read through an optional oidc block.

**Evidence**

`cri.org_allowlist must list at least one Claude Enterprise organization UUID when cri.enabled is true`, `oidc: not configured (customer-routed inference only)`, `customer-routed-inference-only deployment (cri.enabled: true)`

- Area: Gateway
- Names: `cri.enabled`, `claude gateway`
- Tier: Use it now
- Useful: 3/5
- Signal: 5/5

### Self-hosted gateway can admit callers with Anthropic-issued JWTs

The gateway can authenticate callers with Anthropic-issued JWTs verified against a remote key endpoint.

**What**

The gateway server can now accept requests authenticated by `cri+jwt`-typed ES256 tokens instead of a shared key. Turn it on with `cri.enabled: true` in the gateway config. Tokens are verified against keys fetched from `https://api.anthropic.com/api/oauth/cri`, must carry the `inference` scope and belong to an allowlisted organisation. If the keys cannot be fetched the gateway answers 503 with `jwks_unavailable`.

**Details**

- Fetched keys are cached, honouring the `cache-control: max-age` header on the key response.
- Setting `cri.jwks_url`, or an issuer other than the default, is rejected at startup unless the value is loopback and `CLAUDE_GATEWAY_ALLOW_LOOPBACK` is set.
- None of this verification code exists in 2.1.227.

**Evidence**

`cri+jwt`

- Area: Gateway
- Names: `cri.enabled`
- Tier: Use it now
- Useful: 3/5
- Signal: 5/5

### More models now show a deprecation date

More models now report a concrete deprecation date, including one named claude-mythos-preview.

**What**

The deprecation-date table gains 3.5 Haiku, Opus 4 and 4.1, Sonnet 4 aliases and snapshots, and `claude-mythos-preview`, so those models report a concrete date instead of none.

**Details**

- Example entry: `claude-opus-4-1-20250805` is dated August 5th, 2026.

**Evidence**

`"claude-opus-4-1-20250805": "August 5th, 2026"`

- Area: Models
- Names: `claude-mythos-preview`
- Tier: You'll notice
- Useful: 3/5
- Signal: 5/5

### Two new content block types are handled instead of hitting the unknown-type path

Responses can now include mid-conversation model-switch and system blocks without hitting unknown-type handling.

**What**

Responses can now contain blocks of type `fallback` (the point where the API switched models) and `mid_conv_system`. Token accounting, block dispatch and the streaming handler all handle both explicitly, counting them as "other" tokens and returning quietly rather than treating them as unknown. An additional 400-error classifier also now recognises the `effort_unsupported` upstream error class.

**Details**

- The existing `output_config` text match for that error class remains alongside the new classifier.

**Evidence**

`case "mid_conv_system":`

- Area: Streaming
- Tier: You'll notice
- Useful: 3/5
- Signal: 5/5

### Gateway gains a second admission path for machine identities

The self-hosted gateway can admit machine identities that skip managed-settings policy entirely, off by default.

**What**

The self-hosted gateway can accept callers that present an external identity token rather than a user sign-in, configured by a new `cri` block with `enabled`, `audience`, `issuer` and `org_allowlist`. It is off by default (`cri.enabled` false). When on, discovery advertises `cri_enabled: true`, audit and inference records are tagged with `upstream_kind`, and these callers skip managed-settings policy matching entirely.

**Details**

- Minted principals carry the claims `sub`, `cri: true`, `org` and `act`.
- The group and email-domain policy matcher returns no policy at all for an identity marked `cri === true`, so managed settings do not apply to these callers.
- A loopback `jwks_url` exists for tests only and has two distinct refusal messages.
- Entirely new in this build.

**Evidence**

`cri_enabled`

- Area: Gateway
- Names: `cri.enabled`
- Tier: Not switched on
- Useful: 3/5
- Signal: 5/5
- Present in the build but not switched on

### Remote sessions can be pinned to a single device

A remote session can be pinned to one device, refusing attach from anywhere else.

**What**

Attaching to a remote session now checks first whether that session is bound to one device, and registers only when egress is available, an account uuid is present and the local device id matches. Every outcome is reported as `tengu_device_bind_attach` with a result such as bound, egress, account, no_device_here, other_device, session_unbound or session_unreadable. The whole check sits behind an enable predicate; when it is false or throws, the result is `disabled`. Nothing in the build sets that predicate's effective value, so it is decided outside the source.

**Evidence**

`tengu_device_bind_attach`

- Area: Sessions
- Tier: Not switched on
- Useful: 3/5
- Signal: 5/5
- Present in the build but not switched on

### Skills synced from claude.ai

Skills from your claude.ai account can load as a source, but only if you set the sync env var.

**What**

Skills can now come from your claude.ai account as a first-class source alongside local, bundled and plugin skills. Fetching them is off unless you set `CLAUDE_CODE_SYNC_SKILLS`, or set `CLAUDE_CODE_SYNC_SESSION_REFS` with a session id latched in `CLAUDE_CODE_SESSION_ID`. Neither the source label nor the provenance value exists in 2.1.227.

**Details**

- The `/skills` picker groups them under a new source label `claude.ai sync`, and their descriptions carry a `(claude.ai sync)` suffix.
- They are deduplicated against local skills, take part in shadowing and name-collision handling, and are excluded from the bundled and plugin filters.
- The skills UI gained a separate never-invoked warning for them.
- If reconciling them against MCP-provided commands throws, the loader drops the synced skills rather than failing the load.
- In 2.1.227 the sync directory existed but its contents were never loaded.

**Evidence**

`loadedFrom: "syncedSkills"`, `claude.ai sync`

- Area: Skills
- Names: `CLAUDE_CODE_SYNC_SKILLS`, `CLAUDE_CODE_SYNC_SESSION_REFS`, `CLAUDE_CODE_SESSION_ID`
- Tier: Not switched on
- Useful: 3/5
- Signal: 5/5
- Present in the build but not switched on

### Word processor and spreadsheet skills, switched off in this build

Word-processor and spreadsheet skills that publish live editable artifacts are registered but hard-disabled.

**What**

Two new skills that publish a live, editable Artifact are registered at startup: a word-processor-style document and a spreadsheet with a formula bar, sorting and per-cell saving. Nobody can invoke them here. Their enable check is a build-time constant that always returns false, not a remote flag, so no account or setting turns them on in 2.1.228.

**Details**

- Each ships a large instruction file plus a `template.html`.
- The menu entry for the document reads "Publish a working document Artifact".
- The constant is exported as `isProductivitySkillsEnabled` and unconditionally returns false.

**Evidence**

`menuDescription: "Publish a working document Artifact"`

- Area: Skills
- Tier: Not switched on
- Useful: 3/5
- Signal: 5/5
- Present in the build but not switched on

### Memory store requests moved to a new beta header value

Memory store calls send a new beta header value, so update any proxy allowlists.

**What**

Requests to the memory store API now send the beta value `agent-memory-2026-07-22` instead of `managed-agents-2026-04-01`. If your proxy, gateway or deployment allow-lists the old value for memory traffic, it needs updating.

**Details**

- Covers create, retrieve, update, list and delete for both memories and memory stores.
- The old value no longer appears on any memory store path; roughly 99 call sites now carry the new one.
- Other managed-agent endpoints keep the older beta value.

**Evidence**

`agent-memory-2026-07-22`, `managed-agents-2026-04-01`

- Area: Memory
- Tier: You'll notice
- Useful: 4/5
- Signal: 4/5

### Publishing from a Cowork session requires a human to approve

Publishing from a Cowork session always needs your approval, and is denied outright if nobody can be asked.

**What**

Publishing from a Cowork session can no longer be auto-approved. The safety check now states that publishing sends session content to a web-reachable page, and if there is no live surface to ask a human on, the publish is denied outright with instructions not to retry it in that session.

**Details**

- Triggered by detecting a Cowork session at runtime, not by any setting you can change.

**Evidence**

`Cowork-frame publish consent requires a live human consent surface`

- Area: Permissions
- Tier: You'll notice
- Useful: 4/5
- Signal: 4/5

### Write-before-Read rule is now decided locally, not by a remote flag

Writing a file you have not read is now decided locally by model and permissions, not a server flag.

**What**

Whether Write may touch a file you have not read was previously switched by a server-side flag; that flag is gone from the build and the rule is now fixed in the client. Writing without a prior Read is allowed only for non-`.ipynb` files, only for models outside a listed set (including `claude-opus-4-5` and `claude-sonnet-4-5`), and only when reading that path is already auto-allowed by your permission rules. A file you read only as a partial view now always triggers the error.

**Details**

- The removed remote flag was `tengu_velvet_mallet`; nothing in the build reads it any more, so behaviour no longer varies by server config.
- Notebook files are excluded outright, so `.ipynb` writes always require a real Read first.
- If reading the path would itself prompt for permission, the exemption does not apply.

**Evidence**

`File has not been read yet. Read it first before writing to it.`

- Flag `tengu_velvet_mallet`: Off in both readings (read for one account on one subscription tier against v2.1.228; this account: off, anonymous baseline: off, compiled default: not a boolean we can read) These values were read against a different version of Claude Code, so treat them as the nearest reading available instead of one taken on this release.
- Area: Permissions
- Tier: You'll notice
- Useful: 4/5
- Signal: 4/5

### Bundled SDK ships a runtime for server-managed agent work

The bundled SDK carries a worker that polls a server for agent work, unreachable from normal CLI use.

**What**

The bundled Anthropic SDK gained a full worker runtime that polls a server for agent work and runs it locally. It is present in every install but only runs under managed-agents flows, which nothing in normal CLI use enters.

**Details**

- Polls, acknowledges, heartbeats and stops against `/v1/environments/{id}/work`, every request sending the header `anthropic-beta: managed-agents-2026-04-01`
- A session runner streams session events, executes tool calls, honours allow/deny confirmations from the user, and ends the turn after an idle status
- Ships a default toolset of `bash`, `read`, `write`, `edit`, `glob`, `grep`, all confined to a working directory with checks that reject paths escaping it
- None of these runtime pieces existed in 2.1.227

**Evidence**

`Cannot iterate over a consumed SessionToolRunner`

- Area: SDK
- Tier: Not switched on
- Useful: 2/5
- Signal: 5/5
- Present in the build but not switched on

### Model capability list fetched from the API

Claude Code can ask the API which models exist and store their capabilities, but the fetch is short-circuited.

**What**

A new routine asks the API for the list of models, records a capability entry for each and saves them. Two guard checks run before any network call and one of them short-circuits it entirely in this build, so no fetch happens. Failures log a single line.

**Details**

- The request can optionally carry a beta header.
- The failure line is prefixed `[modelCapabilities] fetch failed: `.

**Evidence**

`[modelCapabilities] fetch failed: `

- Area: Models
- Tier: Not switched on
- Useful: 2/5
- Signal: 5/5
- Present in the build but not switched on

### New API resources bundled but not wired to anything

The bundled client carries deployment, tunnel, webhook and "dreams" resources that nothing calls.

**What**

The bundled Anthropic client gained resource classes for deployments (with archive, pause, run and unpause), deployment runs, dreams (with archive and cancel), tunnels (with certificates, token reveal and token rotation) and webhooks. They are registered on the client and send the `managed-agents-2026-04-01` beta header, but no Claude Code command or UI path calls them in this build.

**Evidence**

`managed-agents-2026-04-01`

- Area: SDK
- Tier: Not switched on
- Useful: 2/5
- Signal: 5/5
- Present in the build but not switched on

### Slack ping origin plumbed through message handling, but nothing creates one

Messages can be tagged as coming from a Slack ping, but nothing in this build creates one.

**What**

A new message origin kind for Slack pings is wired through message display, prompt queueing, system-reminder wrapping and harness tag neutralisation, with its own equality check over channel, thread, message, Slack user, sender name and permalink. No code path in this build produces an origin of this kind.

**Details**

- A Slack origin field is passed from the inbound bridge payload into the origin classifier, which currently ignores the argument
- The classifier's two Slack observation checks are hard-coded to return false

**Evidence**

`slack-ping`

- Area: Messaging
- Tier: Not switched on
- Useful: 2/5
- Signal: 5/5
- Present in the build but not switched on

### Bundled Anthropic SDK picks up beta resources

The bundled API client gained clients for deployments, thread events, dreams, tunnels and memory-store versions.

**What**

New client resources are compiled in: deployments and session thread events with streaming (`managed-agents-2026-04-01`), dreams (`dreaming-2026-04-21`), tunnels and tunnel certificates (`mcp-tunnels-2026-06-22`), memory-store versions including redaction (`agent-memory-2026-07-22`), a binary skill-version download, and an MCP OAuth validation call on vault credentials. Messages, batches and token counting lift a user profile id field into a header.

**Evidence**

`anthropic-user-profile-id`

- Area: API Client
- Tier: Under the hood
- Useful: 2/5
- Signal: 5/5

### CLAUDE_CODE_COWORK_FRAME_ARTIFACTS forces artifact permissions to be re-asked

A new environment variable makes every artifact write and republish ask permission again instead of remembering approval.

**What**

A new environment variable, `CLAUDE_CODE_COWORK_FRAME_ARTIFACTS`, turns off the "you already approved this in this session" shortcut for artifact database writes and republishes, so each of those decisions is made fresh. It applies only on the local-agent entrypoint and only when `CLAUDECODE` is not set, and artifact actions fail closed with frame-specific denial text when a permission check cannot complete.

**Details**

- The value is captured once at startup into the process-info object and is stripped case-insensitively from the environment of child processes, so subprocesses do not inherit it.
- Off by default; nothing else in the build sets it.
- It extends the existing plan-mode artifact consent floor rather than replacing it.

**Usage**

`CLAUDE_CODE_COWORK_FRAME_ARTIFACTS=1 claude` **Evidence** `Cowork-frame artifact consent floor fails closed when its permission check cannot complete`, `CLAUDE_CODE_COWORK_FRAME_ARTIFACTS`

- Area: Artifacts
- Names: `CLAUDE_CODE_COWORK_FRAME_ARTIFACTS`
- Tier: Use it now
- Useful: 3/5
- Signal: 4/5

### Skills can be loaded from a `syncedSkills` directory

Skills can now load from a syncedSkills folder and show up labelled "synced" in your commands.

**What**

The skill loader now makes a second pass over your user settings directory, loading skills from `syncedSkills` alongside the usual `skills`. Skills from there are labelled `"synced"` as their owner in the skill list and in `/` command entries.

**Details**

- Each skill records where it was loaded from
- Synced skills have their display name and argument hint sanitized, and any `fallback` value cleared
- A pruning routine removes dynamic and conditional skills that came from `syncedSkills` once they disappear from the synced set
- The loading path is live; what populates the `syncedSkills` directory is not decided by anything in this build

**Evidence**

`syncedSkills`

- Area: Skills
- Names: `syncedSkills`
- Tier: Use it now
- Useful: 3/5
- Signal: 4/5

### A toggleable /diff panel sits behind a gate that is off by default

`/diff` would toggle a sidebar instead of opening a dialog, but the gate is off so you keep the old behaviour.

**What**

`/diff` has two behaviours in this build. With the `tengu_willow_crate` gate on, the command is described as "Toggle the diff panel showing uncommitted changes" and actually opens and closes the diff sidebar. With it off, which is the in-source fallback, the old description stays and the handler does nothing. Absent a remote config value saying otherwise, you get the old behaviour.

**Details**

- A keybinding route in the off state reports "The diff panel isn't available right now"

**Evidence**

`tengu_willow_crate`

- Flag `tengu_willow_crate`: Off in both readings (read for one account on one subscription tier against v2.1.228; this account: off, anonymous baseline: off, compiled default: on) These values were read against a different version of Claude Code, so treat them as the nearest reading available instead of one taken on this release.
- Area: Diff Panel
- Names: `/diff`
- Tier: Not switched on
- Useful: 3/5
- Signal: 4/5
- Present in the build but not switched on

### Cloud sessions can sync files back to your directory each turn

Cloud session file sync could pull changes back to your directory and narrate it, but the gate is off.

**What**

The per-turn file sync with a cloud session now pulls as well as pushes, and narrates what it did in plain language, with lines beginning "Pulled " or "Synced " and warnings such as "the session's file budget is used up" or "names that differ only by case". It runs only when the flag `tengu_violin_wood` is on, which falls back to off, and only for directories whose sync mode is `container_sync`.

**Details**

- Incoming changes that conflict with local edits are written to an `.incoming-` staging file so both copies survive.
- Two new telemetry events, `tengu_dir_sync_push` and `tengu_dir_sync_pull`, carry per-turn counts of applied rows, conflicts, stale entries, deferred rows, bytes written and case collisions.
- Pull is skipped on Windows and reports `unsupported_platform`. The user is told: "On Windows, files Claude changes in the cloud session are not synced back to this directory yet; your changes here still sync to it".

**Evidence**

`the session's file budget is used up`

- Flag `tengu_violin_wood`: Off in both readings (read for one account on one subscription tier against v2.1.228; this account: off, anonymous baseline: off, compiled default: on) These values were read against a different version of Claude Code, so treat them as the nearest reading available instead of one taken on this release.
- Area: Cloud Sessions
- Tier: Not switched on
- Useful: 3/5
- Signal: 4/5
- Present in the build but not switched on

### Diff sidebar with switchable tabs and comparison base

A diff sidebar can switch tabs and cycle its comparison base, remembered in a setting.

**What**

New code drives a side panel that toggles between a diff view and the conversation, and cycles what the diff is compared against: the session's starting point, uncommitted changes, or the branch. Both the tab switch and the base switch emit telemetry. Opening is gated on the `diffSidebarOpen` setting, needs a terminal at least 144 columns wide unless that setting is explicitly true, and reports a message when the directory is not a git repository. The chosen base persists as `diffSidebarBaseMode`.

**Evidence**

`repl_diff_base_switch`

- Area: Diff Panel
- Names: `diffSidebarOpen`, `diffSidebarBaseMode`
- Tier: Not switched on
- Useful: 3/5
- Signal: 4/5
- Present in the build but not switched on

### Artifact is disabled by its own switch, not the Workflows one

Turning off artifacts now uses the artifact switch, not the Workflows one.

**What**

Turning off artifacts now honours `CLAUDE_CODE_DISABLE_ARTIFACT` and the `disableArtifact` setting, which is what the settings schema documented. The check previously read the Workflows environment variable and setting instead.

**Details**

- If you disabled Workflows in order to suppress artifacts, artifacts are switched back on; set `disableArtifact` or the environment variable instead.

**Usage**

`export CLAUDE_CODE_DISABLE_ARTIFACT=1` **Evidence** `disableArtifact`

- Area: Artifacts
- Names: `CLAUDE_CODE_DISABLE_ARTIFACT`, `disableArtifact`
- Tier: Use it now
- Useful: 4/5
- Signal: 3/5

### Headless plugin install refreshes the plugin cache in the background

Headless plugin installs refresh the cache in the background; set CLAUDE_CODE_SYNC_PLUGIN_INSTALL to skip it.

**What**

Installing plugins in headless mode now re-fills the plugin cache after each change instead of leaving it stale, without blocking the install. Set `CLAUDE_CODE_SYNC_PLUGIN_INSTALL` to skip the background work entirely; it is also skipped when a cache-only load is already pinned.

**Details**

- The re-fill is tracked so a later pass can tell whether the current cache came from its own refresh.
- A failed refresh logs a tagged message rather than rejecting and failing the install.

**Evidence**

`headlessPluginInstall: plugins changed`

- Area: Plugins
- Names: `CLAUDE_CODE_SYNC_PLUGIN_INSTALL`
- Tier: Use it now
- Useful: 4/5
- Signal: 3/5

### Bedrock requests go through middleware, and streams become real SSE

The Bedrock backend was rebuilt so streaming and errors behave like every other provider.

**What**

The AWS Bedrock backend was rebuilt. It no longer swaps in a custom stream class; instead it rewrites each outgoing request and translates Bedrock's binary event stream into a standard server-sent-events response, so streaming and errors behave like every other backend.

**Details**

- The rewrite moves the model name into the `/model/{id}/invoke` or `/model/{id}/invoke-with-response-stream` path, lifts `anthropic-beta` header values into the request body, and SigV4-signs the result.
- Responses whose content type includes `application/vnd.amazon.eventstream` are converted to `text/event-stream`.
- Bedrock exception shapes are emitted as `event: error` frames in the stream rather than thrown as backend-specific errors.

**Evidence**

`application/vnd.amazon.eventstream`

- Area: Bedrock
- Tier: You'll notice
- Useful: 4/5
- Signal: 3/5

### Fallback models are reflected in the reported model

If a request falls back to another model, you now see the model that actually answered.

**What**

When a request falls back to a different model mid-stream, the model shown to the client now matches the one that actually answered.

**Details**

- A streamed content block of type "fallback" sets the accumulated message's model to the fallback target.
- The gateway's event re-encoder rewrites the model on the opening `message_start` event when an override model is in play.

**Evidence**

`content_block.type === "fallback"`

- Area: Models
- Tier: You'll notice
- Useful: 4/5
- Signal: 3/5

### Model-backed pre-flight check on shell commands is gone

Shell commands start faster because the safe prefix is now worked out locally.

**What**

Claude Code no longer sends a candidate shell command to a model to work out its safe prefix before running it. That means one fewer round-trip and one fewer thing that can stall or fail before a command starts. The prefix is now derived locally from the already-parsed command.

**Details**

- The removed results `command_injection_detected` and `dangerous_shell_prefix` no longer exist.
- The 10-second message "Pre-flight check is taking longer than expected" can no longer appear.

**Evidence**

`dangerous_shell_prefix`

- Area: Permissions
- Tier: You'll notice
- Useful: 4/5
- Signal: 3/5

### Reaped bridge sessions are unarchived before reattaching

Resuming an older reaped session now unarchives it first instead of failing.

**What**

On resume, a bridge session that was reaped is unarchived first and then reattached, so resuming an older session works instead of failing. Controlled by `tengu_bridge_unarchive_on_resume`, which defaults to on in this build.

**Details**

- The unarchive call is retried once through the trusted-device path.
- A 409 is read as another client having already unarchived the session, and resume continues without undoing anything.

**Evidence**

`tengu_bridge_unarchive_on_resume`

- Flag `tengu_bridge_unarchive_on_resume`: Gate removed from the code (read for one account on one subscription tier against v2.1.228; this account: on, anonymous baseline: on, compiled default: not a boolean we can read) These values were read against a different version of Claude Code, so treat them as the nearest reading available instead of one taken on this release.
- Area: Sessions
- Tier: You'll notice
- Useful: 4/5
- Signal: 3/5

### Remote control reconnects after the server forgets a session registration

If the server forgets your session registration, remote control now re-registers and resumes.

**What**

If the remote control connection closes with code 4090, meaning the session's registration on the server went stale, the client now re-registers and resumes instead of treating the session as lost.

**Details**

- Shown during reconnect as "session registration went stale — re-registering".
- Recorded with cause `epoch_stale` and, on success, recovery code `recovered_epoch_stale`.
- Teardown no longer routes this close code down the abandoned-recovery path.

**Evidence**

`session registration went stale \u2014 re-registering`

- Area: Remote Control
- Tier: You'll notice
- Useful: 4/5
- Signal: 3/5

### The model-based Bash command prefix classifier is gone

Bash permission checks no longer ask a model to figure out the command prefix.

**What**

Bash permission decisions no longer make a model call to extract a command prefix. The prompt, its `command_injection_detected` escape hatch and its example list are absent from the bundle, along with the caching class that drove it.

**Evidence**

`command_injection_detected`

- Area: Permissions
- Tier: You'll notice
- Useful: 4/5
- Signal: 3/5

### Relaunching Claude Code keeps your selected model

Restarting after an update keeps the model you picked instead of reverting to the default.

**What**

When the CLI restarts itself, it now passes `--model` with the model you had selected, so an update relaunch or a renderer switch no longer drops you back to the default model.

**Details**

- Appended next to the existing `--add-dir`, `--effort` and `--permission-mode` arguments.
- Omitted when the selection is the default, when running a mantle deployment, or when it equals the configured fallback model.
- Applies to the auto-update relaunch ("Switching from … to latest") and to `/tui` renderer switching.

**Evidence**

`--model`

- Area: Models
- Names: `--model`
- Tier: You'll notice
- Useful: 5/5
- Signal: 2/5

### Artifact auto-edit and auto-reply notices

You may see notices that an artifact auto-edit needs review, or that auto-replies paused or resumed.

**What**

Three new notification texts can appear alongside the existing "Auto-edited Artifact:" messages: one saying an artifact's auto-edit needs review, one saying auto-replies are paused, and one saying a publish re-enabled them. Each names the artifact.

**Details**

- The fixed parts are "Auto-edit on Artifact: " ... " needs review", and "Auto-replies on Artifact: " ... " are paused" / " were re-enabled by this publish".
- The artifact name is interpolated at runtime between the prefix and suffix.
- None of the three strings exist in the previous build.

**Evidence**

`Auto-replies on Artifact: `

- Area: Artifacts
- Tier: You'll notice
- Useful: 2/5
- Signal: 4/5

### Gateway can verify caller identity against an allowlist, off unless configured

The gateway can check caller tokens against an organisation allowlist when you enable it in config.

**What**

The gateway gained an optional identity-verification block named `cri`, with settings for the expected audience, an allowed-organisation list, and the key and issuer URLs used to check tokens. It only runs when `cri.enabled` is set to true in gateway config; otherwise the boot path is unchanged.

**Details**

- When enabled, the verifier is primed at startup, verified callers are tagged onto upstream requests, and responses for those callers are post-processed.
- The gateway reads an `x-gateway-upstream-kind` header off upstream responses.
- OIDC settings became optional, with the group and email claim names defaulting to `groups` and `email`.
- The two desktop-policy 404 responses now share one error-response helper.

**Evidence**

`x-gateway-upstream-kind`

- Area: Gateway
- Names: `cri.enabled`
- Tier: Not switched on
- Useful: 2/5
- Signal: 4/5
- Present in the build but not switched on

### Gateway customer-routed inference config, disabled by default

The self-hosted gateway accepts a strict customer-routed inference config block, off unless you enable it.

**What**

The gateway config schema now accepts a `cri` block for customer-routed inference, with `enabled` defaulting to false, plus audience, issuer, org allowlist and an optional policy webhook. Validation is strict: the issuer must be canonical https with no query, fragment or trailing slash, because the key URL is derived as `{issuer}/jwks.json`. Loopback issuers require `CLAUDE_GATEWAY_ALLOW_LOOPBACK`. Allowlist entries that are not UUIDs warn that every caller from that org will be denied.

**Details**

- A `cri:` block present with `enabled` not set to true produces an explicit warning that the config is inert.

**Evidence**

`cri.issuer must not end with '/'`

- Area: Gateway
- Names: `cri.enabled`, `CLAUDE_GATEWAY_ALLOW_LOOPBACK`
- Tier: Not switched on
- Useful: 2/5
- Signal: 4/5
- Present in the build but not switched on

### Laptop directory-sync sessions for remote attach

Attaching to a remote session could sync a directory on your laptop, but the gate is off here.

**What**

Attaching to or resuming a remote session can create or take over a directory-sync session on your laptop, with outbound messages held until the sync has posted. Gated by `tengu_violin_wood`, which is off in this build, so nothing changes unless the server turns it on.

**Details**

- If looking up the sync session fails, it is logged as a warning and attach proceeds without it.

**Evidence**

`tengu_violin_wood`

- Flag `tengu_violin_wood`: Off in both readings (read for one account on one subscription tier against v2.1.228; this account: off, anonymous baseline: off, compiled default: on) These values were read against a different version of Claude Code, so treat them as the nearest reading available instead of one taken on this release.
- Area: Cloud Sessions
- Tier: Not switched on
- Useful: 2/5
- Signal: 4/5
- Present in the build but not switched on

### On-disk stores can be served from a new storage backend

Config, plugin and marketplace files can be read from a new storage backend when it's provisioned.

**What**

Feedback drafts, workflow journals, the installed plugins file, marketplace manifests and catalogs, and the global config all gained a path through a v5 storage backend, taken only when that backend and a valid key are present on the call. Nothing in this build forces it on, so which path runs depends on how the backend is provisioned. Everything falls back to ordinary files otherwise.

**Details**

- Feedback drafts support listing, reading the tail in batches, and deletion.
- Workflow journals append records behind a cursor that is required to move forward, and log when it does not.
- Backend failures are logged with the error code and degrade to previous behaviour rather than throwing.

**Evidence**

`LocalFileJournal: journal read cursor failed to advance`

- Area: Internals
- Tier: Not switched on
- Useful: 2/5
- Signal: 4/5
- Present in the build but not switched on

### Server-side path for reading artifact comments, disabled by default

Artifact comments could be fetched server-side instead of through the frame host, but that switch is off.

**What**

Fetching comments on an artifact can go through a new server-side path, falling back to the existing frame-host URL flow if the server call errors. The switch is `tengu_onyx_sluice` and its built-in default is off, so unless a remote configuration value turns it on, nothing changes for you.

**Details**

- A fallback after a failed server call is recorded as `server_primary_fell_back`.

**Evidence**

`server_primary_fell_back`

- Flag `tengu_onyx_sluice`: On for this account, and not off by default (read for one account on one subscription tier against v2.1.228; this account: on, anonymous baseline: on, compiled default: on) These values were read against a different version of Claude Code, so treat them as the nearest reading available instead of one taken on this release.
- Area: Artifacts
- Tier: Not switched on
- Useful: 2/5
- Signal: 4/5
- Present in the build but not switched on

### Bundled API client: new endpoints and a stripped request field

The bundled client swapped memory-version endpoints for deployment runs and added a threads resource.

**What**

The bundled Anthropic client swapped its memory-version endpoints for `/v1/deployment_runs?beta=true`, added a `threads` resource, and now removes `user_profile_id` from message, token-count and batch-create request bodies before sending.

**Details**

- The model deprecation warning table gained `claude-opus-4-1` and `claude-opus-4-1-20250805` (August 5th, 2026) and `claude-mythos-preview` (June 30th, 2026).

**Evidence**

`/v1/deployment_runs?beta=true`

- Area: API Client
- Tier: Under the hood
- Useful: 2/5
- Signal: 4/5

### Gateway runs without an OIDC block, and names two more upstream failures

You can run claude gateway without any OIDC config; sign-in endpoints simply return 404.

**What**

`claude gateway` no longer requires OIDC configuration. With it absent, the device authorization endpoints return 404 "not found", discovery advertises no supported grant types, and the Google groups warning is suppressed. Upstream errors 413 and 501 now get readable messages.

**Details**

- Affected endpoints: `GET /device`, `POST /device`, and `/oauth/device_authorization`.
- Discovery reports `grant_types_supported: []` when OIDC is not configured.
- 413 reads "request too large for this upstream"; 501 reads "upstream does not support this endpoint".

**Evidence**

`upstream does not support this endpoint`

- Area: Gateway
- Names: `claude gateway`
- Tier: Use it now
- Useful: 3/5
- Signal: 3/5

### Managed settings accept per-OS `policyHelpers`

Admins can set a different policy helper program per operating system with a new policyHelpers setting.

**What**

Settings now take a `policyHelpers` object keyed by `macos`, `linux`, `windows` and `wsl`, alongside the existing single `policyHelper`. A bad individual entry is ignored with a status warning, but a structurally invalid `policyHelpers` object coming from an OS-admin policy source stops startup.

**Details**

- WSL falls back to the `linux` entry under some conditions.
- If no entry matches the current platform and there is no singular `policyHelper` fallback, a warning is emitted and no helper runs.

**Evidence**

`"policyHelpers" could not be parsed: expected an object mapping OS keys`

- Area: Managed Settings
- Names: `policyHelpers`, `policyHelper`
- Tier: Use it now
- Useful: 3/5
- Signal: 3/5

### New `policyHelpers` managed-settings key for per-OS policy helper executables

Set policyHelpers per platform; WSL falls back to the linux entry, then to the single policyHelper.

**What**

Administrators can now set a different policy helper executable per platform. `policyHelpers` is an object keyed by `macos`, `linux`, `windows` and `wsl`; the entry matching the current platform wins over the existing single `policyHelper`, and a platform with no entry falls back to `policyHelper`. A `wsl` machine with no `wsl` entry tries the `linux` entry first.

**Details**

- Like `policyHelper`, `policyHelpers` is honored only when it arrives from an admin-controlled policy source, not from user or project settings.
- Validation is strict: a malformed `policyHelpers` from an OS-admin policy source is fatal at startup.
- If `policyHelpers` is configured but has no entry for the current platform and no `policyHelper` is set, a warning is logged and no helper runs.

**Evidence**

`Per-OS variant of policyHelper, keyed by platform: macos, linux, windows, wsl.`

- Area: Managed Settings
- Names: `policyHelpers`, `policyHelper`
- Tier: Use it now
- Useful: 3/5
- Signal: 3/5

### SDK task events say who owns the task

SDK task_started events now include owned_by_subagent so you can tell who owns a task.

**What**

The `task_started` system event in the SDK stream now includes `owned_by_subagent`, so consumers can tell tasks a subagent owns apart from tasks the main thread owns. The session runner tracks the owned task ids alongside it.

**Evidence**

`owned_by_subagent`

- Area: SDK
- Names: `owned_by_subagent`, `task_started`
- Tier: Use it now
- Useful: 3/5
- Signal: 3/5

### Diff panel base can be cycled with ctrl+x b

Press ctrl+x b in the diff panel to cycle which revision it compares against.

**What**

The action that switches which revision the diff panel compares against now has a default key binding, `ctrl+x b`, active while the diff panel is focused. It existed in the previous build with no key attached, so it was unreachable without a custom keymap.

**Details**

- Bound in the default keymap under the `DiffPanel` context only, so the chord does nothing elsewhere.
- The action is listed as "switch diff panel base".

**Evidence**

`{ context: "DiffPanel", bindings: { "ctrl+x b": "app:cycleDiffBase" } }`

- Area: Diff Panel
- Names: `ctrl+x b`
- Tier: Use it now
- Useful: 4/5
- Signal: 2/5

### extraKnownMarketplaces merges instead of being replaced

extraKnownMarketplaces now merges across settings files instead of one layer wiping the others.

**What**

The `extraKnownMarketplaces` setting is now deep-merged across settings layers, so a project or managed-policy file can add a plugin marketplace without wiping out the ones defined at user or enterprise level.

**Details**

- Both settings-merge paths were changed; previously the highest-precedence layer replaced the whole object.

**Evidence**

`extraKnownMarketplaces`

- Area: Plugins
- Names: `extraKnownMarketplaces`
- Tier: Use it now
- Useful: 4/5
- Signal: 2/5

### AWS request signing keeps query strings

AWS requests with query parameters are now signed correctly and stop being rejected.

**What**

Requests to AWS endpoints with query parameters are now signed with those parameters included, so signatures over such URLs should match instead of being rejected.

**Details**

- The request-to-sign conversion copies the URL's search parameters into a query object rather than dropping them.
- The Bedrock credential chain helpers also accept a logger, passed to both the node provider chain and the inner client config.

**Evidence**

`fromNodeProviderChain`

- Area: Bedrock
- Tier: You'll notice
- Useful: 4/5
- Signal: 2/5

### SDK and streaming-input sessions no longer look idle while you are using them

SDK and streaming sessions no longer time out as idle while you are actively typing.

**What**

Inbound user activity over the SDK and streaming input now counts against the idle timer. Previously a session could be treated as idle while the user was actively interacting.

**Details**

- Fires on user messages, bash_command messages, and responses to permission, dialog and elicitation requests.
- Also fires on control requests including interrupt, set_model, set_permission_mode and message_rated.
- Remote workers additionally mark activity on their own idle tracker.

**Evidence**

`recordUserDrivenInbound`

- Area: SDK
- Tier: You'll notice
- Useful: 4/5
- Signal: 2/5

### Session lists no longer pick up journal or other-project entries

The resume list no longer offers journal entries or sessions from other projects.

**What**

Scanning stored transcripts for resumable sessions now skips journal entries and any entry belonging to a different project, so the list you are offered stays within the current project.

**Details**

- Entries marked `journal` are excluded outright.
- Entries whose recorded project does not match the current one are excluded.
- The lookup key is rebuilt from the current project rather than reused from the stored record.

**Evidence**

`transcript`

- Area: Sessions
- Tier: You'll notice
- Useful: 4/5
- Signal: 2/5

### The terminal UI survives a layout crash instead of exiting

A drawing crash now retries the frame instead of dropping you out of the session.

**What**

A layout pass that throws while drawing the interface is now retried immediately rather than taking the session down. If the retry also throws, that single frame is dropped and an error is reported.

**Details**

- Tracks whether the last layout failed and how many commits in a row have failed, and reports separately when failures persist across many consecutive commits.
- Repeated faults with the same message are logged once and then suppressed, under a capped budget of debug lines.

**Evidence**

`ink layout pass still throwing after many consecutive commits, frames dropped`

- Area: Terminal UI
- Tier: You'll notice
- Useful: 4/5
- Signal: 2/5

### Unparseable tool-call JSON gives a clear error instead of breaking the stream

Broken tool-call JSON now gives you a readable error instead of killing the response.

**What**

When the model emits tool input JSON that will not parse, the failure is caught while streaming, the input is replaced with an empty object, and you get a readable message quoting the bad JSON rather than the stream blowing up.

**Details**

- Caught at both the incremental delta and the end of the content block.

**Evidence**

`Unable to parse tool parameter JSON from model. Please retry your request or adjust your prompt.`

- Area: Tool Calls
- Tier: You'll notice
- Useful: 4/5
- Signal: 2/5

### Write tool now refuses files covered by a Read deny rule

Files covered by a Read deny rule can no longer be written, not just edited.

**What**

A `Read` deny rule in your permission settings now blocks writes as well as edits. Attempting to write such a file fails with "File is covered by a Read deny rule in your permission settings and cannot be written." No flag involved; this applies to everyone on this build.

**Details**

- The check runs twice: in the Write tool's permission pre-check, which returns `errorCode: 13`, and again at write time, which throws a file-state error
- Both paths use the same new message
- Previously only the Edit path checked deny rules, with a message ending "...cannot be edited."

**Evidence**

`File is covered by a Read deny rule in your permission settings and cannot be written.`

- Area: Permissions
- Tier: You'll notice
- Useful: 4/5
- Signal: 2/5

### Shared persistence and formula kit inside the doc and sheet templates

The doc and sheet templates save block by block and only claim persistence once the server accepts.

**What**

Both templates embed a common set of marked regions for saving, comments, editing and styling. Edits save back into the artifact block by block, and the save indicator only claims the change is persisted after the server accepts the commit, showing a local-only state otherwise.

**Details**

- Comments live in a hidden block inside the document itself rather than any external service.
- The spreadsheet has a formula engine with its own tokenizer and parser and no use of eval, supporting SUM, AVERAGE, MIN, MAX, COUNT, ROUND, IF, SUMIF and COUNTIF.
- It also supports tab-separated paste and copy, Ctrl/Cmd+D fill-down, and click-to-sort column letters that reorder only the current viewer's view.
- Ships disabled with the two skills.

**Evidence**

`// KIT:persist:begin \u2014 live persistence, live docs only: self.edit is the`

- Area: Artifacts
- Tier: Not switched on
- Useful: 1/5
- Signal: 4/5
- Present in the build but not switched on

### Artifact database `data/users/me` resolves to your own user id

Artifacts can address your own user record as "me" instead of looking up your id.

**What**

An artifact reading or writing the `data/users` collection can use the document id `me` and it is rewritten to the caller's real user id. The id is fetched once and cached per account, with concurrent lookups sharing one request.

**Details**

- If the artifact's published version does not declare both the database and user capabilities, the error says the artifact needs republishing rather than failing opaquely.

**Evidence**

`resolving 'me' needs this artifact's published version to declare the db and user capabilities`

- Area: Artifacts
- Names: `data/users/me`
- Tier: Use it now
- Useful: 2/5
- Signal: 3/5

### Artifact database can resolve "me" to your own user id

Artifact database paths under data/users/me are rewritten to your real user id automatically.

**What**

Paths equal to or under `data/users/me` in the artifact database tool are now rewritten to `data/users/<your id>/...`. The id is fetched by a new "whoami" request with a 15 second timeout, cached per account, with concurrent lookups sharing one request.

**Details**

- New error text covers a malformed identity response and a failure to declare the capability
- The cache means one lookup per account per session rather than one per path

**Evidence**

`data/users/me`

- Area: Artifacts
- Names: `data/users/me`
- Tier: Use it now
- Useful: 2/5
- Signal: 3/5

### Bundled SDK client reads a webhook signing key from the environment

The bundled client now reads a webhook signing key from ANTHROPIC_WEBHOOK_SIGNING_KEY.

**What**

The bundled Anthropic client constructor now takes a `webhookKey` option that defaults to the `ANTHROPIC_WEBHOOK_SIGNING_KEY` environment variable, or null when it is unset. Previously that variable appeared only in bundled documentation and no client code read it.

**Details**

- Set `ANTHROPIC_WEBHOOK_SIGNING_KEY` to have it picked up automatically; passing `webhookKey` explicitly overrides it.

**Evidence**

`ANTHROPIC_WEBHOOK_SIGNING_KEY`

- Area: SDK
- Names: `ANTHROPIC_WEBHOOK_SIGNING_KEY`, `webhookKey`
- Tier: Use it now
- Useful: 2/5
- Signal: 3/5

### Self-hosted runners wait for a just-finished background task before shutting down

Self-hosted runners hold shutdown until a finished background task is read; the grace window is configurable.

**What**

On self-hosted runners, a background task that has finished still counts as in-flight until the next turn reads its result, so drain and shutdown wait for it. The window is capped by `SELF_HOSTED_RUNNER_BG_RESULT_GRACE_MS` in milliseconds; setting it to 0 or an unusable value falls back to the default, so the hold cannot be turned off.

**Details**

- The drain-wait help text documents the new hold and the fallback behaviour.
- The session runner records that a follow-up turn is pending and logs when the wait runs past the normal retire grace period.

**Usage**

`SELF_HOSTED_RUNNER_BG_RESULT_GRACE_MS=5000 claude` **Evidence** `SELF_HOSTED_RUNNER_BG_RESULT_GRACE_MS`

- Area: Self-Hosted Runner
- Names: `SELF_HOSTED_RUNNER_BG_RESULT_GRACE_MS`
- Tier: Use it now
- Useful: 2/5
- Signal: 3/5

### Host auth token refresh hands back the full response

Host auth token refresh now returns the whole response, but only registers when an environment variable is set.

**What**

The SDK control request for a host auth token refresh now resolves the whole control response rather than pulling the token field out of it, leaving that to the callback. The refresh callback is only registered when `CLAUDE_CODE_SDK_HAS_HOST_AUTH_REFRESH` is set, so nothing happens here otherwise.

**Evidence**

`host_auth_token_refresh`

- Area: Agents
- Names: `CLAUDE_CODE_SDK_HAS_HOST_AUTH_REFRESH`
- Tier: Not switched on
- Useful: 2/5
- Signal: 3/5
- Present in the build but not switched on

### Garbage numeric env vars fall back to defaults

Nonsense values in retry, MCP output and hook timeout env vars now fall back to defaults.

**What**

`CLAUDE_CODE_MAX_RETRIES`, `MAX_MCP_OUTPUT_TOKENS` and `CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS` are now checked for being finite numbers before use. A non-numeric or infinite value falls back to the built-in default instead of corrupting the retry count, the MCP output token budget or the SessionEnd hook timeout.

**Details**

- The existing warning about a retry count being clamped to the allowed maximum is unchanged.

**Evidence**

`CLAUDE_CODE_MAX_RETRIES=${t} clamped to ${wNa}`

- Area: Configuration
- Names: `CLAUDE_CODE_MAX_RETRIES`, `MAX_MCP_OUTPUT_TOKENS`, `CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS`
- Tier: Use it now
- Useful: 3/5
- Signal: 2/5

### Skill doctor lists synced skills that were never used

Skill doctor now reports synced skills you have never actually used.

**What**

The unused-skills report adds a category for synced skills that have never been invoked, alongside the existing owned, plugin and MCP skill categories.

**Evidence**

`unusedSynced`

- Area: Skills
- Tier: Use it now
- Useful: 3/5
- Signal: 2/5

### A failed terminal layout no longer takes down the frame

A rendering error now retries once and drops a single frame instead of crashing your session.

**What**

When the layout engine throws while measuring the terminal UI, Claude Code now clears its cached layout for the whole tree and retries once. If the retry works the frame draws normally; if it throws again that single frame is dropped and the session carries on instead of crashing. This is in the normal render path for everyone on this build, not behind a flag.

**Details**

- Each distinct fault message is reported to error tracking at most once, plus a one-shot report when faults keep recurring across many consecutive frames.
- Warn-level debug lines for these faults are capped.

**Evidence**

`ink layout pass threw; immediate re-layout also threw, frame dropped`

- Area: Terminal UI
- Tier: You'll notice
- Useful: 4/5
- Signal: 1/5

### Agent SDK debug logs can go through the storage layer

SDK debug logging tags a session id and can write through the storage layer when given one.

**What**

The SDK debug logger now mints a session id of the form `sdk-<uuid>` at startup and, when the caller hands it a storage handle, appends through that instead of writing the file directly. With no handle it writes to the same debug file as before. Only runs when `DEBUG_CLAUDE_AGENT_SDK` is set.

**Evidence**

`{ namespace: "log", sessionId: Fyi, channel: "debug" }`

- Area: Agents
- Names: `DEBUG_CLAUDE_AGENT_SDK`
- Tier: Use it now
- Useful: 2/5
- Signal: 2/5

### Datadog flush interval read from a second variable name

A telemetry flush interval can now be set with either of two environment variable names.

**What**

One flush-interval call site now reads `CLAUDE_CODE_DATADOG_FLUSH_INTERVAL_MS`. The older `CLAUDE_CODE_DD_ERROR_TRACKING_FLUSH_INTERVAL_MS` is still registered and still read elsewhere, so both names are live and set whichever the call site you care about uses.

**Evidence**

`CLAUDE_CODE_DATADOG_FLUSH_INTERVAL_MS`

- Area: Telemetry
- Names: `CLAUDE_CODE_DATADOG_FLUSH_INTERVAL_MS`, `CLAUDE_CODE_DD_ERROR_TRACKING_FLUSH_INTERVAL_MS`
- Tier: Use it now
- Useful: 2/5
- Signal: 2/5

### SDK hosts can report that an auth token is unchanged

SDK hosts can say a credential is unchanged so Claude Code fails fast instead of retrying.

**What**

The `getHostAuthToken` control response gains an optional `materialUnchanged` boolean. It is only consulted when `authToken` is null, meaning the credential was delivered out of band; setting it true tells Claude Code to fail fast instead of backing off and retrying.

**Details**

- The CLI-side handler now passes an object result straight through rather than always wrapping the returned value as `{ authToken }`.

**Evidence**

`materialUnchanged is only consulted when authToken is null (out-of-band delivery)`

- Area: SDK
- Names: `getHostAuthToken`, `materialUnchanged`
- Tier: Use it now
- Useful: 2/5
- Signal: 2/5

### /diff tells you when the panel is unavailable

/diff now tells you when the diff panel can't be opened instead of silently doing nothing.

**What**

Running `/diff` when the diff sidebar cannot be opened now prints a message instead of appearing to do nothing.

**Details**

- Availability is checked first; the message is "The diff panel isn't available right now — run /diff again to see your changes".
- The existing "not a git repo" message is still checked, after the availability check.

**Evidence**

`The diff panel isn’t available right now — run /diff again to see your changes`

- Area: Slash Commands
- Names: `/diff`
- Tier: Use it now
- Useful: 3/5
- Signal: 1/5

## New Features

### Cross-session messaging between Claude Code sessions is now authenticated

Messages between your Claude Code sessions are now token-authenticated, strictly on Windows and best-effort elsewhere.

**What**

Each session now mints two random tokens for its local message inbox. The child token is passed to processes Claude Code spawns as `CLAUDE_CODE_MESSAGING_TOKEN`, and the peer token is published in a key file so other sessions can connect. On Windows the auth handshake is mandatory and connections that never send an auth frame are dropped; on macOS and Linux it is optional, and a failed key publish degrades to unauthenticated with the warning "peers will send unauthenticated".

**Details**

- Two tokens per session: a peer token for other sessions, a child token for spawned processes.
- Dropped connections are recorded under `cross_session_inbox_auth` with an `unauthed_drop` marker.
- `CLAUDE_CODE_MESSAGING_TOKEN` was added to the environment propagation allow and strip lists, so it reaches children deliberately rather than by accident.
- The startup hint line was rewritten to describe the auth handshake.

**Evidence**

`CLAUDE_CODE_MESSAGING_TOKEN`

- Area: Cross-Session Messaging
- Names: `CLAUDE_CODE_MESSAGING_TOKEN`
- Tier: You'll notice
- Useful: 3/5
- Signal: 3/5

### Skills synced from claude.ai keep out of the way of your local ones

Skills synced from claude.ai are skipped when their name, path or alias collides with your local skill.

**What**

Skills pulled down from claude.ai are now checked against your local skills before loading. If a synced skill's name, folder path or any of its aliases matches one a local skill already claims, the synced copy is dropped and the local one wins.

**Details**

- Synced skill names containing a colon, or starting with `mcp__`, are refused outright.
- Favoriting a synced skill stores it under its own `synced-skill:` key, falling back to the plain `skill:` key so existing favorites keep working.
- The reminder about skills you have loaded but never used now has a separate line for synced skills, and points out that deleting a synced skill locally just brings it back on the next sync.

**Evidence**

`synced from claude.ai loaded but never invoked`

- Area: Skills
- Tier: You'll notice
- Useful: 3/5
- Signal: 3/5

### Rate-limit state is published to remote sessions

Remote viewers of your session now see the same rate-limit state you do.

**What**

Rate-limit updates are now attached to session metadata as `rate_limit_info` for bridged and print-mode sessions, so a remote viewer sees the same limit state as the local session.

**Details**

- Publishes are de-duplicated per target, and a window that has already expired is cleared rather than sent.
- The publish is wrapped so a failure only logs instead of breaking the rate-limit listener.

**Evidence**

`rate_limit_info`

- Area: Sessions
- Tier: You'll notice
- Useful: 3/5
- Signal: 3/5

### Peer session listings include environment and connection state

Peer session lists now show which environment each session runs in and whether it is connected.

**What**

Listed peer sessions can now report which kind of environment they run in and whether they are currently connected, in addition to title, status and last-updated time.

**Details**

- `connected` is true when the server reports `connected` and false when it reports `disconnected`.
- Rows that fail validation are skipped and logged.

**Evidence**

`[bridge:peers] skipping malformed session row: `

- Area: Cross-Session Messaging
- Tier: You'll notice
- Useful: 2/5
- Signal: 3/5

### Gateway warns when no OIDC is configured and several sections are dead

The gateway now tells you which configured sections do nothing because sign-in was never set up.

**What**

If the gateway runs without OIDC it can never mint a session JWT, which silently disables managed policies, admin spend limits and telemetry forwarding. The gateway now names exactly which of those configured sections are inert and suggests either removing them or configuring OIDC.

**Evidence**

`oidc is not configured (CRI-only deployment), so no gateway session JWT can ever be minted`

- Area: Gateway
- Tier: You'll notice
- Useful: 2/5
- Signal: 2/5

### Local messaging inbox publishes an auth key so peers can prove who they are

Your local message inbox publishes a key file so other sessions can prove who they are.

**What**

The local socket other Claude Code processes use to send you messages now writes an auth key file and exports `CLAUDE_CODE_MESSAGING_TOKEN` to child processes. On platforms where auth is required, a failure to publish the key shuts the inbox down rather than accepting unauthenticated peers.

**Details**

- The key file is written after the socket permissions are set.
- Shutdown in the hard-failure case is recorded with cause `key_publish_failed`.
- Where the platform treats auth as optional, the failure logs a warning and the inbox keeps running with peers sending unauthenticated.

**Evidence**

`refusing to run an inbox no peer can authenticate to`

- Area: Cross-Session Messaging
- Names: `CLAUDE_CODE_MESSAGING_TOKEN`
- Tier: Under the hood
- Useful: 2/5
- Signal: 2/5

### SDK client gains fetch middleware and wider streaming event coverage

The bundled API client can wrap network calls and understands more streaming event types.

**What**

The bundled API client can now wrap its own network calls in middleware, and its streaming event filter passes through many more event types. Middleware that reads the response body is rejected with an error telling you to clone it or return a replacement.

**Details**

- Middleware runs around fetch and receives a parse context.
- The rejection message is exact: middleware must use `response.clone()` to inspect the body, or `return new Response(body, response)` to consume and replace it.
- Newly forwarded streaming events include `session.updated`, outcome-evaluation spans, agent thread messages, session thread status transitions, `event_start`/`event_delta` and `system.message`.
- New client namespaces: deployments, deployment runs, webhooks, dreams and tunnels.

**Evidence**

`middleware consumed the response body; use response.clone() to inspect it, or return new Response(body, response) to consume and replace it`

- Area: SDK
- Tier: Under the hood
- Useful: 2/5
- Signal: 2/5

### SDK system messages report background tasks and turn context

Programs driving Claude Code through the SDK now see more detail about background work and turn context.

**What**

Programs driving Claude Code through the SDK get more detail about work running in the background.

**Details**

- A new `background_tasks_changed` system message carries the list of active task ids.
- Task status updates include `is_backgrounded`; `task_notification` gains a `status`.
- `turn_starting` now carries a `mode`, plus the `task_id` when that mode is `task-notification`.

**Evidence**

`background_tasks_changed`

- Area: SDK
- Tier: Under the hood
- Useful: 2/5
- Signal: 2/5

## Improvements

### A policy helper is only honoured from an admin settings source

A policyHelper only runs if it comes from admin settings; anywhere else it is ignored.

**What**

A `policyHelper` configured through any non-admin settings source is now logged and dropped instead of being executed.

**Evidence**

`policyHelper ignored: delivered via non-admin source`

- Area: Settings
- Names: `policyHelper`
- Tier: You'll notice
- Useful: 3/5
- Signal: 3/5

### Artifact actions in Cowork sessions are denied outright when nobody can approve them

In Cowork sessions, artifact writes and comment replies are denied outright when nobody can approve.

**What**

Writing to the artifact database, replying to comments and resolving comment threads used to raise an approval prompt. In a Cowork session with no one present to answer it, they now return a denial that tells the model not to retry during that session.

**Details**

- Where an approval card can still be shown on Cowork surfaces, database write approval no longer lasts the rest of the session; it asks again on every write.
- Resolving a comment thread used to be allowed automatically and now asks on Cowork surfaces.

**Evidence**

`Database writes from this Cowork session need the approval card, and no one can answer it in this session.`

- Area: Artifacts
- Tier: You'll notice
- Useful: 3/5
- Signal: 3/5

### Device tools reject calls tagged with someone else's cloud session

Tools on your machine refuse calls from a cloud session other than the one being served.

**What**

When a cloud session calls a tool on your machine with a `session_id` that does not match the session the device is actually serving, the call is refused with a result explaining the machine is busy with another session and telling the model not to retry in a loop.

**Details**

- Refusals are reported as `tengu_device_tool_refused` with reason `session_mismatch`.
- Device tools now carry an `analyticsName` used in that reporting.

**Evidence**

`tengu_device_tool_refused`

- Area: Cloud Sessions
- Tier: You'll notice
- Useful: 3/5
- Signal: 3/5

### Skill loading refuses to merge synced skills after a local failure

If local skills fail to load, account-synced skills are dropped rather than merged in unchecked.

**What**

If your local skill directories or plugin skills fail to load, skills synced from your account are now dropped rather than merged in, because their names cannot be checked for collisions against the ones that failed.

**Details**

- The loader result carries a flag set when either the skill-directory or plugin-skill load threw, and set true in the total-failure fallback.
- The consequence is fewer skills available in that session rather than a silent name clash.

**Evidence**

`local or plugin skills failed to load, so its name cannot be checked against them`

- Area: Skills
- Tier: You'll notice
- Useful: 3/5
- Signal: 3/5

### Subagent messages no longer count as turns in tee'd stream-json output

Subagent chatter in the JSON output stream no longer counts as a new turn.

**What**

In the JSON event stream written to stdout for external observers, user and assistant events produced by a subagent are now marked as such. The supervisor that tracks turn boundaries skips marked events, so a subagent talking no longer opens a follow-up turn or resets idle and activity tracking.

**Details**

- The marker is applied to events that carry a parent tool-use id, meaning they came from a tool-spawned subagent rather than the main conversation.
- Both the code that adds the marker and the guard that ignores it are new.

**Evidence**

`from_subagent`

- Area: SDK
- Tier: You'll notice
- Useful: 3/5
- Signal: 3/5

### Downloaded skill archives are inspected before extraction

Downloaded skill archives are checked before unpacking so they cannot write outside the skills folder.

**What**

The path that downloads a session's skills now checks the archive before unpacking it, so a malicious or malformed bundle cannot write outside the skills directory.

**Details**

- Zip versus tar is detected by magic bytes rather than filename.
- The archive is listed first and refused outright if any member has an absolute path or a `..` component, or is a symlink, hardlink or device entry.
- Extraction goes to a staging directory, and a skill whose name would resolve outside the `skills` directory is refused.
- The skill version must resolve to a concrete numeric version before the download starts.
- Reached only through the environment worker's skill setup.

**Evidence**

`refusing to extract archive with symlink/hardlink/device member`

- Area: Skills
- Tier: You'll notice
- Useful: 3/5
- Signal: 3/5

### Synced skills can no longer take a slash-command name that sometimes exists

Synced skills can no longer take over slash-command names that only exist in some states.

**What**

Slash commands that only appear in certain states (fleet fork, fleet background, daemon, skill doctor, logout) now reserve their names permanently. A skill synced from your account that collides with one of those names, or with a local or plugin command, is dropped and logged instead of shadowing it.

**Details**

- Each conditional command set now declares both the names it uses when open and the names it uses when closed, and every name from both lists is reserved.
- If local or plugin skills fail to load at all, every synced skill is dropped, because there is no way to check its name for collisions.

**Evidence**

`a non-synced command or an earlier synced skill owns that name`

- Area: Slash Commands
- Tier: You'll notice
- Useful: 3/5
- Signal: 3/5

### Artifact database writes in a Cowork session always ask you

Every artifact database write in a Cowork session asks you; automatic approval cannot cover it.

**What**

Writing to the artifact database from a Cowork session is now a safety check that the automatic permission classifier is not allowed to approve on your behalf. Previously only the first write in a session, and a stricter prompt in plan mode, needed confirmation.

**Details**

- Applies only when the session is a Cowork session; other sessions keep the existing first-write confirmation.
- The reason given is that these writes are durable changes to shared state.

**Evidence**

`Artifact database writes from a Cowork session are durable shared-state changes`

- Area: Permissions
- Tier: You'll notice
- Useful: 3/5
- Signal: 3/5

### Scheduled routines are told their notification is the whole delivery

Scheduled routines now write notifications where the first sentence is your phone banner.

**What**

The prompt for a scheduled routine run now says the notification is how the run reaches its owner, and requires the message to be wrapped in tags where the first sentence becomes the phone banner and the full text becomes the email body.

**Evidence**

`This is a scheduled routine \u2014 the notification is how the run reaches its owner.`

- Area: Scheduled Routines
- Tier: You'll notice
- Useful: 3/5
- Signal: 3/5

### Lookalike skill names can no longer shadow each other

Skills with visually identical names can no longer sneak past collision checks.

**What**

Skill and command name collisions are compared after normalizing confusable characters, so a synced skill with a visually identical name can no longer slip past the check that drops it in favour of a local skill of the same name.

**Details**

- Names are put through NFKC, then separators, control characters, format characters, default-ignorable code points and the braille blank U+2800 are stripped.
- All dash punctuation and the minus sign fold to `-`; five colon lookalikes (U+A789, U+2236, U+0589, U+05C3, U+02D0) fold to `:`.
- Case folding is applied last.

**Evidence**

`/[\ua789\u2236\u0589\u05c3\u02d0]/g`

- Area: Skills
- Tier: You'll notice
- Useful: 3/5
- Signal: 3/5

### Subagents doing delegated observation no longer trigger compact hooks

Observation subagents no longer fire your PostCompact hooks when they compact.

**What**

When a subagent set up for delegated observation compacts its context, your `PostCompact` hooks are skipped and `PreCompact` hook output is limited to whether compaction was blocked. Compaction in the main session and in ordinary subagents runs hooks as before.

**Details**

- `PostCompact` returns immediately for these agents.
- `PreCompact` still runs, but any custom instructions or user-facing message the hook returns are discarded.

**Evidence**

`hook_event_name: "PostCompact"`

- Area: Hooks
- Names: `PostCompact`, `PreCompact`
- Tier: You'll notice
- Useful: 3/5
- Signal: 3/5

### API rejections are classified from a structured marker instead of prose alone

Request rejections are now identified from a structured marker rather than guessing from error wording.

**What**

Claude Code now reads a `capability_rejected: ` marker with a reason token out of 400 and 413 responses, rather than only lowercasing the error text and searching it for phrases. Token matching uses a word boundary, so `image_block` no longer matches `image_blockade`. Prose matching remains as a fallback, so nothing changes against servers that do not send the marker.

**Details**

- Reason codes include prompt_too_long, mid_conv_system, cache_control_field, thinking_signature, effort_unsupported, media_budget, max_tokens_context_overflow, and per-beta-header entries.

**Evidence**

`capability_rejected: `

- Area: API Errors
- Tier: Under the hood
- Useful: 2/5
- Signal: 3/5

### Gateway protocol document now matches the deployment

The gateway's protocol document is now generated to match how your deployment is actually configured.

**What**

The gateway used to serve one fixed protocol document at `/protocol`. It is now generated per deployment: the wording branches on whether an identity provider is configured, and a customer-routed-inference section covering token verification, forwarding rules and the `capability_rejected` error class is appended when CRI is on. Without an identity provider, the document notes that sign-in routes return 404.

**Evidence**

`text/markdown; charset=utf-8`, `# Claude Code gateway protocol`

- Area: Gateway
- Names: `/protocol`
- Tier: You'll notice
- Useful: 2/5
- Signal: 3/5

### Same title rule now repeated across artifact skills and the publish tool

The same short-title rule now applies across artifact skills, PR review pages and publishing.

**What**

Four more places carry the same rule: name an artifact like a product, not a caption. The artifact-design skill gains a paragraph on it, the workshop skill applies it to the template's `<title>`, the PR-review template's title slot changes, and the publish tool's `title` parameter description repeats the instruction.

**Evidence**

`Name the page like a product, not a caption.`

- Area: Artifacts
- Tier: You'll notice
- Useful: 2/5
- Signal: 3/5

### Gateway config now rejects unsafe customer-routed-inference setups

Gateway config refuses to start with an empty audience or organization allowlist.

**What**

Operator config parsing fails a deployment that enables customer-routed inference with an empty audience list or an empty organization allowlist. `oidc` is now required unless customer-routed inference is enabled.

**Details**

- The rejection messages spell out the reason: audience binding is what stops a token minted for another organization's gateway from being replayed against yours.

**Evidence**

`oidc is required \u2014 it may be omitted only for a customer-routed-inference-only deployment (cri.enabled: true)`

- Area: Gateway
- Names: `oidc`
- Tier: You'll notice
- Useful: 2/5
- Signal: 3/5

### Agent list explains that names are how you address an agent

Claude gets clearer guidance on addressing agents by exact name across sessions and machines.

**What**

The description of the agent-listing tool was rewritten to spell out the four kinds of row it can return: subagents running inside this session, other sessions on this machine, cloud sessions, and Remote Control sessions on other machines. It states that a row's name is the address to send to, that it must be copied exactly, and that the short reference in brackets should only be added when two rows share a name.

**Details**

- Part of the text is filled in at runtime with the actual name of the send tool.

**Evidence**

`each row labeled by kind. Names are the address`

- Area: Agents
- Tier: You'll notice
- Useful: 2/5
- Signal: 3/5

### Published artifact bundles rejected when they contain frame-runtime markers

Artifact bundles containing frame-runtime markers are now refused at publish time.

**What**

The check that runs before an artifact bundle is published now also refuses bundles containing `<!-- frame-runtime -->` and `<!-- /frame-runtime -->` markers, joining the existing refusals for mermaid and chart-runtime markers.

**Details**

- Rejection message: `bundle contains a frame-runtime serve sentinel`.

**Evidence**

`bundle contains a frame-runtime serve sentinel`

- Area: Artifacts
- Tier: You'll notice
- Useful: 2/5
- Signal: 3/5

### Synced skills no longer have file attachments extracted, except when remote

Account-synced skills no longer have file attachments pulled out, except on remote or Cowork runs.

**What**

Skills that arrive through skill syncing now skip file-attachment extraction from their content, matching how skills from MCP servers and the memory store already behaved. The exception is remote or cowork runs, where extraction still happens.

**Details**

- The remote check is `CLAUDE_CODE_REMOTE` or `CLAUDE_CODE_IS_COWORK`; either being set restores extraction for synced skills.
- The previous rule was a per-source list that skipped extraction only for `mcp` and `memoryStore` skills.

**Evidence**

`if (e.loadedFrom === "syncedSkills") return !Jy_();`

- Area: Skills
- Tier: You'll notice
- Useful: 2/5
- Signal: 3/5

### Oversized media in a request is now its own rejection class

Sending too many images or documents now reports a clear media-budget error and retries without them.

**What**

A 400 whose message mentions too much media is classified as a media budget failure; the retry path strips both document and image blocks, and the error surfaces to you as `media_budget_exceeded`. Previously this was matched ad hoc against two literal substrings.

**Details**

- The surrounding classifiers now take a message string rather than an Error object.

**Evidence**

`media_budget_exceeded`

- Area: API Errors
- Tier: You'll notice
- Useful: 3/5
- Signal: 2/5

### Plugin installs can no longer explode through symlinks

Plugin installs stop at 20,000 files or 512MB so symlink loops cannot fill your disk.

**What**

Copying a plugin directory now runs against a budget of 20,000 entries and 512 MB. A plugin that materialises more files than that through symlinks fails with "Plugin materializes more files through symlinks than the copy budget allows" instead of filling the disk.

**Details**

- The copier takes an explicit dereference flag and threads a shared budget accumulator through the walk.
- Symlinks pointing at ancestor directories and cyclic targets are skipped and logged rather than followed.

**Evidence**

`Plugin materializes more files through symlinks than the copy budget allows`

- Area: Plugins
- Tier: You'll notice
- Useful: 3/5
- Signal: 2/5

### Bursts of artifact notifications collapse into one notice

Rapid-fire artifact notifications now collapse into one notice instead of arriving individually.

**What**

Repeated auto-reply, auto-edit and artifact-changed notifications for the same artifact are now merged into a single queued notice with a settle timer and a maximum settle window, instead of arriving one by one.

**Details**

- The merged notice counts how many distinct comment threads were involved.
- Only the newest few detail bodies are kept, with a line saying how many older ones were dropped.
- The disclosure shown when the turn stops records what was discarded.

**Evidence**

`earlier ${r.droppedDetails === 1 ? "notice" : "notices"} in this burst omitted`

- Area: Artifacts
- Tier: You'll notice
- Useful: 3/5
- Signal: 2/5

### Bridge reconnects re-register after a conflict, up to a limit

After a network drop, sessions re-register automatically, but only a limited number of times per hour.

**What**

After a network interruption, a bridge session that comes back to a conflict now re-registers instead of giving up, but no more than a fixed number of times per hour. Past that it stops with an explicit message.

**Details**

- Worker registration uses compare-and-swap fencing so a stale connection cannot overwrite a newer one.
- If another connection has genuinely taken the session over, this device stands down and says so rather than retrying.

**Evidence**

`another connection took over this session while it was being restored \u2014 this device is standing down`

- Area: Sessions
- Tier: You'll notice
- Useful: 3/5
- Signal: 2/5

### A failing checkout hook skips that repo instead of ending the session

A failing checkout hook now skips that repo and cleans up instead of ending the session.

**What**

If a checkout hook fails for a repository that has no push target configured, the runner now skips that source and carries on, cleaning up the partial checkout, rather than aborting the whole session.

**Details**

- If the partial checkout directory cannot be deleted, or its name collides with a prepared checkout only by letter case on a case-insensitive filesystem, it is left in place and you are warned that its contents may still be visible. Removal is retried when the session ends.
- Hook failures now carry the hook's exit code.

**Evidence**

`CheckoutHookFailedError`

- Area: Self-Hosted Runner
- Tier: You'll notice
- Useful: 3/5
- Signal: 2/5

### Settings problems split into notices and genuinely invalid entries

Minor settings problems now show only in /status instead of padding the invalid-settings list.

**What**

Settings validation can now mark a problem as informational or as fatal at startup. Informational ones show up in `/status` but no longer pad out the "Invalid settings" list or the invalid-entries dialog; fatal ones are logged at error level.

**Details**

- Validation callbacks flag an issue as status-only or startup-fatal; status-only issues are collected separately as status notices.
- The invalid-entries dialog and the "Invalid settings" list now contain only real validation failures.

**Evidence**

`statusNotices`

- Area: Settings
- Names: `/status`
- Tier: You'll notice
- Useful: 3/5
- Signal: 2/5

### Auto-reply stop notices carry an explanation

The notice that artifact auto-replies stopped now explains why and lists discarded replies.

**What**

The notification telling you artifact auto-replies have stopped was a single long line; it is now a short summary with a separate body holding the reason and a list of any queued replies that were discarded.

**Details**

- The notification can be marked as carrying a discard record when stale queued rows were dropped, so downstream display can call that out.

**Evidence**

`Artifact auto-replies are disarmed for the rest of this session`

- Area: Artifacts
- Tier: You'll notice
- Useful: 3/5
- Signal: 2/5

### Remote Control session list shows offline peers

Remote Control lists now say "offline" and "waiting on a human" instead of raw statuses.

**What**

In the Remote Control session list, a disconnected peer now reads "offline" and a session needing input reads "waiting on a human" rather than a raw status value.

**Details**

- The footnote shown when the list may be incomplete now says "account session list" instead of interpolating a label.

**Evidence**

`(account session list incomplete just now`

- Area: Remote Control
- Tier: You'll notice
- Useful: 3/5
- Signal: 2/5

### More background notifications carry their task id

More background notifications now carry a task id, so you get fewer duplicate alerts.

**What**

Background shell completion, subagent finish, artifact watch lifecycle and observer notifications now attach the task id when queuing a notification. The queue already used that id to avoid notifying the same task twice and to release keepalives, so you should see fewer duplicates.

**Evidence**

`task-notification`

- Area: Terminal UI
- Tier: You'll notice
- Useful: 3/5
- Signal: 2/5

### Artifact titles must read as names, not summaries

Artifact titles are now short name-like phrases, with the explanation moved to the description.

**What**

The Artifact tool's instructions to the model now require a `<title>` to be a short, distinctive noun phrase, with no generic category label and no name followed by an explainer after a dash or colon. The explanation belongs in the `description` parameter instead, so published artifacts and gallery cards get shorter, name-like titles.

**Evidence**

`never a name plus an appended explainer after a dash or colon`

- Area: Artifacts
- Tier: You'll notice
- Useful: 3/5
- Signal: 2/5

### Auto mode description drops the cost line on paid plans

Pro, Max and Team accounts no longer see the "costs slightly more" line on Auto mode.

**What**

The description of Auto mode is now two strings. Pro, Max and Team accounts see it without the sentence about sessions costing slightly more; everyone else still sees that sentence.

**Evidence**

`Sessions are slightly more expensive.`

- Area: Models
- Tier: You'll notice
- Useful: 3/5
- Signal: 2/5

### Workflow phases are announced only when reached

Workflow progress updates appear as each phase starts rather than flooding you up front.

**What**

Progress events for workflow phases are now emitted when a phase is actually reached rather than when it is first registered, so listing phases up front no longer floods the display with progress updates.

**Details**

- Phase indices are still assigned on first sight; a separate flag records whether the phase has been announced.

**Evidence**

`workflow_phase`

- Area: Workflows
- Tier: You'll notice
- Useful: 3/5
- Signal: 2/5

### Repeated artifact auto-reply notices collapse into one line

Repeated auto-replies to one artifact now show a single summary line naming that artifact.

**What**

When Claude auto-replies to the same artifact several times, you now get a single summary line naming that artifact instead of a generic "tasks completed" rollup.

**Details**

- The auto-reply notice carries a grouping key made of the family name `auto-replied` plus the artifact name and thread id, so only notices for the same artifact merge.
- Detail text is preserved alongside the grouping key.

**Evidence**

`more auto-reply `

- Area: Artifacts
- Tier: You'll notice
- Useful: 3/5
- Signal: 2/5

### Self-hosted runner waits for the turn that follows a background task

A session still owing a report after a background task stays alive instead of being treated as idle.

**What**

A session whose background task has finished but which still owes one more turn to report the result now counts as waiting on you, so the runner keeps it alive instead of treating it as idle.

**Details**

- Appears in the `[runner:session]` debug log as "finished background task awaiting the follow-up turn", next to the existing live-task and wakeup reasons.
- Driven by a new callback the session uses to declare that a follow-up turn is pending.

**Evidence**

`finished background task awaiting the follow-up turn`

- Area: Self-Hosted Runner
- Tier: You'll notice
- Useful: 3/5
- Signal: 2/5

### `api-key` header is now redacted in logs

An API key sent in the api-key header is now masked in logs.

**What**

Header dumps mask `api-key` alongside `authorization`, `x-api-key`, `cookie` and `set-cookie`, so an API key passed in that header no longer appears in logs.

**Details**

- The socket messaging mismatch warning also passes the message type and session id through a sanitizer before logging.

**Evidence**

`t.toLowerCase() === "api-key"`

- Area: Internals
- Tier: You'll notice
- Useful: 3/5
- Signal: 2/5

### Artifact watch endings now show up as task notifications

When an artifact watch ends you now get a proper task notification with its title and reason.

**What**

When a live artifact watch is dropped, you get a task notification instead of a plain housekeeping message. It carries a short reason, such as one saying the watch was stopped to make room for a newer one, and shows the watch's own title as the name.

**Details**

- The notification type is `artifact-watch-lifecycle`, and it carries both the existing long explanation and a new short reason field.
- The two paths that can end a watch each supply their own short reason.

**Evidence**

`made room for a newer watch`

- Area: Artifacts
- Tier: You'll notice
- Useful: 3/5
- Signal: 2/5

### Cloud sessions are labelled "in the cloud"

Cloud sessions now read "in the cloud" instead of the misleading other-machine label.

**What**

Sessions running in a cloud environment are now listed as "in the cloud" in the session directory instead of "on another machine (Remote Control)", which was misleading for sessions that were never on another machine.

**Details**

- Applies to bridge sessions whose environment is set to something other than "bridge".
- Session listings also now return status and tags alongside the title and timestamps.

**Evidence**

`in the cloud`

- Area: Sessions
- Tier: You'll notice
- Useful: 3/5
- Signal: 2/5

### IPC diagnostics redact tokens and long hex strings

Local messaging logs now hide tokens and long hex strings instead of printing them.

**What**

Log output from the local messaging transport now replaces runs of 32 or more hex characters with a `<hex:...>` placeholder, and refuses to print any fragment whose text matches `/token/i`, substituting a withheld marker instead. Applied to peer addresses and error fragments.

**Evidence**

`(redacted: fragment may carry an auth token)`

- Area: Internals
- Tier: Under the hood
- Useful: 2/5
- Signal: 2/5

### The gateway now requires a model in the request body

The self-hosted gateway now rejects requests that omit a model with a 400.

**What**

A request with no `model` field, or an empty one, is now a 400. Previously a missing model was tolerated and only type-checked when present. The allowlist rejection message also escapes the model name before echoing it back.

**Evidence**

`model is required`

- Area: Gateway
- Tier: You'll notice
- Useful: 2/5
- Signal: 2/5

### Terminal titles can no longer be dressed up as URLs

Terminal titles are sanitised so text cannot disguise itself as a real URL.

**What**

The text Claude Code writes into your terminal title bar is normalized to NFC before control code points are turned into spaces, enclosing-marker and circle glyphs are collapsed, long titles are truncated with an ellipsis, and `://` is rewritten with a lookalike slash so a title cannot read as a real URL.

**Details**

- Normalization runs first, so composed and decomposed forms of the same character are sanitized identically.

**Evidence**

`normalize("NFC")`

- Area: Terminal UI
- Tier: You'll notice
- Useful: 2/5
- Signal: 2/5

### Gateway log lines escape control characters

Gateway log lines escape control characters so request text cannot forge fake log entries.

**What**

Every `[gateway]` line written to stderr now passes through an escaper that converts newlines, carriage returns, tabs, C0 and C1 controls and line separators into `\n`, `\r`, `\t` or `\uXXXX`, so text coming from a request cannot forge extra log lines.

**Evidence**

`lxH = { "\n": "\\n", "\r": "\\r", "\t": "\\t" };`

- Area: Gateway
- Tier: Under the hood
- Useful: 2/5
- Signal: 2/5

### Transcript cleanup prunes a new sidecar file and refuses risky directory names

Cleanup now deletes leftover per-directory sync files so they stop piling up next to transcripts.

**What**

Retention cleanup now recognises and deletes the per-directory sync-state file written next to a transcript as `<name>.dir-sync.json`, along with its temporary variants, so these no longer accumulate.

**Details**

- The check made before recursively removing a session's companion directory was tightened. It previously only rejected empty, `.` and `..` names.
- It now also rejects names containing path separators, reserved or device-like names, names ending in a dot or space, and names ending in `.jsonl`.

**Evidence**

`.dir-sync.json`

- Area: Sessions
- Tier: You'll notice
- Useful: 2/5
- Signal: 2/5

### Gateway strips detail out of upstream errors before returning them

The self-hosted gateway now strips detail from upstream errors before handing them back.

**What**

The self-hosted gateway no longer passes upstream error bodies through as-is. It drops the `anthropic-organization-id` header, reads at most 64KB of the body, keeps only error types on a fixed allowlist, and substitutes a generic message. A small set of gateway headers is forwarded, and `retry-after` survives on 429s.

**Details**

- Status-to-type mapping gained 413 request_too_large and 501 not_supported.
- Model names appearing in operator-facing errors are stripped of non-printable characters and truncated to 128 characters.
- Only reached on the gateway request path for customer-routed inference.

**Evidence**

`upstream error`

- Area: Gateway
- Tier: You'll notice
- Useful: 2/5
- Signal: 2/5

### Messaging refusals name the actual sources

Teammate messaging errors now name which sources were checked when a recipient cannot be verified.

**What**

The messages the teammate messaging tool shows Claude when it cannot verify or disambiguate a recipient now say plainly which sources were checked, and the impersonation warning no longer assumes the sessions share a name.

**Evidence**

`Your account's other sessions (Remote Control and cloud) could not be checked just now`

- Area: Messaging
- Tier: You'll notice
- Useful: 2/5
- Signal: 2/5

### Combining marks stripped from input text

Input text with stacked combining marks is now flattened before it reaches you.

**What**

Text sanitization now normalizes to NFC and replaces combining diacritical marks with spaces, which is the path that defends against zalgo-style or invisible-mark text.

**Details**

- Covers U+0300–U+036F plus the other combining blocks (U+0483–U+0489, U+1AB0–U+1AFF, U+1DC0–U+1DFF, U+20D0–U+20F0, U+A66F–U+A67D, U+FE20–U+FE2F) and two further character classes.
- Runs before the existing cleanup.

**Evidence**

`[\u0300-\u036F\u0483-\u0489\u1AB0-\u1AFF\u1DC0-\u1DFF\u20D0-\u20F0\uA66F-\uA67D\uFE20-\uFE2F]`

- Area: Elsewhere
- Tier: You'll notice
- Useful: 2/5
- Signal: 2/5

### Checkout hook failure reports a structured reason

A checkout hook that leaves no repository now reports its exit code and reason.

**What**

In the self-hosted runner, a checkout hook that exits successfully but leaves no repository behind now raises a typed error carrying the exit code and the reason "exited 0 but left no .git in the checkout directory", instead of a plain error with only the long message.

**Evidence**

`exited 0 but left no .git in the checkout directory`

- Area: Self-Hosted Runner
- Tier: Under the hood
- Useful: 2/5
- Signal: 2/5

### Sessions bound elsewhere get a live writer only

A session already claimed by another client gets live updates only, with no history replay.

**What**

A session already bound to another client now skips history backfill and installs only a live writer, the same treatment as a cross-account veto.

**Details**

- The log reads "[bridge:repl] Persistence backfill suppressed (cross-account veto or foreign binding) — installing live writer only".
- Backfill suppression callbacks now receive a payload and ignore cases flagged as uncertainty only.

**Evidence**

`[bridge:repl] Persistence backfill suppressed (cross-account veto or foreign binding) — installing live writer only`

- Area: Sessions
- Tier: Under the hood
- Useful: 2/5
- Signal: 2/5

### Oversized plans dropped from the session snapshot

Very large plan files are left out of session snapshots instead of bloating them.

**What**

A plan file larger than the snapshot size cap is now skipped with a debug log rather than included, the same way an oversized workshop doc has always been handled.

**Details**

- The new log names the plan's character count and says it is being skipped.

**Evidence**

`Workshop doc over snapshot cap (${n.length} chars), skipping`

- Area: Sessions
- Tier: You'll notice
- Useful: 2/5
- Signal: 2/5

### Gateway OAuth endpoints return 404 when sign-in is not configured

Gateway sign-in endpoints return a flat 404 when no identity provider is configured.

**What**

On the self-hosted gateway, `POST /oauth/device_authorization` and `POST /oauth/token` now answer with a plain 404 `not found` when the OIDC identity-provider config is absent, before any rate-limiting or form parsing. Previously the handlers ran regardless.

**Details**

- This matches the discovery document, which already omitted both endpoints unless OIDC config was present.
- The guard reads a config precondition; the exact config key it checks is not fully pinned down in the build.

**Evidence**

`/oauth/device_authorization`

- Area: Gateway
- Tier: You'll notice
- Useful: 2/5
- Signal: 2/5

### Republished PR reviews keep their original page title

Republishing a PR review page keeps the title it already had.

**What**

When a PR review page is published again, the builder reuses the title stored on the previous page instead of regenerating one, so a hand-edited or earlier title survives republishing.

**Details**

- A new helper pulls the existing `<title>...</title>` out of the previously published HTML, matching after `</head><body>` or on a unique match in the document.
- The regenerated default, used when no stored title is found, changed word order from "PR review: <name>" to "<name> PR review".

**Evidence**

`storedTitleLine`

- Area: Artifacts
- Tier: You'll notice
- Useful: 2/5
- Signal: 2/5

### Artifact share titles and name folding tightened

Shared artifact titles get cleaner quoting, and German sharp-S spellings no longer count as different names.

**What**

Live artifact sharing can now be given a callback that supplies the title, falling back to the URL, with quote characters normalised to apostrophes and an extra sanitizing pass. Separately, name collision handling for synced items now folds capital ẞ (U+1E9E) to `ß` before case folding, so the two spellings can no longer end up as different names.

**Evidence**

`.replace(/\u1e9e/g, "\xDF")`

- Area: Artifacts
- Tier: Under the hood
- Useful: 2/5
- Signal: 2/5

### Removing a marketplace clears its cached copy

Removing a plugin marketplace now also deletes its cached copy from disk.

**What**

Removing a plugin marketplace, or overwriting one whose source URL changed, now also deletes the cached copy on disk, so a stale checkout is no longer left behind.

**Details**

- A failed delete logs a warning and the removal still proceeds.

**Evidence**

`Failed to delete cached marketplace `

- Area: Plugins
- Tier: You'll notice
- Useful: 3/5
- Signal: 1/5

### Model selection forwarded on restart-in-place

Your selected model is passed along whenever Claude Code re-executes itself.

**What**

The argument list Claude Code builds when it re-executes itself now carries the session's selected model, sent as `default` when nothing is selected.

**Details**

- Same path as the update relaunch ("Switching from ... to latest… conversation will continue") and `/tui` renderer switching.
- Skipped entirely on `mantle` environments and when the selection matches the configured fallback model.

**Evidence**

`...(s !== void 0 ? ["--model", s] : []),`

- Area: Models
- Names: `--model`
- Tier: Under the hood
- Useful: 3/5
- Signal: 1/5

### Resuming an agent transcript says why it could not be loaded

Logs now say why a subagent transcript could not be resumed instead of just "not found".

**What**

When a sub-agent transcript cannot be resumed, the logs now distinguish a missing transcript, one the storage layer refused for permission reasons, and an outright error, instead of a bare "not found".

**Details**

- Results are also tagged with how they were served, so a cached load can be told apart from a fresh one.

**Evidence**

`getAgentTranscript: the storage backend cannot serve this agent transcript (permission); not resumable`

- Area: Elsewhere
- Tier: Under the hood
- Useful: 2/5
- Signal: 1/5

### Whiteboard artifacts get short titles

Whiteboard artifacts are now just titled "Whiteboard" instead of a long phrase.

**What**

The whiteboard skill now titles its artifact `Whiteboard` by default instead of a long explanatory phrase, in the page template, the publish fallback and the merge-state default.

**Details**

- The `--title` guidance now asks for a short topic name followed by the word "whiteboard", for example "Ingest pipeline whiteboard", and forbids appending an explainer after a dash or colon.
- The helper's error text was updated to match, and the prototype skill's publish instruction now also asks for a short stable title.

**Evidence**

`fail('--title needs the board\'s name (e.g. "Ingest pipeline whiteboard"), got '`

- Area: Artifacts
- Tier: You'll notice
- Useful: 2/5
- Signal: 1/5

### Background-state reads retry before giving up

Background state reads retry on transient errors instead of failing immediately.

**What**

Reads of background state now retry on a delay schedule while the error looks transient, so a momentarily unavailable read no longer surfaces as a failure.

**Details**

- Retried when the error is `Unavailable`, or `Failed` but outside a known set of permanent failure classes.
- A read that succeeded only after retries reports `tengu_bg_state_read_recovered` with the number of attempts; a clean first read is noted separately.

**Evidence**

`tengu_bg_state_read_recovered`

- Area: Background Tasks
- Tier: You'll notice
- Useful: 2/5
- Signal: 1/5

### Clearer wording when teammate messaging cannot check the account

Messaging warnings now say the account session list could not be checked, which is clearer.

**What**

The warning shown when peer messaging cannot reach the account now says "the account session list could not be checked" instead of talking about Remote Control sessions on other machines.

**Details**

- Name resolution telemetry now reports `prefix_of_claimed_pin` when you typed a prefix of a claimed name, rather than always reporting it as a typed pinned name.
- Cloud session worker status strings go through one shared mapper.

**Evidence**

`prefix_of_claimed_pin`

- Area: Messaging
- Tier: You'll notice
- Useful: 2/5
- Signal: 1/5

### Watch eviction gives a short reason

Dropping a watched artifact now shows a short "made room for another watch" reason.

**What**

When a session is at its limit for watched artifacts and drops one so a protected watch can reconnect, the long explanation is now paired with a short form, "made room for another watch", for compact display.

**Evidence**

`made room for another watch`

- Area: Artifacts
- Tier: You'll notice
- Useful: 2/5
- Signal: 1/5

### Socket paths and message origins redacted in debug logs

Debug logs now mask socket paths, tokens and long hex values from local messaging.

**What**

Debug lines for the local socket client (`[uds-client] Sending control:` and `[uds-client] Sent to `) and peer-message origin summaries now run through the redactor that withholds anything matching /token/i and masks long hex runs, so raw socket paths and values no longer land in logs.

**Details**

- Detection of a dead agent socket widened: it now also recognises a typed error class in addition to ENOENT and ECONNREFUSED.

**Evidence**

`stale_socket`

- Area: Internals
- Tier: Under the hood
- Useful: 2/5
- Signal: 1/5

### Plugin in-use markers get their own directory and escape checks

Plugin in-use markers now live in their own folder and cannot escape the plugins directory.

**What**

The per-process marker that records a linked plugin as in use is now written under a dedicated `.in_use-links` directory whose resolved path must stay inside the plugins directory.

**Details**

- Each path component is created and re-resolved against the real filesystem path; if any step resolves outside, the marker is not written and the scan is skipped, with a log naming the offending path.

**Evidence**

`.in_use-links`

- Area: Plugins
- Tier: Under the hood
- Useful: 1/5
- Signal: 1/5

## Bug Fixes

### Renames from the phone no longer land on the wrong conversation

Renaming a conversation from your phone no longer retitles whichever chat is open here.

**What**

A title rename arriving over the mobile bridge is dropped, with a log saying why, when the bridge is bound to a session other than the one currently loaded and cannot confirm which one. Previously it was applied to whatever conversation was open.

**Evidence**

`[bridge:repl] Dropping inbound rename mirror: foreign binding with no bound-sid exposure`

- Area: Mobile Bridge
- Tier: You'll notice
- Useful: 3/5
- Signal: 3/5

### Refusals now stop the tool loop

When Claude refuses, the turn now ends rather than the tool loop grinding on.

**What**

When the model refuses, the assistant turn ends there instead of the tool loop continuing on. The refusal category is also carried through, so refusal telemetry reports why.

**Details**

- Both streaming accumulators copy `stop_details` from the message delta onto the accumulated message.
- The loop breaks when `stop_reason` is `refusal`.
- `stop_details.category` feeds the existing refusal-fallback telemetry fields.

**Evidence**

`stop_details`

- Area: Tool Calls
- Tier: You'll notice
- Useful: 3/5
- Signal: 3/5

### Bootstrap responses are thrown away if the credential changed mid-flight

Startup config from an old account is discarded if you switched credentials mid-request.

**What**

After the startup configuration fetch returns, Claude Code re-checks which credential is active; if it changed while the request was in flight, the response is discarded instead of applied, so a stale account's settings cannot land on the new one.

**Details**

- Logs `[Bootstrap] Discarding response fetched under a superseded credential`.
- Records the reason `superseded_credential` on the bootstrap fetch event.

**Evidence**

`superseded_credential`

- Area: Auth
- Tier: You'll notice
- Useful: 3/5
- Signal: 3/5

### Gateway no longer forwards client auth headers upstream

The self-hosted gateway strips your client's API key before calling upstream providers.

**What**

Every path where the self-hosted gateway attaches its own operator credential to an upstream provider request now deletes the inbound `authorization` and `x-api-key` headers first, so a key supplied by the client can no longer reach the upstream alongside the gateway's own.

**Evidence**

`(r.delete("authorization"), r.delete("x-api-key"));`

- Area: Gateway
- Tier: You'll notice
- Useful: 3/5
- Signal: 3/5

### A policy helper can no longer configure another policy helper

An admin policy helper can no longer chain to another policy helper.

**What**

Settings returned by an administrator's policy helper executable now have both `policyHelper` and `policyHelpers` stripped before validation, with a warning logged for each, so a helper cannot point at a further helper. Previously the key was only discarded after validation.

**Details**

- The salvage pass over the helper's parsed output now only rescues individual MCP server entries, and keeps a malformed field whole so the strict schema rejects it instead of quietly dropping it.

**Evidence**

`policyHelper: stripped ${f} from helper output (no recursion)`, `policyHelpers`

- Area: Enterprise Policy
- Names: `policyHelper`, `policyHelpers`
- Tier: Under the hood
- Useful: 2/5
- Signal: 3/5

### Comment redaction drops the file path as well as the quoted text

Redacted artifact comments now have their source file path stripped too, not just the quote.

**What**

When comment text on an artifact is redacted, comments that carry a source file path now count as needing redaction too, and that path is removed. Previously only the quoted snippet triggered redaction.

**Details**

- The rendered thread line no longer hardcodes an `[on text]` prefix; the marker is computed from the comment and shown before the text.

**Evidence**

`anchor_path`

- Area: Artifacts
- Tier: You'll notice
- Useful: 2/5
- Signal: 3/5

### Memory sync safety notices are kept per session

Memory sync warnings about vanished files are now tracked per session, not globally.

**What**

The warning shown when many synced memory files disappear at once, telling you nothing was deleted from shared memory, that files return on the next sync, and to delete in smaller batches, is now stored per session instead of in one process-wide place. The same applies to the notices about mirroring uncovered files.

**Evidence**

`Memory sync did NOT delete anything from shared memory this cycle`

- Area: Memory
- Tier: You'll notice
- Useful: 2/5
- Signal: 3/5

### Reconnecting remote control re-sends session metadata

Reconnecting remote control re-sends session details, including rate limits, so the server isn't stale.

**What**

When the remote control connection is rebuilt with a fresh worker credential, the session pushes its metadata again, including rate limit information, so the server no longer holds stale details after a reconnect.

**Evidence**

`onTransportRebuilt`

- Area: Sessions
- Tier: You'll notice
- Useful: 2/5
- Signal: 3/5

### A /resume mid-upload no longer sends the wrong transcript

Resuming another conversation mid-upload no longer attaches the old session's transcript.

**What**

If you resume a different conversation while a transcript backfill is still running, the upload now aborts and logs instead of pinning content from the old session onto the live one. The check runs both before the main upload and before the subagent pass.

**Evidence**

`[persistence-sync] Refusing backfill: session id changed since the scan anchor was pinned`

- Area: Sessions
- Names: `/resume`
- Tier: You'll notice
- Useful: 3/5
- Signal: 2/5

### Vertex clients no longer send a stale Authorization header

On Vertex, stale Authorization headers no longer clash with Google's own credentials.

**What**

When Claude Code talks to Anthropic models on Google Vertex, the HTTP client now blanks out `Authorization` (and `X-Api-Key`, unless a check passes) so Google's own auth client is the only thing supplying credentials. If auth resolution is skipped, your own `Authorization` value is passed through instead.

**Details**

- The blanking happens through default headers set at client construction time, so it applies to every request that client makes.
- Where credential resolution is bypassed, the caller-supplied value travels as a separate `wireAuthorization` field rather than being dropped.
- The client's internal auth promise now has a no-op catch, so a failed credential fetch no longer surfaces as an unhandled promise rejection.

**Evidence**

`wireAuthorization`

- Area: Vertex
- Tier: You'll notice
- Useful: 3/5
- Signal: 2/5

### Settings watcher notices changes that happened while it was not watching

Settings edits made while the watcher was paused are now picked up afterwards.

**What**

The user settings watcher now records, per file, whether the file was there and at what version, so a later snapshot can spot an edit or a deletion that occurred during a gap in watching.

**Details**

- The deletion log line gains a suffix marking that case.

**Evidence**

` (while unwatched)`

- Area: Settings
- Tier: You'll notice
- Useful: 3/5
- Signal: 2/5

### Bedrock no longer fails when neither a region nor a base URL is set

Bedrock no longer refuses to start when no region or base URL is configured.

**What**

Using the AWS Bedrock provider without `AWS_REGION`, `AWS_DEFAULT_REGION` or `ANTHROPIC_AWS_BASE_URL` previously threw "No AWS region or base URL found." Construction now continues with the base URL left unset.

**Evidence**

`ANTHROPIC_AWS_BASE_URL`

- Area: Bedrock
- Names: `AWS_REGION`, `ANTHROPIC_AWS_BASE_URL`
- Tier: You'll notice
- Useful: 3/5
- Signal: 2/5

### Interrupt no longer races ahead of queued messages

Interrupting a remote session no longer arrives ahead of messages still queued to send.

**What**

Pressing interrupt in a remote session used to fire immediately even when outbound messages were still being held, so the interrupt could land before them. It now fires immediately only when nothing is held, otherwise it waits for the held sends to be released.

**Details**

- `cancelSession` registers a pending interrupt and sends after the held-send release settles, whether that succeeds or fails.

**Evidence**

`[RemoteSessionManager] Sending interrupt signal`

- Area: Sessions
- Tier: You'll notice
- Useful: 3/5
- Signal: 2/5

### Sessions are held for a finished task's follow-up turn

A session stays alive for the follow-up turn after a background task finishes.

**What**

A session with no running background tasks but a finished one whose follow-up turn has not happened is no longer retired out from under that turn.

**Details**

- The retire timer logs and holds the session in that case instead of releasing it.
- The status line reports a finished background task awaiting its follow-up turn.

**Evidence**

`retire time passed while a finished background task's follow-up turn is pending`

- Area: Background Tasks
- Tier: You'll notice
- Useful: 3/5
- Signal: 2/5

### Malformed transcript records are rejected before use

Corrupt transcript lines can no longer feed a bad working directory into your session.

**What**

Two transcript scanners now validate records before reading them, so a corrupt line can no longer inject a bad working directory or non-list replacements into a session.

**Details**

- Records must pass a shape check, `replacements` must be an array, and the relocated working directory must be a non-empty string.

**Evidence**

`content-replacement`

- Area: Sessions
- Tier: You'll notice
- Useful: 3/5
- Signal: 2/5

### Directory copy no longer follows a symlink into a parent of where it is writing

Installing a plugin no longer follows a symlink upward into the folder it is filling.

**What**

Copying a directory tree, such as installing a plugin, now skips any symlink whose target resolves to an ancestor of the destination, alongside the existing skip for symlinks that point back into themselves. This prevents a copy from walking upward into the directory it is filling.

**Details**

- The skip is logged rather than silent.
- The older cyclic-target check is unchanged and still applies.

**Evidence**

`copyDir: skipping symlink to an ancestor directory `

- Area: Plugins
- Tier: You'll notice
- Useful: 3/5
- Signal: 2/5

### Env vars set to an empty string now count as unset

Environment variables set to an empty string now count as unset and use the default.

**What**

Two environment-reading helpers changed so that a variable set to an empty string or to whitespace reads as absent and the default applies, rather than yielding an empty value.

**Details**

- The Deno code path also gained the same fallback, which it was missing.

**Evidence**

`globalThis.Deno.env?.get?.(e)?.trim()`

- Area: Configuration
- Tier: You'll notice
- Useful: 3/5
- Signal: 2/5

### Managed-settings MCP server entries are kept instead of discarded

A malformed MCP servers block in managed settings is kept with a warning, not silently dropped.

**What**

When a policy-managed settings file has an MCP servers field that is not the expected shape, the field is now kept and a warning is logged, instead of the whole field being silently removed with "This field was ignored."

**Details**

- Applies to the policy settings path only; other settings validation still drops invalid fields as before.

**Evidence**

`mcpServerEntrySalvageOnly`

- Area: MCP
- Tier: You'll notice
- Useful: 3/5
- Signal: 2/5

### Refreshed OAuth tokens no longer leave a stale account cached

After a token refresh, requests use your current account rather than the previous one.

**What**

After a token refresh, Claude Code clears the cached snapshot of the signed-in account (account id, email, organisation), so requests made after the refresh use the current account rather than the previous one.

**Details**

- Applies after recovering from a 401 using the token on disk, after an SDK token callback refresh, after adopting a newer token written by another Claude Code process, and on login-state reset.
- Request metadata for the account id now falls back to the stored OAuth account.

**Evidence**

`tengu_oauth_401_recovered_from_disk`

- Area: Auth
- Tier: You'll notice
- Useful: 3/5
- Signal: 2/5

### OAuth refresh races clear the cached account

When another process refreshes your login, the session re-reads who you are signed in as.

**What**

When Claude Code detects that another process already refreshed the login token, it now clears its in-process cache of the authenticated account before recording the race, so the session re-derives who you are signed in as rather than holding on to the pre-refresh account.

**Details**

- The cache is invalidated by bumping its epoch, which happens before the `tengu_oauth_token_refresh_race_resolved` telemetry fires.

**Evidence**

`tengu_oauth_token_refresh_race_resolved`

- Area: Auth
- Tier: You'll notice
- Useful: 2/5
- Signal: 2/5

### Device-binding failures explain themselves

If a session can't be tied to this machine, the error now says which situation happened.

**What**

When a session cannot be tied to this machine, the error now says which of four situations occurred rather than showing a bare code.

**Details**

- Covered cases: the session could not be read, the session is bound to another device, this machine has no device registration, and the local registration could not be read.
- The new reasons merge into the existing device-binding message map.

**Evidence**

`this session is bound to another device (a different machine or config directory)`

- Area: Auth
- Tier: You'll notice
- Useful: 2/5
- Signal: 2/5

### Cache read failures no longer propagate to the caller

Pull-request and closed-issue cache read errors no longer break the thing loading them.

**What**

Reads from the pull-request status cache and the closed-issues cache are wrapped so a storage error is logged and an empty result returned, instead of throwing out of the loader.

**Details**

- Expected errors and genuine faults take different logging paths; the closed-issues failure log names the underlying error.

**Evidence**

`closed-issues cache read failed: ${i}`

- Area: Elsewhere
- Tier: You'll notice
- Useful: 2/5
- Signal: 2/5

### Unrenderable message errors no longer echo arbitrary text

The "unable to render message" notice no longer echoes arbitrary text from the message.

**What**

The "Unable to render message type" fallback used to print the raw type value from the message block. It now prints the type only if it is on an allowlist, and otherwise reports `non-allowlisted (non-string)` or just a length.

**Details**

- The allowlist now includes the newly handled `fallback` and `mid_conv_system` types.

**Evidence**

`non-allowlisted (non-string)`

- Area: Terminal UI
- Tier: You'll notice
- Useful: 2/5
- Signal: 2/5

### AWS credential failures say what went wrong

AWS credential problems now come with one clear message instead of a raw SDK error.

**What**

When Bedrock or Vertex request signing cannot get AWS credentials, you now get one clear message instead of a raw SDK rejection.

**Details**

- The provider chain is awaited inside a try/catch and any failure is rewrapped as a typed error with a fixed message.

**Evidence**

`Failed to resolve AWS credentials from the credential provider chain.`

- Area: Bedrock
- Tier: You'll notice
- Useful: 3/5
- Signal: 1/5

### Linked plugin directories are no longer marked orphaned

Plugins you symlinked for local development are no longer flagged orphaned and swept away.

**What**

The routines that write, read and clear the orphan marker on cached plugins now stop early if the plugin path is a symbolic link. Development plugins linked into the cache are no longer flagged as orphaned or swept by cache cleanup.

**Evidence**

`Failed to remove .orphaned_at`

- Area: Plugins
- Tier: You'll notice
- Useful: 3/5
- Signal: 1/5

### Streamed tool inputs serialize correctly once complete

Completed tool inputs no longer show up as empty when written out.

**What**

A tool call's assembled input is now a normal enumerable property, so a finished tool input serializes as expected rather than appearing empty.

**Details**

- Partial JSON chunks are accumulated by a shared helper instead of a hand-rolled hidden buffer property on each delta.
- At the end of a content block the accumulated `input` is redefined as enumerable, writable and configurable.

**Evidence**

`input_json_delta`

- Area: Tool Calls
- Tier: You'll notice
- Useful: 3/5
- Signal: 1/5

### Connection errors show the underlying cause

Connection failures now show the real underlying error text.

**What**

Generic connection failures now surface the text of the real underlying error instead of a bare, uninformative message.

**Details**

- New helpers walk up to five levels of nested error causes to find the first informative message.
- Errno-shaped codes are recognised: anything matching `E` followed by capitals or digits, plus `UNKNOWN` and text starting with "Unknown system error".

**Evidence**

`Unknown system error`

- Area: Internals
- Tier: You'll notice
- Useful: 3/5
- Signal: 1/5

### Terminal recovers from a failed layout pass

A failed layout measurement now clears the cache and redraws instead of showing a broken frame.

**What**

If the terminal layout engine throws while measuring a frame, the renderer now clears its cached layout and retries from scratch. If the retry also fails, that frame is skipped rather than drawn broken, and the fault is reported to error tracking once.

**Details**

- New renderer state tracks whether the last layout failed and how many have failed consecutively.
- When the thrown value is not an error, it is reported with the description `ink layout pass threw a value that cannot be described`.
- None of this recovery path existed in v2.1.227.

**Evidence**

`ink layout pass threw a value that cannot be described`

- Area: Terminal UI
- Tier: You'll notice
- Useful: 3/5
- Signal: 1/5

### Symlinked plugin checkouts are no longer marked orphaned

Symlinked plugin checkouts used for development are no longer cleaned up as orphans.

**What**

A plugin version installed as a symlink, which is how local development checkouts are usually set up, is no longer flagged as orphaned and cleaned up.

**Details**

- The install path is checked for being a symlink before the orphan marker is written, and the code logs "Not marking a symlinked plugin version: <path>" and stops.

**Evidence**

`Not marking a symlinked plugin version`

- Area: Plugins
- Tier: You'll notice
- Useful: 3/5
- Signal: 1/5

### Sessions no longer fall back to a made-up transcript path

Resuming a session no longer points at a transcript file that was never there.

**What**

Working out which transcript file belongs to a session id now reports one of three answers: found, definitely absent, or present but unverifiable. An unverifiable candidate is remembered and used as a last resort instead of the code inventing a path that does not exist.

**Details**

- Listing calls to the storage backend are memoized during a single resolution, so repeated transcript listings do not re-page the backend.

**Evidence**

`unverifiable`

- Area: Sessions
- Tier: You'll notice
- Useful: 2/5
- Signal: 1/5

### Marketplace catalog cache recovers from a leftover directory

A stray directory blocking the marketplace catalog file is now removed and the download retried.

**What**

If writing a downloaded marketplace catalog fails because a directory is sitting where the file should go, that directory is removed and the write is retried once.

**Details**

- When removal also fails, the message names the exact directory to delete by hand.

**Evidence**

`Please manually delete the directory at`

- Area: Plugins
- Tier: You'll notice
- Useful: 2/5
- Signal: 1/5

### Plugin cache cleanup re-checks a directory before deleting it

Plugin cache cleanup now leaves directories alone if they hold subfolders or links.

**What**

A directory queued for deletion during plugin cache cleanup is checked again and its contents listed just before removal. If it holds a subdirectory, a symlink or an entry whose type cannot be determined, it is left alone and the reason logged.

**Evidence**

`it still holds a directory, a symlink or an entry of unknown type`

- Area: Plugins
- Tier: You'll notice
- Useful: 2/5
- Signal: 1/5

### Pasted text with exotic quote characters is cleaned up

More curly and ornamental quote characters get cleaned up when you paste text.

**What**

Smart-quote normalization recognizes more double-quote lookalikes, including the ornamental and heavy quote characters, and now matches runs of two or more apostrophe lookalikes.

**Details**

- Added characters include U+2E42, U+02F5/U+02F6, U+2034 and U+2037, U+2760 and the U+1F676 to U+1F678 ornaments.
- The character class is now matched with a Unicode-aware regex.

**Evidence**

`\u275D\u275E\u2760`

- Area: Elsewhere
- Tier: You'll notice
- Useful: 2/5
- Signal: 1/5

### Numbers in exponent notation tokenize as one value

Numbers like 1e-9 are now read as a single value instead of being split.

**What**

The number scanner previously stopped at digits, `-` and `.`, splitting a value like `1e-9`. It now also consumes `e`/`E` and a sign directly after it.

**Evidence**

`type: "number"`

- Area: Internals
- Tier: You'll notice
- Useful: 2/5
- Signal: 1/5

### Marketplace auto-update source reporting corrected

Marketplace auto-update settings no longer credit the wrong settings file as their source.

**What**

When a settings scope names a plugin marketplace but says nothing about auto-update, Claude Code no longer credits a lower-precedence scope for that setting; it reports no source. A cache-key validator also checks membership in a set rather than hardcoding one form.

**Evidence**

`managed settings (managed-settings.json)`

- Area: Plugins
- Tier: You'll notice
- Useful: 2/5
- Signal: 1/5

### Google credential failures recognised when wrapped in another error

Google credential failures buried inside another error are now identified properly.

**What**

Google authentication failures are now detected by walking the chain of nested causes rather than only inspecting the top-level error, so a wrapped credential failure is classified correctly instead of surfacing as a generic error. One more matching message was added to the existing set.

**Details**

- Matched messages include `Could not load the default credentials`, `Could not refresh access token` and `invalid_grant`, plus one further substring.
- The top-level value no longer has to be an `Error` instance for the check to fire.

**Evidence**

`Could not refresh access token`

- Area: Elsewhere
- Tier: You'll notice
- Useful: 2/5
- Signal: 1/5

### Custom theme loading survives storage errors

A failure reading custom themes no longer breaks theme loading entirely.

**What**

A failure reading your custom themes no longer breaks theme loading. Listing and reading are wrapped so Claude Code keeps whatever themes it already has or could read.

**Details**

- A failed listing logs "[theme] list themes failed" and retains the previously loaded themes.
- A failed batch read logs "[theme] batched theme read failed" and continues with the readable ones.
- A single unreadable theme is counted as a read error and skipped.

**Evidence**

`[theme] batched theme read failed: `

- Area: Elsewhere
- Tier: You'll notice
- Useful: 2/5
- Signal: 1/5

### Resuming a session behind a symlink explains what to do

If a transcript is a symlink, resume now explains the problem instead of showing a raw error.

**What**

If resuming a session fails because the transcript file is a symbolic link, you now get a specific explanation instead of the raw system error.

**Details**

- Triggered when the underlying error code is ELOOP.
- The message says session storage does not read through the link, and tells you to put the original file back if you made the link yourself, or remove the link otherwise.

**Evidence**

`which session storage does not read through. If you made that link yourself, put the original file back in its place; otherwise remove the link.`

- Area: Sessions
- Tier: You'll notice
- Useful: 2/5
- Signal: 1/5

### Transcript cleanup prunes stray directories instead of walking them

Transcript cleanup deletes stray old folders outright instead of crawling through them.

**What**

The pass that deletes old transcripts now verifies a directory name is a well-formed lowercase session id before treating it as a session directory. Anything else older than the retention cutoff is deleted outright rather than recursed into.

**Details**

- The pass also now recognises `.dir-sync.json` and its temp files as its own sidecar files when deciding what it may remove.

**Evidence**

`.dir-sync.json.tmp.`

- Area: Sessions
- Tier: You'll notice
- Useful: 2/5
- Signal: 1/5

### Half-typed exponents no longer break streaming tool input

A half-typed exponent while a tool call streams no longer produces a malformed value.

**What**

While a tool call is still streaming, the repair step for incomplete JSON now discards a trailing number ending in `+`, `e` or `E`, not just one ending in `.` or `-`, so a partly written exponent no longer parses into a malformed value.

**Evidence**

`r.push({ type: "number", value: a });`

- Area: Tool Calls
- Tier: You'll notice
- Useful: 2/5
- Signal: 1/5

### Expected storage write failures stop showing up as errors

Ordinary permission or path write failures no longer surface as error reports.

**What**

Failed writes to the session metadata cache caused by permissions, the environment, a missing file or an over-long path are now logged quietly rather than raised as error reports. Other write failures still report.

**Details**

- Treated as benign: failure classes "permission" and "environment", and codes "UnexpectedAbsent", "ENOENT" and "ENAMETOOLONG".

**Evidence**

`ENAMETOOLONG`

- Area: Sessions
- Tier: You'll notice
- Useful: 2/5
- Signal: 1/5

### Ancestor process checks no longer read a failed lookup as a no

A failed process lookup is no longer mistaken for a confirmed no when checking parent processes.

**What**

Deciding whether a given process is an ancestor of this one now returns three states instead of true/false, so a lookup that produced no usable information is distinguished from one that positively ruled the process out.

**Details**

- The result is only cached when the process start token read before and after walking the ancestor chain matches.
- The shell command that walks the parent chain now emits explicit END and FAIL sentinels, so a truncated or failed walk is reported instead of being treated as an empty chain.

**Evidence**

`no-evidence`

- Area: Elsewhere
- Tier: Under the hood
- Useful: 1/5
- Signal: 1/5

### `EISDIR` added to the filesystem error code list

Reading a directory as a file is now recognised as a filesystem error.

**What**

The error classification list in the fetch-and-file area now includes `EISDIR`, alongside `ENOTDIR` and `EMFILE`, so attempts to read a directory as a file are recognised as filesystem errors.

**Evidence**

`"EISDIR"`

- Area: Elsewhere
- Tier: Under the hood
- Useful: 1/5
- Signal: 0/5

## In Development

### Plugin installs can fail closed on unreadable marketplace sources

Plugin installs can now be blocked outright when a marketplace source can't be read.

**What**

Resolving which marketplace a plugin dependency comes from gained a fail-closed option. When the list of known marketplaces comes back empty, an unresolvable marketplace is treated as blocked rather than quietly allowed, which can turn an install that used to succeed into a policy block.

**Details**

- The option is `failClosedOnUnknownSource`; the block is reported as `marketplace-blocked-by-policy`.
- It only applies when managed plugin policy sources are present. Without them the old permissive behaviour stands.

**Evidence**

`failClosedOnUnknownSource`

- Area: Plugins
- Tier: You'll notice
- Useful: 3/5
- Signal: 3/5

### Local IPC sends refuse a pipe with no live owner

Messages to another Claude session now fail loudly if no live session owns that pipe.

**What**

Connecting to a local message pipe is now asynchronous and first resolves who owns it. If no running session has registered an inbox there and the pipe cannot be attributed to a live process, the send throws instead of writing to an unvouched socket, with a dedicated error class.

**Details**

- The requirement for a live owner is computed per call, and the build does not show what it falls back to when that computation says the check is not required.

**Evidence**

`No running session has registered an inbox at`

- Area: Cross-Session Messaging
- Tier: Under the hood
- Useful: 2/5
- Signal: 3/5

### The cross-session inbox can bind after startup if remote config turns it on

Cross-session messaging can now switch on mid-session if config arrives late, instead of staying off.

**What**

When the cross-session messaging gate is off at startup and configuration has not settled, Claude Code now installs a watcher that binds the socket later if a config refresh enables it, then publishes the socket path to the session record. Previously the inbox was skipped for the life of the process.

**Details**

- The late path is distinguished in telemetry by a bind_late marker.
- The gate name does not appear in readable form in the build, so which remote flag flips it is not visible from here.

**Evidence**

`[uds-messaging] Late bind: gate enabled by a GrowthBook refresh after startup`

- Area: Cross-Session Messaging
- Tier: Under the hood
- Useful: 2/5
- Signal: 3/5

### Gateway answers 503 when customer-routed inference keys are unavailable

If key material can't be fetched, the gateway answers 503 with a retry hint instead of an auth error.

**What**

With `cri.enabled` on, a bearer token routed through the customer-routed authenticator whose verification key material cannot be fetched now gets a 503 and a `Retry-After: 30` header, rather than being reported as an authentication failure.

**Evidence**

`customer-routed inference is temporarily unavailable: the gateway cannot obtain current verification keys`

- Area: Gateway
- Names: `cri.enabled`
- Tier: Not switched on
- Useful: 2/5
- Signal: 3/5
- Present in the build but not switched on

### Skills-sync state file is validated before use

Synced-skill state files are schema-checked, so one bad record no longer discards the lot.

**What**

The file recording synced-skill state is now schema-checked on read rather than trusted. Unparseable state yields nothing, non-string directory and pending-claim entries are filtered out, and a single bad skill record is dropped instead of discarding the whole file. Only reachable when skill sync is enabled via `CLAUDE_CODE_SYNC_SKILLS`.

**Evidence**

`pendingClaims`

- Area: Skills
- Names: `CLAUDE_CODE_SYNC_SKILLS`
- Tier: Not switched on
- Useful: 2/5
- Signal: 3/5
- Present in the build but not switched on

### Remote-control heartbeat and stale-epoch recovery, all off

Remote sessions gain reconnect and reactivation heartbeats plus stale-connection recovery, all off here.

**What**

Three new remote-control behaviours were added behind server-side flags: a beat on reconnect (`tengu_ccr_reconnect_beat`), a beat on reactivation (`tengu_ccr_reactivation_beat`) and recovery from a stale connection epoch (`tengu_bridge_recover_stale_epoch`). All three fall back to false in this build, so they are on only if server config says so.

**Details**

- The client's liveness option was renamed to advertise heartbeat-probe support.
- An idle tracker is now passed into the client and fed by user-driven inbound traffic: user messages from the remote worker, successful control responses, and bash commands.

**Evidence**

`tengu_ccr_reactivation_beat`

- Flag `tengu_ccr_reconnect_beat`: On for this account, and not off by default (read for one account on one subscription tier against v2.1.228; this account: on, anonymous baseline: on, compiled default: on) These values were read against a different version of Claude Code, so treat them as the nearest reading available instead of one taken on this release.
- Flag `tengu_ccr_reactivation_beat`: On for this account, and not off by default (read for one account on one subscription tier against v2.1.228; this account: on, anonymous baseline: on, compiled default: on) These values were read against a different version of Claude Code, so treat them as the nearest reading available instead of one taken on this release.
- Flag `tengu_bridge_recover_stale_epoch`: Not enough to say (read for one account on one subscription tier against v2.1.228; this account: no value returned, anonymous baseline: no value returned, compiled default: on) These values were read against a different version of Claude Code, so treat them as the nearest reading available instead of one taken on this release.
- Area: Remote Control
- Tier: Not switched on
- Useful: 2/5
- Signal: 3/5
- Present in the build but not switched on

### Storage-backed global config writer

A second global-config writer goes through the storage backend with better corruption handling.

**What**

A second implementation of the global config writer was added that goes through the storage backend, with stronger corruption handling. Which one runs depends on whether that backend is available, and the build does not settle that at any single point.

**Details**

- It takes a rate-limited rotating backup keeping 5 copies, re-reads and merges under a lock, and refuses any write that would drop cached auth credentials.
- On a parse error it auto-repairs from the cached snapshot, and backs up the corrupted file through the backend, skipping the copy when identical content was already saved.
- Existing telemetry (`tengu_config_auth_loss_prevented`, `tengu_config_stale_write`) is unchanged.

**Evidence**

`The corrupted file has already been backed up.`

- Area: Internals
- Tier: Under the hood
- Useful: 2/5
- Signal: 3/5

### Remote control gains reconnect and reactivation heartbeats, all off by default

Remote control adds reconnect, reactivation and idle heartbeats, each behind a gate that defaults off.

**What**

The remote-control connection's liveness probe is renamed to heartbeat probe throughout, and two new triggers are added: a beat after a reconnect that found the connection stale, and a beat when activity resumes after idle. Each has its own gate, `tengu_ccr_reconnect_beat`, `tengu_ccr_reactivation_beat` and `tengu_ccr_idle_heartbeat`, all falling back to off, with the real values coming from remote config.

**Details**

- Renamed pieces include `onHeartbeatProbe`, `advertiseHeartbeatProbeSupport` and the log line `cli_sse_heartbeat_probe_received`.
- Successful beats report as `ccr_heartbeat_probe`, `ccr_reconnect_beat` or `ccr_reactivation_beat`.

**Evidence**

`ccr_reactivation_beat`

- Flag `tengu_ccr_reconnect_beat`: On for this account, and not off by default (read for one account on one subscription tier against v2.1.228; this account: on, anonymous baseline: on, compiled default: on) These values were read against a different version of Claude Code, so treat them as the nearest reading available instead of one taken on this release.
- Flag `tengu_ccr_reactivation_beat`: On for this account, and not off by default (read for one account on one subscription tier against v2.1.228; this account: on, anonymous baseline: on, compiled default: on) These values were read against a different version of Claude Code, so treat them as the nearest reading available instead of one taken on this release.
- Flag `tengu_ccr_idle_heartbeat`: On for this account, and not off by default (read for one account on one subscription tier against v2.1.228; this account: on, anonymous baseline: on, compiled default: on) These values were read against a different version of Claude Code, so treat them as the nearest reading available instead of one taken on this release.
- Area: Remote Control
- Tier: Not switched on
- Useful: 2/5
- Signal: 3/5
- Present in the build but not switched on

### Built-in skills can be registered disabled

The claude-api and Chrome skills can be registered without being invocable.

**What**

The `claude-api` skill and the Chrome browser skill now accept a disabled option, letting a caller register them without making them invocable. The Chrome skill keeps its existing feature checks behind that switch.

**Evidence**

`isEnabled: () => !e`

- Area: Skills
- Tier: Under the hood
- Useful: 2/5
- Signal: 3/5

### Artifact watch notices gain summaries and an end reason

Artifact watch notices now say what happened and why watching stopped.

**What**

The notice layer that batches artifact watching updates gained short summaries: "Comments are waiting on Artifact: ...", "Auto-edited Artifact: ... — review the change" and "Artifact changed: ...". When watching stops, the notification now includes a short reason in parentheses, such as "connection lost" on the repeated-reconnect path.

**Details**

- These strings only appear when artifact watching or auto-reply is running; what turns that on is decided elsewhere

**Evidence**

`Stopped watching Artifact: `

- Area: Artifacts
- Tier: You'll notice
- Useful: 3/5
- Signal: 2/5

### Child-session supervisor logs turn state

The supervisor now records what each child session is doing between turns.

**What**

The supervisor that runs child sessions now records what each child is doing between turns, which is groundwork for surfacing that state rather than something visible yet.

**Details**

- It classifies follow-up turns against new ones, notes whether a child has stopped at a prompt or is still working, and records child exit.
- Stream events flagged as coming from a sub-agent are now ignored by this path.

**Evidence**

`child parked at a prompt`

- Area: Subagents
- Tier: Under the hood
- Useful: 1/5
- Signal: 3/5

### Artifact database reports a rejected session credential

A rejected artifact credential now reports a distinct "report this" outcome rather than a permission answer.

**What**

A 403 with a recognised body from the artifact database now produces a distinct outcome, `credential_rejected` with reason `custody_403`, classified as unavailable. Its explanation states this is a client/server integration fault to be reported, not an answer about whether the artifact exists or whether you may see it. The artifact database itself is behind the `CLAUDE_CODE_ARTIFACT_DB` environment variable or the `tengu_umber_lattice` gate, whose fallback is false.

**Evidence**

`the server refused this session's credential at the artifact-db door`, `credential_rejected`

- Flag `tengu_umber_lattice`: Off in both readings (read for one account on one subscription tier against v2.1.228; this account: off, anonymous baseline: off, compiled default: on) These values were read against a different version of Claude Code, so treat them as the nearest reading available instead of one taken on this release.
- Area: Artifacts
- Names: `CLAUDE_CODE_ARTIFACT_DB`
- Tier: Not switched on
- Useful: 1/5
- Signal: 3/5
- Present in the build but not switched on

### Doc and sheet prompts are trimmed to the artifact's capabilities

Doc and sheet skill instructions are assembled at run time to match what the artifact supports.

**What**

When one of the new document or spreadsheet skills runs, its instruction file is assembled at invocation rather than sent whole, so Claude is only told to do things the artifact actually supports.

**Details**

- A marked block instructing Claude to act on reader comments (make the edit, reply, resolve) is kept only when the artifact has a thread id, and deleted otherwise.
- A trailing note is appended only when the artifact reports capabilities.
- Front matter is stripped, and anything the user typed after the command is appended under a `## User Request` heading.
- Unreachable in this build, since both skills are registered as disabled.

**Evidence**

`<!-- comment-verbs:begin -->`

- Area: Skills
- Tier: Not switched on
- Useful: 1/5
- Signal: 3/5
- Present in the build but not switched on

### Per-session directory sync registry for laptop sessions

Each session gets at most one directory sync, with the oldest released once a cap is hit.

**What**

New code keeps one directory sync per session, shutting down an existing one before replacing it and releasing the oldest once a cap is reached. It only attaches when the session's git root resolves to container sync mode and a remote already exists, so most sessions never reach it.

**Details**

- Each entry pairs a status publisher with a bounded backlog and a lane row client, keyed by session id.

**Evidence**

`container_sync`

- Area: Cloud Sessions
- Tier: Not switched on
- Useful: 1/5
- Signal: 3/5
- Present in the build but not switched on

### Idle tracking and a reactivation heartbeat for remote sessions

Remote sessions can measure idle time and send an extra beat when you come back, if the server allows.

**What**

The remote bridge client can now sample how long a session has been idle and report it in heartbeats, and send an extra "reactivate" heartbeat when activity resumes after an idle period, logged as `cli_heartbeat_reactivation_beat`. None of it runs in this build unless the server enables it.

**Details**

- Gated on `tengu_ccr_reactivation_beat`, falling back to off; related gates `tengu_ccr_idle_heartbeat` and `tengu_ccr_reconnect_beat` also fall back to off.
- Even when enabled, the reactivation beat requires an idle tracker to be supplied to the client.
- New in this build.

**Evidence**

`cli_heartbeat_reactivation_beat`

- Flag `tengu_ccr_reactivation_beat`: On for this account, and not off by default (read for one account on one subscription tier against v2.1.228; this account: on, anonymous baseline: on, compiled default: on) These values were read against a different version of Claude Code, so treat them as the nearest reading available instead of one taken on this release.
- Flag `tengu_ccr_idle_heartbeat`: On for this account, and not off by default (read for one account on one subscription tier against v2.1.228; this account: on, anonymous baseline: on, compiled default: on) These values were read against a different version of Claude Code, so treat them as the nearest reading available instead of one taken on this release.
- Flag `tengu_ccr_reconnect_beat`: On for this account, and not off by default (read for one account on one subscription tier against v2.1.228; this account: on, anonymous baseline: on, compiled default: on) These values were read against a different version of Claude Code, so treat them as the nearest reading available instead of one taken on this release.
- Area: Remote Control
- Tier: Not switched on
- Useful: 1/5
- Signal: 3/5
- Present in the build but not switched on

### Background runner plumbing for pending follow-ups

The self-hosted runner carries a pending-follow-up flag that nothing sets yet.

**What**

The self-hosted runner threads a new pending-follow-up callback from its entry point through session spawn, and each session now carries a `bgResultPendingFollowup` flag initialised to false alongside its live background task list and in-flight turn marker. Nothing in the build shows where the flag is set, so it has no observable effect yet.

**Evidence**

`bgResultPendingFollowup`

- Area: Background Tasks
- Tier: Under the hood
- Useful: 1/5
- Signal: 3/5
- Present in the build but not switched on

### Diff panel usage event

Opening the diff panel records one event with your terminal width bucket.

**What**

Showing the diff panel fires one event per session recording whether it opened automatically or because you opened it, plus your terminal width bucketed as `under_110`, `110_to_143`, `144_to_199` or `200_plus`. Only reachable when the `tengu_willow_crate` gate is on, and its fallback here is false.

**Evidence**

`tengu_repl_diff_panel_shown`

- Flag `tengu_willow_crate`: Off in both readings (read for one account on one subscription tier against v2.1.228; this account: off, anonymous baseline: off, compiled default: on) These values were read against a different version of Claude Code, so treat them as the nearest reading available instead of one taken on this release.
- Area: Diff Panel
- Tier: Not switched on
- Useful: 1/5
- Signal: 2/5
- Present in the build but not switched on

### Second copy of the scan skip-list and test-path heuristics

A second copy of the folder skip-list and test-path patterns was added, with no stated consumer.

**What**

A list of directory names to skip while scanning the filesystem was added, covering `node_modules`, `.venv`, `venv`, `.tox`, `.nox`, `.direnv` and `__pypackages__` among others, alongside a second copy of the test-path heuristics that match segments like `/__snapshots__/` and filenames ending in `.spec.<ext>`. Which part of Claude Code reads the new copy is not established by the change itself.

**Evidence**

`__pypackages__`

- Area: Elsewhere
- Tier: Under the hood
- Useful: 1/5
- Signal: 2/5

### Gateway token endpoint returns 404 when its flow is not set up

The gateway's token endpoint returns a plain 404 when its flow was never set up.

**What**

In the `claude gateway` server, a POST to `/oauth/token` now returns a plain 404 before parsing the form body if a component built earlier during server setup is missing. The gateway subcommand only runs on the native binary and throws otherwise.

**Details**

- Which configuration section supplies that component is not determined by the surrounding code

**Evidence**

`claude gateway requires the native binary. Install via https://claude.ai/install.sh instead of npm.`

- Area: Gateway
- Tier: Under the hood
- Useful: 1/5
- Signal: 1/5

### Local socket messaging: sanitized logs and unused auth state

Local socket logs now scrub addresses and unexpected message values before printing.

**What**

The local unix-socket channel between Claude Code processes now routes unhandled control actions and message types through the error logger with values passed through a sanitizer, and sanitizes the socket address it prints during namespace checks.

**Details**

- The server object gained auth fields: `authRequired`, `activeTokens`, `activeKeyFile`, `authOkReported`, `authDropReported`, `lastStartFailureCause` and `lastStartDegradedCause`, all cleared on teardown.
- `authRequired` comes from a `requireAuth` start option; nothing in this build passes it, so token auth on this channel is inert here.

**Evidence**

`[uds-messaging] Unhandled control action: `

- Area: Cross-Session Messaging
- Tier: Under the hood
- Useful: 1/5
- Signal: 1/5

## Internal Changes

### Per-session state replaces process-wide globals

Per-session state is now inherited by forked sessions, so a child no longer overwrites its parent's.

**What**

State that used to live once per process now lives on the session and is inherited by forked sessions, so a child session no longer shares or overwrites its parent's. This covers observer pairings, the autonomous-loop preamble latch, compaction precompute, the worktree resume refusal, memory sync notices, command history, known Slack channels, and path and directory scan caches.

**Details**

- Forks read the parent's values through their root session rather than starting empty.
- Post-compaction cleanup and the observer arm and retire helpers now take the session as an argument.

**Evidence**

`autonomousLoopPreamble`

- Area: Session State
- Tier: Under the hood
- Useful: 3/5
- Signal: 3/5

### Usage reports a thinking-token count

Token usage now includes a thinking-token count, accumulated correctly in both streaming and non-streaming modes.

**What**

Token usage now carries an `output_tokens_details` object with a `thinking_tokens` field, parsed from responses and combined correctly across both streaming and non-streaming accumulation.

**Details**

- The zero-usage baseline starts at `{ thinking_tokens: 0 }`.
- `thinking_tokens` may be null, and placeholder or synthetic usage records set `output_tokens_details` to null.

**Evidence**

`output_tokens_details`

- Area: Token Usage
- Tier: Under the hood
- Useful: 3/5
- Signal: 3/5

### Session state moved out of module globals

Lots of shared state now lives per session, so two sessions in one process stop stepping on each other.

**What**

MCP wiring, interaction and idle tracking, model overrides, refusal-fallback latches, command and Slack completion caches and artifact notice coalescing now live in classes held in a WeakMap keyed by a session root instead of shared module-level variables. Several `reset()` methods went away, since discarding the root now discards the state.

**Evidence**

`refusalFallbackLatchOriginRequestId`

- Area: Session State
- Tier: Under the hood
- Useful: 2/5
- Signal: 3/5

### USER_TYPE joins the specially handled environment variables

The launcher now handles USER_TYPE specially when setting up the environment it passes down.

**What**

The launcher now treats `USER_TYPE` like the messaging socket and managed settings path variables when preparing the environment.

**Evidence**

`"USER_TYPE"`

- Area: Environment Variables
- Names: `USER_TYPE`
- Tier: Under the hood
- Useful: 2/5
- Signal: 3/5

### Per-session state replaces module-level globals

Process-wide state moved onto the session object, the biggest structural change in this build.

**What**

State that used to live in module-level singletons now hangs off the session object, so two sessions in one process no longer share it. No behaviour change on its own, but it is the largest structural change in this release.

**Details**

- The session object previously carried only workflow usage consent; it now owns a dozen stores, including observers, shell and command history, pending hints, prompt suggestions, plugin sync, precompute, tool progress throttling and the stash of pending write permissions.
- Forked sessions share these stores with their root session rather than getting fresh copies.
- Hundreds of call sites now take an explicit session parameter; for example the pending-write-permission stash is reached through the session rather than a global.

**Evidence**

`writePermissionStash`

- Area: Session State
- Tier: Under the hood
- Useful: 2/5
- Signal: 3/5

### Teleport session listings include the bound device id

Remote teleport sessions listed locally now carry the id of the device they are bound to.

**What**

Remote teleport sessions mapped into the local session shape now carry a bound device identifier, lower-cased, alongside the existing status and context fields.

**Evidence**

`bound_device_uuid`

- Area: Sessions
- Tier: Under the hood
- Useful: 2/5
- Signal: 3/5

### Artifact auto-reply notices carry a short line and a full explanation

Artifact comment auto-reply notices now carry a short line plus a fuller reason for different places.

**What**

The notices raised when artifact comment auto-reply is capped, suppressed or re-enabled now travel as a summary plus a detail rather than one flat string, so a brief line and the full reason can be shown in different places.

**Details**

- Covers the hourly cap, plan mode, notify-only mode, a blocked pipeline, an edit that landed while the reply failed, and auto-replies being turned back on.

**Evidence**

`Auto-reply is notify-only in this permission mode`

- Area: Artifacts
- Tier: Under the hood
- Useful: 2/5
- Signal: 3/5

### Startup commands grouped by feature with open and closed sets

Startup commands are grouped by feature, and a closed group can register alternative commands instead of none.

**What**

The list of commands registered at startup moved from a flat array with inline conditionals to named groups, each deciding for itself whether it is open. A closed group now registers alternative commands rather than registering nothing.

**Details**

- Groups are fleetFork, fleetBackground, daemon, skillDoctor and logout, each with its own predicate covering things like demo mode, daemon availability and gateway auth.

**Evidence**

`skillDoctor`

- Area: Slash Commands
- Tier: Under the hood
- Useful: 2/5
- Signal: 3/5

### Named outcomes for failed context-source checkout cleanup

Failed cleanup of a checked-out source now names the case, including a case-only path collision.

**What**

Two failure modes for tidying up a checked-out context source now have descriptions and telemetry keys: removal failed and the partial checkout may still be visible to the session, or the path collided with an already-prepared copy differing only in letter case and was left alone.

**Evidence**

`kept_prepared_twin`

- Area: Context Sources
- Tier: Under the hood
- Useful: 2/5
- Signal: 3/5

### Grouped task notifications record what was coalesced

Grouped notifications now record what was bundled together, not just that you saw it.

**What**

A task notification covering a group of events other than artifact changes now records the group's slug, family, display name and count before marking the artifact seen, where previously it only marked it seen.

**Evidence**

`artifact-changed`

- Area: Terminal UI
- Tier: Under the hood
- Useful: 2/5
- Signal: 3/5

### SDK memory client drops redact, gains a page-number paginator

The bundled memory-versions client lost its redact call and gained page-number pagination.

**What**

The managed-agents memory-versions client no longer exposes a redact method, and a new paginator handles responses that give the next and previous page as numbers.

**Evidence**

`"managed-agents-2026-04-01"`

- Area: API Client
- Tier: Under the hood
- Useful: 1/5
- Signal: 3/5

### API client request handling moved to a middleware chain

API requests are built through a middleware chain, and middleware errors no longer get retried as network failures.

**What**

Claude Code's copy of the Anthropic SDK builds requests through a middleware chain instead of overriding request preparation. Errors raised by middleware are no longer retried as if they were network failures.

**Details**

- Bedrock SigV4 signing and Vertex path rewriting run as middleware over a complete request; Vertex rewrites the URL path and strips the beta query parameter instead of editing a path field.
- Bedrock reads the AWS region from the shared AWS config file and derives a default base URL from it.

**Evidence**

`middleware error (not retryable)`

- Area: API Client
- Tier: Under the hood
- Useful: 2/5
- Signal: 2/5

### Job state writes fail loudly on a missing directory

Job state writes now fail outright if their folder vanished, instead of silently rebuilding it.

**What**

Job state and sidecar writes on the v5 storage backend path now require the parent directory to already exist, so a write into a directory deleted underneath fails instead of quietly recreating the tree. Failures carry the backend's telemetry code through a shared extractor.

**Evidence**

`[jobs] v5 sidecar write failed`

- Area: Internals
- Tier: Under the hood
- Useful: 2/5
- Signal: 2/5

### Credential refresh failures are now distinguishable

Credential refresh failures now report which specific thing went wrong instead of just failing.

**What**

Refreshing host credentials throws with a distinct error for each failure mode: the credential file failing to re-read, an endpoint variable changing between reads, and a refresh that came back without the bearer key. All three previously returned null.

**Evidence**

`host-creds refresh dropped the bearer key`

- Area: Auth
- Tier: Under the hood
- Useful: 2/5
- Signal: 2/5

### Ten more subsystems moved from module globals to per-session state

Diagnostics, plugin scans, agent commands and MCP listings are now built per session rather than once at startup.

**What**

Language-server diagnostics, plugin and skill scan caches, the agent command store, MCP listing state and others are now created lazily per session rather than once at import. Several reset methods that cleared the old globals were deleted, and many functions gained a leading session argument.

**Evidence**

`scanUncertaintyHoldSids`

- Area: Session State
- Tier: Under the hood
- Useful: 2/5
- Signal: 2/5

### More environment variables read through the typed registry

More environment variables are read through typed declarations; the tool concurrency cap still defaults to ten.

**What**

Several hand-written environment variable readers were replaced by typed accessors. `CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY` still defaults to 10 and is now declared with a minimum of 1, so a non-positive value falls back to the default as before.

**Details**

- The project UUID reader no longer trims its own value, and the Datadog flush interval reader no longer parses the raw string.
- A messaging token string variable was added to the same registry module, next to the existing messaging socket variable.

**Evidence**

`CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY`

- Area: Environment Variables
- Names: `CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY`
- Tier: Under the hood
- Useful: 2/5
- Signal: 2/5

### Team-file lock contention is no longer counted as a storage error

Losing a lock race on a shared team file is no longer counted as a storage error.

**What**

When a teammate team-file update loses a lock race, it is now reported as "lock_contended" and the generic failure telemetry is suppressed, so contention stops inflating the storage error count.

**Details**

- Matched on an unavailable result carrying a lock-contended telemetry code, mirroring an existing check for a suspect lock.

**Evidence**

`swarm_team_file_update`

- Area: Telemetry
- Tier: Under the hood
- Useful: 2/5
- Signal: 2/5

### Adopted agent transcripts are relinked by hard link with a verified copy fallback

Adopting an agent's transcript now hard-links the file, copying only when linking is impossible.

**What**

On the v5 transcript storage backend, adopting an agent now places its transcript and metadata file under the expected name using a hard link, falling back to a streaming copy when linking is not possible. The older relink path still runs on other backends.

**Details**

- The fallback copy verifies the inode, refuses on `ADOPT_IDENTITY_MISMATCH`, and preserves timestamps.
- If the transcript step throws, the metadata file write is rolled back.
- A new `tengu_adopt_link` event records the kind, the method used and any fallback code, reporting `adopt_link_fallback_copy` when a copy was needed.

**Evidence**

`tengu_adopt_link`

- Area: Internals
- Tier: Under the hood
- Useful: 2/5
- Signal: 2/5

### Managed policy helper failures throw instead of returning text

Managed policy lookup failures now raise typed errors with codes instead of returning a message.

**What**

A failing policy helper now raises a typed error carrying a code such as bad_path, rather than handing back a message string, and the check for where the admin settings came from moved out to the caller. Telemetry names are unchanged.

**Evidence**

`policyHelper failed: `

- Area: Managed Settings
- Tier: Under the hood
- Useful: 2/5
- Signal: 2/5

### Checkout-hook failures carry a structured reason

A checkout hook that succeeds but produces nothing now reports a structured reason and exit code.

**What**

When a checkout hook exits successfully but leaves nothing usable behind, the failure is now a typed error with an exit code and a short reason, so it can be classified rather than only printed.

**Details**

- Reasons: `exited 0 but did not create the checkout directory` and `exited 0 but the checkout path is not a directory`.

**Evidence**

`exited 0 but the checkout path is not a directory`

- Area: Elsewhere
- Tier: Under the hood
- Useful: 2/5
- Signal: 2/5

### Environment variables read through one typed accessor

Environment flags are now parsed centrally, so values are compared to explicit true or false.

**What**

Direct `process.env` reads were replaced with a shared env object across terminal detection, scroll telemetry, git-bash resolution, editor selection, Bedrock and Vertex selection, workflows and MCP. Truthiness parsing is now centralized, so variables like `CLAUDE_CODE_WORKFLOWS` are compared to explicit true/false rather than tested loosely.

**Details**

- `CLAUDE_CODE_DISABLE_CLAUDE_API_SKILL` now passes a `disabled` option to the skill registrar instead of skipping registration entirely.
- Behaviour is intended to be unchanged.

**Evidence**

`CLAUDE_CODE_DISABLE_CLAUDE_API_SKILL`

- Area: Environment Variables
- Names: `CLAUDE_CODE_WORKFLOWS`
- Tier: Under the hood
- Useful: 2/5
- Signal: 2/5

### Background process records gain a filetime start stamp

Background process records keep a second start timestamp so a recycled process id is spotted.

**What**

Records for background and daemon processes can carry a second start timestamp alongside the existing one, with the platform deciding which is used to confirm a process is still the one that was recorded.

**Evidence**

`procStartFt`

- Area: Background Tasks
- Tier: Under the hood
- Useful: 2/5
- Signal: 2/5

### Agent session stream passes through plain tool results

Managed agent sessions now pass through plain tool results, not just custom tool events.

**What**

The event filter on the managed agent session stream now accepts `user.tool_result` alongside the custom tool result events it already allowed, and the session tool runner emits and reads that shape when reporting results from non-custom tools.

**Evidence**

`"user.tool_result"`

- Area: Agents
- Tier: Under the hood
- Useful: 2/5
- Signal: 2/5

### Per-session state replaces process-wide singletons

Diagnostics, fallback history, shell hints and observer pairings are no longer shared between sessions in one process.

**What**

Pending language-server diagnostics, the ask-Claude fallback exchange history, shell hint delivery, worktree resume refusals and agent-observer pairings all moved from process globals onto the session. Nothing changes for a single session, but these are no longer shared across sessions in one process.

**Details**

- Diagnostics are now looked up per session and the reset entry point was renamed to clear all state.
- The old standalone history class was deleted; that history now hangs off the session.

**Evidence**

`LSP Diagnostics: Resetting all state`

- Area: Session State
- Tier: Under the hood
- Useful: 2/5
- Signal: 2/5

### Twelve more caches moved onto the session object

A dozen caches now hang off the session and are inherited when a session forks.

**What**

The session object used to carry one field and now carries a dozen, including observers, fallback history, prompt suggestions, pending hints, plugin sync, tool progress throttling, stashed write permissions and the autonomous loop preamble. Each is inherited from the root session when a session forks.

**Details**

- This is why hundreds of call sites in this area gained a session argument, including observer pairing, auto-mode classifier telemetry, marketplace and plugin cache invalidation, hook registration and transcript sharing.
- These caches are no longer shared between sessions running in the same process.

**Evidence**

`sessionRefsGate`

- Area: Session State
- Tier: Under the hood
- Useful: 2/5
- Signal: 2/5

### MCP tool telemetry attributes tools to their plugin

Tool calls from plugin-supplied MCP servers are now attributed to their plugin in telemetry.

**What**

Tool calls served by an MCP server that came from a plugin now carry plugin attribution in telemetry, at each tool result and error site. Servers you configured yourself are unaffected.

**Details**

- Redaction allowlist gains `plugin_id_hash`, `plugin_name_redacted` and `marketplace_name_redacted`.
- The attribution blob is attached to the tool descriptor only when the server config names a plugin source.

**Evidence**

`pluginTelemetry`, `plugin_name_redacted`

- Area: MCP
- Tier: Under the hood
- Useful: 2/5
- Signal: 2/5

### Streaming shape tweaks for thinking, usage and compaction

Thinking updates always carry a token estimate, and compacted blocks keep their encrypted content.

**What**

Thinking deltas now always carry an estimated token count, sending null when there is none rather than omitting the field, and readers turn that back into an absent value. Usage records default to a null output token breakdown, and accumulated compaction blocks now keep the encrypted content from the delta.

**Evidence**

`output_tokens_details: null`

- Area: Elsewhere
- Tier: Under the hood
- Useful: 2/5
- Signal: 2/5

### About 220 more environment variables move to the typed registry

About 220 environment variables now parse and validate in one place, including bounds checks.

**What**

Direct `process.env` reads were replaced with typed registry lookups across roughly 220 sites, covering `CLAUDE_MEMORY_STORES`, `CLAUDE_PROJECT_UUID`, `ANTHROPIC_BASE_URL`, `CLAUDE_JOB_DIR`, `TERM_PROGRAM`, `BAT_THEME`, `PATHEXT`, terminal multiplexer detection and more. Parsing and trimming now happen once in the registry, so `MCP_OAUTH_CALLBACK_PORT` is checked only against the 65535 upper bound, its lower bound of 1 being enforced by its declaration.

**Evidence**

`MCP_OAUTH_CALLBACK_PORT`

- Area: Environment Variables
- Names: `CLAUDE_MEMORY_STORES`, `CLAUDE_PROJECT_UUID`, `ANTHROPIC_BASE_URL`, `CLAUDE_JOB_DIR`, `MCP_OAUTH_CALLBACK_PORT`
- Tier: Under the hood
- Useful: 2/5
- Signal: 2/5

### Inbox startup reports separate failure and degradation causes

Cross-session inbox startup now reports exactly why it failed, or that it started with weaker guarantees.

**What**

Startup telemetry for the cross-session inbox now records a specific failure cause, or a separate degraded cause when the socket bound with reduced guarantees, instead of a single bind-failed outcome.

**Evidence**

`agents_cross_session_inbox`

- Area: Elsewhere
- Tier: Under the hood
- Useful: 1/5
- Signal: 2/5

### Heartbeats report idle time and fire an extra beat after a stale reconnect

Session heartbeats now say how long you have been idle and advertise probe support under a new name.

**What**

Heartbeats now include how many seconds the session has been idle, and the field advertising probe support was renamed to `supports_heartbeat_probe`.

**Details**

- After reconnecting, if the last good heartbeat is older than 1.5 times the current interval and no rate-limit response was seen recently, one extra beat is sent and reported as `cli_heartbeat_reconnect_stale_beat`.

**Evidence**

`cli_heartbeat_reconnect_stale_beat`

- Area: Sessions
- Tier: Under the hood
- Useful: 1/5
- Signal: 2/5

### Storage selector threaded through many call sites

Many background writes now name which storage they use instead of assuming one.

**What**

Feedback queue reads and pruning, theme saving, agent observer park and unpark, daemon job key deletion, worker roster updates, MCP connect and retry, and scheduled task arming all now pass an explicit storage selector rather than assuming one. Deleting a daemon record prefers removing the storage key and only unlinks the file as a fallback.

**Evidence**

`storageV5`

- Area: Internals
- Tier: Under the hood
- Useful: 1/5
- Signal: 2/5

### Distinct telemetry when remote-control token refresh runs out of options

Running out of ways to refresh the remote-control token is now recorded under its own code.

**What**

Exhausting the remote-control token refresh chain with neither credential source available is now recorded under its own code before the auth failure is surfaced.

**Evidence**

`chain_exhausted_no_oauth`

- Area: Sessions
- Tier: Under the hood
- Useful: 1/5
- Signal: 2/5

### Bridge teardown records why a session was abandoned

Giving up on a remote bridge session after a fatal reconnect is now recorded with that reason.

**What**

When a remote bridge session fails fatally during reconnect, the abandonment is now recorded with the reason `abandoned_after_fatal_reconnect` before the connection is cleared and the error is printed.

**Evidence**

`abandoned_after_fatal_reconnect`

- Area: Sessions
- Tier: Under the hood
- Useful: 1/5
- Signal: 2/5

### Cross-session inbox startup failures record why they failed

Inbox startup now names whether binding or key publishing failed, and flags degraded starts.

**What**

Startup telemetry for the cross-session inbox now names the cause, either failing to bind the socket or failing to publish the key, rather than attributing every failure generically. A start that succeeds but is degraded is now reported as degraded instead of success.

**Evidence**

`getUdsStartFailureCause`

- Area: Elsewhere
- Tier: Under the hood
- Useful: 1/5
- Signal: 2/5

### device_bash telemetry no longer reports session matching

Device bash limit and timeout events dropped their session-matching field.

**What**

The output-limit and timeout events for device bash dropped the field that compared session ids; the timeout event now carries only its duration in milliseconds.

**Evidence**

`tengu_device_bash_output_limit`

- Area: Telemetry
- Tier: Under the hood
- Useful: 1/5
- Signal: 2/5

### Cloud client registry and error responses with headers

Bedrock and Vertex clients register themselves, and local server errors can now carry response headers.

**What**

Bedrock and Vertex clients now add themselves to a module-level set when constructed, and the local server's JSON error helper accepts optional response headers so an error can carry things like an auth challenge.

**Evidence**

`request_id`

- Area: API Client
- Tier: Under the hood
- Useful: 1/5
- Signal: 2/5

### Forking a conversation seeds the message cache

Forking a conversation preloads its message index, so the first lookups skip re-reading the file.

**What**

After a fork writes the new transcript, the set of message ids for the new session is inserted straight into the transcript store's in-memory cache, so later lookups do not re-read the file just written. Insert is skipped if an entry is already there.

**Evidence**

`primeSessionMessagesCache`

- Area: Sessions
- Tier: You'll notice
- Useful: 2/5
- Signal: 1/5

### Installed plugin list saved through a transactional store when one exists

The installed-plugin list is written through a safer read-modify-write when storage supports it.

**What**

Writing the record of which plugins are installed now goes through a read-modify-write on the storage backend when one is available, and falls back to the previous direct file write when there is none.

**Details**

- The read accepts the older file shape and migrates it, or parses the current one directly.
- A write that fails because a lock looks suspect is retried, honouring a retry delay supplied by the server.
- File permissions come from the process umask, and load failures are reported separately from save failures.

**Evidence**

`installed plugins registry update settled without an outcome`

- Area: Plugins
- Tier: Under the hood
- Useful: 2/5
- Signal: 1/5

### Version 2.1.228

Version is now 2.1.228, which is what version output and update banners report.

**What**

Build constants move to version 2.1.228 with build time 2026-08-11T01:33:09Z and a new commit sha, which accounts for most of the diff through update banners, `claude --version`, user-agent strings, doctor output and daemon version-mismatch warnings.

**Details**

- Completing onboarding now stamps `lastOnboardingVersion` with this version.

**Evidence**

`BUILD_TIME: "2026-08-11T01:33:09Z"`, `"2026-08-11T01:33:09Z"`

- Area: Internals
- Names: `claude --version`
- Tier: Under the hood
- Useful: 2/5
- Signal: 1/5

### Stale process files from sibling processes are reaped

Leftover process files from other dead processes are now cleaned up too.

**What**

The cleanup pass that removes leftover process-id files gained a branch for a second filename pattern, unlinking or handing off entries that belong to dead processes other than the current one before falling back to the existing numeric `.json` handling.

**Evidence**

`if (!/^\d+\.json$/.test(i)) continue;`

- Area: Background Tasks
- Tier: Under the hood
- Useful: 2/5
- Signal: 1/5

### Layout nodes can drop their cached measurements in place

Layout measurements can be thrown away in place so a screen can be re-measured once before giving up.

**What**

The layout node class gained a recursive cache clear that marks every node in the tree dirty and resets its cached measurement and flexbox generation counters, with an iteration budget derived from the live node count. This is what makes the single re-layout retry different from simply laying out again.

**Evidence**

`clearLayoutCacheRecursive()`

- Area: Elsewhere
- Tier: Under the hood
- Useful: 1/5
- Signal: 1/5

### Storage listings use one bounded pager

Storage listings share one pager that says whether a scan finished, hit its cap, or errored.

**What**

Hand-rolled paging loops were replaced by a shared helper that reports whether a listing finished, hit its page cap, or errored, with an optional page budget shared across sibling scans. Truncated listings are now logged rather than quietly returning partial results.

**Evidence**

`listing of ${e} truncated at the page cap`

- Area: Internals
- Tier: Under the hood
- Useful: 1/5
- Signal: 1/5

### Some request headers now append instead of replace

A few request headers now combine values instead of the last one overwriting the rest.

**What**

The SDK's header merging gained a small set of names, currently `x-stainless-helper`, that combine by comma-joining unique trimmed values instead of the later value winning, plus a sentinel that clears a header and stops later values appending to it.

**Evidence**

`x-stainless-helper`

- Area: API Client
- Tier: Under the hood
- Useful: 1/5
- Signal: 1/5

### History counting can page from the storage backend

Command-history matching can count entries by paging the storage layer, falling back to the old path.

**What**

When a storage backend is passed in, counting matching command-history entries pages records from it, handling both LF and CR-separated entries. It falls back to the previous counting path when no backend is available or the page budget runs out.

**Evidence**

`readRecords`

- Area: Elsewhere
- Tier: Under the hood
- Useful: 1/5
- Signal: 1/5

### Assistant messages carry two new null fields

Messages Claude Code writes itself now include two empty fields to match the upgraded message format.

**What**

Messages Claude Code synthesizes itself, such as tool-use notices, stop-sequence messages and virtual transcript entries, now set `diagnostics: null` and a null token-details field in usage, matching the upgraded SDK message type.

**Evidence**

`diagnostics: null`

- Area: API Client
- Tier: Under the hood
- Useful: 1/5
- Signal: 1/5

### Lock contention is now a distinct prompt-history write failure

Prompt-history saves blocked by another process are now tracked separately from real write failures.

**What**

When saving prompt history fails because another process held the lock, it is reported under its own key rather than counted as a generic write failure.

**Details**

- Detected from the error's telemetry code being lock-contended or lock-acquire-failed; the distinct key is `history_save_lock_failed`.

**Evidence**

`history_save_lock_failed`

- Area: Telemetry
- Tier: Under the hood
- Useful: 1/5
- Signal: 1/5

### Environment variables read through one shared accessor

Environment reads go through one shared accessor; same variables, same behaviour.

**What**

Many direct process environment reads now go through the shared environment object instead. Same variables, same behaviour, read through a single coercing accessor.

**Details**

- Covers alt-screen full repaint, bench live counts, TMUX/STY/WT_SESSION detection, ConEmu and TERM_PROGRAM progress reporting, VISUAL/EDITOR, and the custom API key settings row.

**Evidence**

`CLAUDE_CODE_ALT_SCREEN_FULL_REPAINT`

- Area: Environment Variables
- Tier: Under the hood
- Useful: 1/5
- Signal: 1/5

### Plugin install-state writers no longer touch disk

Plugin install-state helpers now report the write they want instead of touching disk themselves.

**What**

The functions that record a plugin version, clear its auto-install flag or remove an installation now return the intended write and its result, such as `{ write: !1, result: "no-installation" }`, leaving persistence to the caller. Marketplace add and remove and cache materialization gained explicit storage parameters.

**Evidence**

`no-installation`

- Area: Plugins
- Tier: Under the hood
- Useful: 1/5
- Signal: 1/5

### Distinct error type for retry-worthy API failures

Retries can now match a dedicated retryable error type separate from timeouts.

**What**

The bundled API client gained an error class with the default message `Retryable error.` and an optional underlying cause, giving retries a type to match on separately from timeouts.

**Evidence**

`Retryable error.`

- Area: API Client
- Tier: Under the hood
- Useful: 1/5
- Signal: 1/5

### Plugin, marketplace and config code passes a storage handle explicitly

Plugin, marketplace and config operations now receive the session's storage explicitly instead of reaching for a global.

**What**

Marketplace listing and updating, plugin enable, disable and install, installed-plugin migration, config saving, stats rebuild, feedback drafts and the resume flow now take the session's storage handle as an argument instead of reading a module-level singleton.

**Details**

- Fetching a marketplace by URL was split: one function returns the fetched data, a separate one writes it to the cache.

**Evidence**

`Saving marketplace to cache`

- Area: Plugins
- Tier: Under the hood
- Useful: 1/5
- Signal: 1/5

### MCP SSE liveness callback renamed to heartbeat

An MCP connection-liveness callback was renamed to heartbeat; nothing behaves differently.

**What**

The SSE transport's liveness probe callback, its setter and its error log were renamed to heartbeat. The log now reads "SSETransport: onHeartbeatProbe handler threw". No behaviour change.

**Evidence**

`SSETransport: onHeartbeatProbe handler threw: `

- Area: MCP
- Tier: Under the hood
- Useful: 1/5
- Signal: 0/5

### Config backup moved into a shared helper

Config backups moved into a shared helper; still one a minute, five kept.

**What**

The config writer no longer inlines its own backup logic; it calls a shared helper before writing. Behaviour is the same: a timestamped copy, at most one a minute, with five kept.

**Evidence**

`Failed to backup config: `

- Area: Configuration
- Tier: Under the hood
- Useful: 1/5
- Signal: 0/5

## Removed

### Auto mode explanation text dropped

The explanation of Auto mode, including its cost and isolation warnings, is no longer in the build.

**What**

The paragraph describing Auto mode, covering the per-tool-call check for risky actions and prompt injection, the slightly higher session cost, and the advice to use it only in isolated environments, is no longer present in this build.

**Evidence**

`Auto mode lets Claude handle permission prompts automatically`

- Area: Permissions
- Tier: You'll notice
- Useful: 2/5
- Signal: 3/5
