# Claude Code v2.1.222

> Claude Code v2.1.222, released 4 Aug 2026 (2026-08-04). 121 entries read out of the shipped bundle. Unofficial, and not affiliated with Anthropic.

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

Fourteen features sit in this build without being switched on. A predicate that would let destructive MCP tools run under remote auto mode is present but disabled, still routing those tools to a permission prompt, and a third command-spawn wrapper picked up the guard that refuses a command resolving only inside the current directory, also off here. The `claude import` command was rewritten into a parser and a rewrite decision but is not wired up and prints a message instead of starting a session, though its Codex config reader now parses TOML with Bun's built-in parser. Also parked: a status footer indicator whose text arrives from the server, an IDE indicator in the footer, SDK stream shutdown that preserves rather than denies parked permission requests, and a telemetry exporter path that stores failed batches as append-only streams keyed by run id.

Commands, skills and plugin manifests now accept a free-form `metadata` object in frontmatter, preserved through parsing and never interpreted. The `remoteControlAtStartup` setting can no longer be enabled by project or local settings files, so cloning a repo cannot change your startup behaviour. MCP connections through the claude.ai proxy that hit a rejected session token now give you a specific error telling you to run /login, visible in the server list, the panel and the reconnect result, instead of a generic auth failure that gets retried. Among the other 64 entries: the stall watchdog now wraps event-stream bodies from first-party, gateway and Bedrock providers, the connection-failure screen names a configured proxy when a check fails through one, resuming into a missing worktree is explained rather than silently continuing without isolation, and a model refusal fallback no longer implies your session model changed.

Permission responses are now matched against the tool they were issued for, so a stale or crossed reply is logged and dropped rather than approving the wrong call. Forked and background agent queries default to requiring the canUseTool callback, which means a hook returning "allow" no longer bypasses it and deny rules can beat a hook allow. Leaving a worktree only moves the permission anchor when the target is already covered, so exit cannot broaden what the session may touch, and worktree cleanup throws instead of changing into a UNC or network-spelled working directory. Resume strips configuration environment variables from copied data and tolerates copy failures, and a permanently refused agent resume now raises an error class that stops the retry loop.

## 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.

### Destructive MCP tools can be included in remote auto-mode

Destructive MCP tools could stop skipping permission prompts in remote auto mode, but that is off.

**What**

A new predicate combines the auto/plan remote mode check with a tool's isDestructive result, so destructive MCP tools still get a permission prompt. It is off in this build.

**Details**

- When on, an MCP ask-override no longer exempts destructive tools from the prompt.
- Gated on tengu_remote_auto_mode_include_destructive_mcp, fallback value in source is false.
- A new mcpServerAskOverride field was added to four permission telemetry payloads.

**Evidence**

`tengu_remote_auto_mode_include_destructive_mcp`

- Flag `tengu_remote_auto_mode_include_destructive_mcp`: Off in both readings (read for one account on one subscription tier against v2.1.222; 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: Permissions
- Names: `tengu_remote_auto_mode_include_destructive_mcp`
- Tier: Not switched on
- Useful: 3/5
- Signal: 4/5
- Present in the build but not switched on

### Parked AskUserQuestion prompts can survive stream shutdown

Questions left open when the stream closes could stay answerable, but that behaviour is switched off for you today.

**What**

When the SDK control stream closes with an AskUserQuestion permission request still parked, the request can now be kept answerable instead of being cancelled and denied.

**Details**

- preserved requests are added to `preservedParkRequestIds`
- telemetry `tengu_auq_park_preserved_at_shutdown`, plus `tengu_auq_park_preserve_reverted` when a hook decision retracts a preserved park
- gated on tengu_cinder_heron with fallback "off"; the helper returns true only when the value resolves to the string "preserve"
- absent a remote value, the old cancel-and-deny behaviour is what you get

**Evidence**

`Preserving parked AskUserQuestion request_id=`, `tengu_cinder_heron`

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

### Unsafe-PATH command resolution guard, off in this build

A guard that would reject commands found only in your current directory stays disabled.

**What**

A third spawn wrapper picked up the same guard as the other two: resolve the command and refuse it if it only resolves inside the current directory.

**Details**

- The predicate that controls all three call paths is a function that returns false in this build, so no command is refused at runtime.
- If enabled, a command found only in the current directory would be rejected as being in an unsafe location.

**Evidence**

`not found or is in an unsafe location (current directory)`

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

### Session state tracks pending branch links

Sessions can now track branch links alongside pull-request links, with detection for git, gh, glab and curl commands.

**What**

Session state carries pending branch links alongside pending PR links, with the PR-created path folded into one helper.

**Details**

- A `pendingBranchLinks` Map was added to the session state object and to both reset paths.
- Sits next to the existing `pendingPrLinks` set.
- A new regex matches git, gh, glab and curl in commands.

**Evidence**

`pendingBranchLinks`

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

### Skill frontmatter metadata is kept on the loaded record

Skills can carry a metadata block in their frontmatter, but nothing uses it yet.

**What**

Plugin skill loading now carries an object-valued `metadata` frontmatter field through into the loaded skill record.

**Details**

- Stored alongside declaredFields rather than being dropped during parse.
- No user-visible behaviour attaches to the field in this build.

**Evidence**

`declaredFields`

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

### Artifacts dialog can dismiss frames with `x`

In the artifacts frames dialog, press x to detach the highlighted frame from your session.

**What**

The frames dialog now takes attachedSlugs and an onDetach callback, marks each row with isAttached, and adds an `x` chord that removes the highlighted frame from the session's list, followed by a confirmation line.

**Details**

- Each frame row renders with an attached indicator so it is clear which frames are currently in play.
- Pressing `x` on the highlighted row detaches it from the session rather than deleting the artifact.
- Dismissal is confirmed inline in the dialog.

**Usage**

`/artifacts` **Evidence** `frame_link_dismiss_dialog`

- Area: Artifacts
- Names: `/artifacts`, `x`
- Tier: Use it now
- Useful: 4/5
- Signal: 0/5

### Byte watchdog covers first-party, gateway and Bedrock streams

Stalled streams are now detected across direct, gateway and Bedrock connections, and you can force it with an env var.

**What**

The rule for wrapping a text/event-stream body in the stall watchdog was widened from a single caller-supplied boolean to name the providers explicitly.

**Details**

- Now instruments firstParty, gateway and anthropicAws responses.
- Gated on tengu_stream_watchdog_default_on, which falls back to true when the flag is unavailable.
- CLAUDE_ENABLE_BYTE_WATCHDOG overrides the decision, and CLAUDE_ENABLE_BYTE_WATCHDOG_BEDROCK covers Bedrock specifically.

**Evidence**

`tengu_stream_watchdog_default_on`

- Flag `tengu_stream_watchdog_default_on`: Not enough to say (read for one account on one subscription tier against v2.1.222; this account: no value returned, anonymous baseline: no value returned, compiled default: off) 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: Streaming
- Names: `CLAUDE_ENABLE_BYTE_WATCHDOG`, `CLAUDE_ENABLE_BYTE_WATCHDOG_BEDROCK`
- Tier: Use it now
- Useful: 4/5
- Signal: 0/5

### Commands, skills and plugin manifests accept a free-form metadata object

You can add a free-form metadata block to command, skill and plugin files that Claude Code keeps but ignores.

**What**

Authors can attach an arbitrary `metadata` object to command and skill frontmatter and to plugin manifests; it is preserved through parsing but never interpreted by Claude Code.

**Details**

- Frontmatter parsing keeps `metadata` when the value is an object; the parsed command shape and its serialization allowlist both include "metadata".
- Plugin manifest validation warns with the fragment "'metadata' must be a free-form object" and states the value will be ignored at load time when it is not an object.
- Both the plugin manifest schema and the skill schema document the field as a free-form map for the author's own use, for example entitlement or catalog fields.
- Metadata keys are no longer folded into the computed `declaredFields` set.

**Evidence**

`Free-form metadata for the plugin author's own use (e.g. entitlement or catalog fields). Preserved on the parsed manifest but not read by Claude Code.`, `'metadata' must be a free-form object`

- Area: Plugins
- Names: `metadata`
- Tier: Use it now
- Useful: 4/5
- Signal: 0/5

### MCP server panel explains a rejected claude.ai session token

An MCP server rejecting your login token now tells you to run /login and reconnect.

**What**

A connected MCP server whose discovery bearer token was rejected now tells you to re-login instead of showing a generic tools-list failure.

**Details**

- A dedicated Issue line instructs you to run /login and then reconnect.
- The generic tools-list error is suppressed in that specific case.

**Evidence**

`claude.ai rejected the session token. Run /login, then reconnect.`

- Area: MCP
- Names: `/login`
- Tier: Use it now
- Useful: 4/5
- Signal: 0/5

### MCP: rejected claude.ai session token is now visible

Your MCP server list now flags a rejected login token, and clears the warning once it recovers.

**What**

A connected MCP server whose discovery bearer token was rejected now says so in the server list and in the reconnect result, rather than the state being tracked only internally.

**Details**

- Message directs you to run /login and then reconnect.
- A successful tools/list now clears discoveryAuthFailure in both MCP client implementations, so a recovered server stops showing the warning.

**Evidence**

`but your claude.ai session token was rejected. Run /login, then reconnect.`

- Area: MCP
- Names: `/login`, `/mcp`
- Tier: Use it now
- Useful: 4/5
- Signal: 0/5

### Dedicated error when claude.ai rejects a session bearer token

If claude.ai rejects your session token, you now get a clear message telling you to run /login instead of endless retries.

**What**

MCP connections proxied through the claude.ai connector now surface a specific error telling you to re-login, instead of a generic auth failure that gets retried.

**Details**

- New `ClaudeAiProxyBearerRejectedError` class with code `CLAUDEAI_BEARER_REJECTED`.
- Message: "claude.ai rejected the session token — it may lack connector scopes or be invalid. Run /login."
- Two retry classifiers return false for it, so it is neither retried nor treated as a generic auth failure.
- Added to the known-error name allowlists, alongside `McpAuthError` and `McpError`.
- Connection failures report `errorCode: "CLAUDEAI_BEARER_REJECTED"` on `tengu_mcp_server_connection_failed`.

**Evidence**

`CLAUDEAI_BEARER_REJECTED`

- Area: MCP
- Names: `/login`
- Tier: You'll notice
- Useful: 4/5
- Signal: 0/5

### Exiting a worktree no longer widens the permission anchor

Leaving a worktree can no longer quietly widen what the session is allowed to touch.

**What**

ExitWorktree and the session-exit path only move the permission anchor when the target is already covered, so leaving a worktree cannot broaden what the session is allowed to touch.

**Details**

- the move requires coverage by the session's live launch anchor and witnesses
- a record-derived widening is refused and logged instead of applied

**Evidence**

`ExitWorktree: not moving the permission anchor to`

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

### Forked and background agent queries require canUseTool by default

Background and forked agents now run your permission callback even when a hook says allow, and deny rules win.

**What**

Forked query tool contexts now default requireCanUseTool to true, so a hook returning "allow" no longer bypasses the permission callback there, and deny rules can now beat a hook allow.

**Details**

- requireCanUseTool is set to true when the caller does not specify it.
- The hook decision path was reordered so deny and ask rules are evaluated before the "hook allowed but canUseTool is required" fallback, letting a deny rule override the hook even when canUseTool is required.

**Evidence**

`requireCanUseTool`

- Area: Permissions
- Names: `canUseTool`
- Tier: You'll notice
- Useful: 4/5
- Signal: 0/5

### Model refusal fallback no longer claims your session model changed

Fallback notices now say only that one response came from another model; your session model is unchanged.

**What**

When a request falls back to another model, the notice explains that only that response came from the other model, and the session model display marks the switch as automatic.

**Details**

- The old "Switched to X" wording is gone; the notice now says the response was generated or completed by the other model and that the session model is unchanged.
- `/status` and the model display append a note that the model was automatically switched.
- The fallback notice is carried into the extracted last-assistant-response text with a ⚠ prefix.

**Evidence**

`This response was generated by ${n} instead. Your session model is unchanged.`

- Area: Models
- Names: `/status`
- Tier: You'll notice
- Useful: 4/5
- Signal: 0/5

### Permission responses are checked against the tool they were issued for

A permission answer meant for a different tool is ignored, so nothing gets wrongly approved.

**What**

A can_use_tool answer that names a different tool than the one being asked about is now ignored, so a stale or crossed response cannot approve the wrong call.

**Details**

- both the SDK control-protocol client and the remote permission bridge record the pending tool name
- the hook and permission response schema gained an optional toolName field
- telemetry distinguishes a match from a mismatch
- in the remote session manager, a mismatch is logged and the local prompt stays up rather than being dismissed, since the worker would drop the answer anyway

**Evidence**

`bridge_permission_toolname_check`, `worker will drop it, keeping prompt`

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

### Proxy hint in the connection-failure screen

If a connection fails through your proxy, the error now names the proxy environment variable to check.

**What**

When a connectivity check fails and the request went through a configured proxy, the error panel now points at the proxy rather than leaving you to guess.

**Details**

- The panel names the environment variable the proxy is configured through and asks you to check that it allows the host.
- A docs link is appended alongside the hint.
- The extra line only appears when a proxy is configured and was actually used.

**Evidence**

`See https://code.claude.com/docs/en/network-config`

- Area: Internals
- Names: `HTTPS_PROXY`
- Tier: You'll notice
- Useful: 4/5
- Signal: 0/5

### Resume no longer inherits a stale config directory or a half-copied file

Resumed sessions no longer inherit a stale config directory or abort on a failed file copy.

**What**

Session-store resume tolerates copy failures and strips configuration environment variables from copied data.

**Details**

- A failed file copy removes the partial destination and logs a skip instead of aborting resume.
- A JSON transformer removes a set of keys plus `CLAUDE_CONFIG_DIR` from any copied `env` object, so the resumed session does not pick up the recorded config directory.

**Evidence**

`sessionStore resume: skipping `

- Area: Sessions
- Names: `CLAUDE_CONFIG_DIR`
- Tier: You'll notice
- Useful: 4/5
- Signal: 0/5

### Resuming into a missing or unverifiable worktree is now explained

Resuming into a missing worktree now warns you, and headless runs exit with an error instead of continuing.

**What**

Worktree re-entry on resume returns a structured outcome that the session reports, instead of quietly continuing without isolation.

**Details**

- a vanished worktree clears the binding and continues without isolation
- an unverifiable or rejected pin keeps the binding
- interactive sessions show a warning message; headless and print paths write an error to stderr and exit non-zero rather than continuing unisolated
- telemetry `tengu_worktree_resume_root_rejected` carries the reason and a poisoned flag

**Evidence**

`The worktree binding has been cleared.`

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

### claude.ai MCP proxy surfaces a rejected session token

When your claude.ai session token is rejected, you now get a clear error telling you to run /login.

**What**

A 401 from the claude.ai proxy that a token refresh cannot fix now becomes a typed error telling you to run /login, instead of silently returning the original failed response.

**Details**

- error code CLAUDEAI_BEARER_REJECTED, recognised by connect, tools/list, prompts/get and skills/list
- sets `discoveryBearerRejected` on the client and skips the usual "degraded" telemetry
- emits mcp_connect_claudeai_bearer_rejected and mcp_reconnect_bearer_rejected
- the failed entry is evicted from the connection cache so a later attempt retries

**Evidence**

`claude.ai rejected the session token — it may lack connector scopes or be invalid. Run /login.`

- Area: MCP
- Names: `/login`
- Tier: You'll notice
- Useful: 4/5
- Signal: 0/5

### remoteControlAtStartup can no longer be turned on by repo-scoped settings

A cloned repo's settings can no longer switch on Remote Control at startup; only your own settings can.

**What**

A checked-in project or local settings file can no longer enable Remote Control at startup, closing off a path where cloning a repo changed your startup behaviour.

**Details**

- The setting is read through the security-sensitive settings path, so only user, policy or flag scope can enable it.
- Project and local settings can still disable it: a false value wins.
- A true value at repo scope is ignored and logged.
- `projectSettings` is ignored entirely when it aliases user settings.

**Evidence**

`repo-scoped settings cannot enable Remote Control; set it at user scope (/config)`

- Area: Permissions
- Names: `remoteControlAtStartup`
- Tier: You'll notice
- Useful: 4/5
- Signal: 0/5

### /btw answers can now show a refusal-fallback notice

If a /btw answer came from a fallback model after a refusal, you now see a warning.

**What**

When a /btw question is answered by a fallback model after a refusal, the panel shows a warning notice above the answer naming the swap.

**Details**

- The panel accepts `initialFallbackNotice` and stores a `fallbackNotice` alongside each question/response pair in its history
- The notice renders prefixed with a warning sign, above the answer
- Text comes from a `refusal_fallback` payload returned with the response, carrying `original_model`, `fallback_model` and `content` (truncated)

**Usage**

`/btw what does this repo use for tests?` **Evidence** `initialFallbackNotice`

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

### Bad `metadata` in frontmatter and manifests now warns

You now get a warning when metadata in a skill, command, plugin or marketplace file is the wrong type.

**What**

Authoring a skill, command, plugin manifest or marketplace entry with a non-object `metadata` value produces a validation warning instead of silent loss.

**Details**

- The warning names the type actually found and states that the value is dropped or ignored at load time.
- Applies to skill and command frontmatter, plugin manifests, and marketplace plugin entries.

**Evidence**

`'metadata' must be a mapping (key: value pairs); got `

- Area: Plugins
- Names: `metadata`
- Tier: Use it now
- Useful: 3/5
- Signal: 0/5

### Frame strip points to `/artifacts` when frames overflow

When too many frames fit on the row, you now see a pointer to /artifacts instead of a bare count.

**What**

The frame row layout now reserves space for a browse hint instead of showing a bare "+N" counter when there are more frames than fit.

**Details**

- The hint names the command that lists everything, so overflow is actionable rather than just a count.
- Layout reserves the room up front, so the hint does not push frames off the row.

**Evidence**

`/artifacts to see all`

- Area: Artifacts
- Names: `/artifacts`
- Tier: Use it now
- Useful: 3/5
- Signal: 0/5

### Host-managed provider sessions take model policy from the parent process

Setting CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST makes the parent process dictate your model settings, ignoring your settings file.

**What**

When Claude Code runs under a host that manages the provider, settings-file model configuration is ignored in favour of policy handed down by the parent process.

**Details**

- Settings resolution gained `hostManagedProvider: te.CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST`.
- With that env var set, `model`, `fallbackModel`, `modelOverrides` and provider env vars are stripped from the policy view.
- A `hostModelOverlay` built from parent-managed settings is applied in their place; admin `modelOverrides` are stashed and re-applied to enforcement.
- The managed-settings source enum gained a "parent" value, returned when a parent slice or host model overlay exists.
- Inactive unless `CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST` is set.

**Evidence**

`hostManagedProvider`

- Area: Settings
- Names: `CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST`
- Tier: Use it now
- Useful: 3/5
- Signal: 0/5

### Server-driven footer indicator

The footer can display a short message sent by the server, trimmed to 32 characters.

**What**

The status footer can show a short indicator whose text arrives from the server in the client-data payload.

**Details**

- Read from the client-data field `footer_indicator`.
- Text is stripped of control characters, trimmed, truncated to 32 characters and clamped before display.
- Resolves to null when the server sends nothing parseable, in which case nothing renders.

**Evidence**

`footer_indicator`

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

## New Features

### Artifact comment auto-reply understands "sent to Claude" lanes

Artifact comment threads now track which comments were actually sent to you, and re-sending one triggers a reply.

**What**

Comment threads are now classified per comment into human, agent and unknown lanes, and sending or re-sending a comment to Claude is itself a trigger for auto-reply rather than only a new comment being one.

**Details**

- each comment carries a lane; a `toClaudeAt` timestamp marks it as sent to Claude
- new trigger kind `redesignated` covers existing comments newly toggled to Claude, tracked through a per-thread `sentToClaudeAt` map
- the composed prompt now explains the row heads `[human]`, `[assistant]`, `[human, sent to you]` and `[unverified lane]`

**Evidence**

`A human sent an EXISTING comment in the thread to you on an artifact you published`

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

### Artifact tool output exposes sent-to-Claude and awaiting-reply flags

Threads with comments addressed to you sort first, and each comment shows whether it awaits your reply.

**What**

Thread listings sort threads with comments addressed to Claude first, and individual comments now report whether they were sent to Claude and whether they are still waiting on a reply.

**Details**

- per-comment fields `sent_to_claude`, `sent_to_claude_degraded` and `awaiting_reply`
- size-capped elisions state how many hidden comments were sent to Claude, including a count for those whose addressed status could not be read

**Evidence**

`addressed-status-unreadable`

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

### Skill import from other agents refuses unsafe copies

Importing a skill from another agent now skips bad copies and tells you why instead of silently breaking.

**What**

Importing a skill from another agent now inspects the source directory and skips the copy with an explanation instead of producing a skill Claude Code cannot load.

**Details**

- Skips when SKILL.md is larger than the size Claude Code loads.
- Skips when SKILL.md starts with `---` YAML frontmatter, which Codex treats as plain text.
- Skips when the directory contains subdirectories that Claude Code would adopt as plugin components.
- Skips when several case or encoding variants of SKILL.md exist in the same directory, and tells you to copy it manually.
- A copy that proceeds rewrites SKILL.md and removes the destination if the copy fails part way.

**Evidence**

`skill directory has multiple SKILL.md case/encoding variants \u2014 copy the skill manually`

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

### /btw side questions show a fallback-model notice

Side questions now show a banner naming the original and fallback model instead of switching silently.

**What**

When a side question falls back to a different model, the answer is now preceded by a warning banner naming the original and fallback models instead of silently switching.

**Details**

- The side-question flow returns a `refusalFallback` object carrying the original model, the fallback model and the notice content alongside the response.
- The UI renders the banner above the answer only when a fallback is present.
- The control-request response carries the same information as `refusal_fallback` with `original_model`, `fallback_model` and `content` fields.

**Evidence**

`fallbackNotice`

- Area: Slash Commands
- Names: `/btw`
- Tier: You'll notice
- Useful: 3/5
- Signal: 0/5

### Artifact comments now mark which comments were sent to Claude

Comment rows are now labelled "sent to you" so you can tell requests apart from side chatter.

**What**

Comment rows carry an explicit label when the comment was addressed to Claude, so it is clear which parts of a thread are a request to you and which are other people talking to each other.

**Details**

- Rows can show a ", sent to you" label, with an "awaiting reply" variant and a variant for when the status cannot be read.
- Threads containing such comments are sorted to the top of the list, and the header count mentions them.
- The tool description now states that only labelled comments are addressed to Claude.
- The anti-injection preamble gained explicit rules that the attribution bracket only ever appears at the start of a row.

**Evidence**

` Threads with comments sent to you are listed first; list order is not chronological.`

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

### Remote-bridge presence heartbeats can self-heal the transport

Remote sessions now rebuild a stuck connection when heartbeats keep failing but data still flows.

**What**

The v2 remote-control transport now counts consecutive presence heartbeat failures and, when heartbeats keep failing while the SSE read stream still looks alive, closes with a new code 4093 and rebuilds the transport rather than sitting on a dead connection.

**Details**

- New close code 4093 with human-readable reason text explaining that presence heartbeats to the server kept failing.
- New internals: resetHeartbeatStreak, onHeartbeatLost, and isReadStreamRecentlyAlive, which is what distinguishes a stalled heartbeat from a genuinely dead read stream.
- Gated on tengu_bridge_selfheal_heartbeats; the fallback value in source is true, with the real value coming from remote config.
- Skipped entirely when the session is outboundOnly.

**Evidence**

`presence heartbeats to the server kept failing (code 4093)`

- Flag `tengu_bridge_selfheal_heartbeats`: Off by default, switched on for this account (read for one account on one subscription tier against v2.1.222; this account: on, anonymous baseline: on, compiled default: off) 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 Bridge
- Names: `tengu_bridge_selfheal_heartbeats`
- Tier: You'll notice
- Useful: 2/5
- Signal: 0/5

### Remote bridge self-heals when heartbeats fail but SSE is alive

The remote bridge tears down half-dead sessions so the connection rebuilds instead of hanging.

**What**

The v2 remote bridge can now detect the case where heartbeats keep failing while the SSE read stream is still delivering data, and tears the session down so the transport is rebuilt instead of sitting in a half-dead state.

**Details**

- Session state tracks `consecutiveHeartbeatFailures` and `heartbeatStreakStartedAtMs`, plus a new `onHeartbeatLost` callback.
- Liveness is judged by a new `isReadStreamRecentlyAlive()` helper backed by `lastLivenessEvidenceAtMs`.
- On trigger the session closes with code 4093 and logs "[bridge:repl] CCR v2: heartbeats failing while SSE healthy — closing for transport rebuild".
- Wired through a `selfHealHeartbeats` option read from the `tengu_bridge_selfheal_heartbeats` gate, whose fallback is true; the real value comes from remote config.

**Evidence**

`tengu_bridge_selfheal_heartbeats`

- Flag `tengu_bridge_selfheal_heartbeats`: Off by default, switched on for this account (read for one account on one subscription tier against v2.1.222; this account: on, anonymous baseline: on, compiled default: off) 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 Bridge
- Names: `tengu_bridge_selfheal_heartbeats`
- Tier: You'll notice
- Useful: 2/5
- Signal: 0/5

## Improvements

### MCP server list shows a token-rejected state

The MCP server list now shows a distinct line when a server failed because your session token was rejected.

**What**

In the MCP status UI, a server that is connected but whose discovery failed because the claude.ai session token was rejected gets its own line rather than the generic tools-fetch-failure line.

**Details**

- rendered only for the bearer-rejected case; other discovery failures keep the existing wording

**Evidence**

`connected · session token rejected`

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

### Thread rendering keeps summoning comments and marks new truncation kinds

Comments that triggered a turn are always kept in view instead of being trimmed away.

**What**

The thread renderer was replaced so the comments that actually caused this turn are never dropped to fit the budget.

**Details**

- always keeps the newest comment plus every comment that summoned this turn, budgeting characters across them
- new markers `[summoning comment truncated]` alongside `[newest comment truncated]` and `[N comment(s) elided]`
- `summoning_truncated` added to the compose_thread_elided telemetry

**Evidence**

`[summoning comment truncated]`

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

### Worktree git commands run with a scrubbed git environment

Stray git environment variables in your shell can no longer misdirect worktree operations.

**What**

Worktree plumbing no longer inherits ambient git environment variables, so a stray GIT_DIR or a repo fsmonitor cannot redirect worktree operations.

**Details**

- covers create, remove, lock, status, reset, rev-parse and sparse-checkout
- blanks GIT_DIR, GIT_WORK_TREE, GIT_INDEX_FILE, GIT_CONFIG_COUNT, GIT_CONFIG_PARAMETERS, the per-index GIT_CONFIG_KEY_n and GIT_CONFIG_VALUE_n pairs, and more
- all calls go through the shared `-c core.hooksPath=/dev/null -c core.fsmonitor=` prefix

**Evidence**

`GIT_ALTERNATE_OBJECT_DIRECTORIES: void 0`

- Area: Worktrees
- Names: `GIT_DIR`, `GIT_WORK_TREE`, `GIT_INDEX_FILE`
- Tier: You'll notice
- Useful: 3/5
- Signal: 0/5

### Worktree creation validates the resulting root before use

New worktrees are checked before use, and creation fails loudly if the directory is unusable.

**What**

After creating a worktree, Claude Code checks that the new directory is acceptable as an isolation root and fails loudly if it is not.

**Details**

- applies to every creation path: interactive, agent, hook-based and resumed
- throws on rejection and records telemetry `git_worktree_create_root_rejected`

**Evidence**

`git_worktree_create_root_rejected`

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

### Parked agents refuse to resume into a worktree that vanished

A parked agent refuses to resume if its worktree disappeared, instead of landing in the wrong directory.

**What**

Resuming a parked or isolated agent verifies the recorded worktree first and refuses rather than silently landing somewhere else.

**Details**

- stats and verifies the recorded worktree and touches it
- falls back to a covered directory only when the session's fences cover it
- otherwise raises a terminal error saying the agent cannot be resumed

**Evidence**

`This agent cannot be resumed: its worktree`

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

### Plugin and skill search now degrade to empty on 403

Plugin and skill search shows nothing rather than an error when your account lacks access.

**What**

A 403 from the plugin and skill search or list routes returns an empty result instead of surfacing an error, so an unentitled account sees nothing rather than a failure.

**Details**

- The degraded path logs the route and status and records a `not_entitled` telemetry code.
- Other 4xx responses still throw.
- The error envelope parser now tolerates a response missing `type` or `message`.

**Evidence**

`[plugin-skill-list] degraded to empty: list-plugins 403`

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

### Subagent models outside the allowlist step down within their family

Subagents asking for a disallowed model now drop to the newest allowed model in the same family.

**What**

A subagent or teammate asking for a model that is not in `availableModels` now falls to the newest allowed model in the same family rather than straight to the parent or default model.

**Details**

- Only applies when a same-family allowed model exists; otherwise the old parent/default fallback still runs.
- Emits telemetry code `family_alias_stepped_down` and a matching warning log.

**Evidence**

`family_alias_stepped_down`

- Area: Models
- Names: `availableModels`
- Tier: You'll notice
- Useful: 3/5
- Signal: 0/5

### Messages from other sessions get a sanitized sender label

Messages arriving from another session now show who sent them plus a chord to expand.

**What**

Transcript rows for messages arriving from another session render through a shared component that names the sender and offers an expand chord.

**Details**

- Shows "Message" or "N messages" followed by "from @<name>" plus the expand chord.
- The name is restricted to letters, digits, dot, underscore and hyphen, and truncated with an ellipsis.
- SendMessage tool labels and teammate summaries use the same sanitizer.

**Evidence**

`from @`

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

### Deferred PR-existence checks retry on later git commands

Claude now re-checks later whether your pull request was actually created, instead of giving up after one try.

**What**

Detection of a newly created pull or merge request no longer depends on a single check right after the create command.

**Details**

- Pending checks are tracked per cwd plus branch with an attempt counter, in a bounded map.
- A successful `gh pr create` or `glab mr create` clears the entries for that cwd.
- Later matching git commands re-check pending branches until the attempt cap is reached.

**Evidence**

`tengu_git_operation`

- Area: Git Integration
- Names: `gh pr create`, `glab mr create`
- Tier: You'll notice
- Useful: 3/5
- Signal: 0/5

### Agent-to-agent messaging gets a classifier review path

Messaging another agent may now require classifier review instead of being automatically allowed.

**What**

The tool for messaging another agent no longer auto-allows in every permission mode; in some modes it defers to classifier review.

**Details**

- The permission check can now return a passthrough behavior with a message stating that classifier review is required.
- Recipient names must match a single-line pattern.
- Over-long summaries are truncated rather than rejected outright.
- The classifier input string now carries the summary, shutdown reason, request id and plan feedback.

**Evidence**

`Message to another agent requires classifier review.`

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

### Skill import warning reworded

The skill import warning now spells out that bundled scripts and instructions go live once you import.

**What**

The confirmation shown before importing a plugin or skill no longer talks about allowed-tools frontmatter, and instead spells out that bundled scripts and body instructions become active once imported.

**Details**

- Warning now asks you to review SKILL.md and its directory, not just the file.
- States that the import is unchecked by default.

**Evidence**

`bundled scripts and body instructions become active in Claude Code. Unchecked by default; review SKILL.md and its directory before importing.`

- Area: Skills
- Names: `SKILL.md`
- Tier: You'll notice
- Useful: 3/5
- Signal: 0/5

### Subagent model fallback says why it fell back

When a subagent can't use its requested model, you now see why it fell back.

**What**

When a subagent cannot get the model it asked for, the notify callback now carries a reason distinguishing a same-family step-down from inheriting the parent's model, and the message shown downstream differs accordingly.

**Details**

- Reason is either family_step_down, meaning a fallback within the same model family was picked, or parent_inherit, meaning the subagent fell back to the parent session's model.

**Evidence**

`family_step_down`

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

### Remote control enable/disable runs as a tracked async task

Toggling remote control now carries pending permission prompts over to the new session instead of losing them.

**What**

Handling of the remote_control control request is now wrapped in a single async operation with its own error handling, and it hands off state to the newly created remote session instead of dropping it.

**Details**

- Old session is torn down only after its callbacks are cleared.
- Pending permission requests and pending user-dialog requests are replayed to the new remote session.
- The initial name supplied in the request is preserved across the swap.

**Evidence**

`getPendingUserDialogRequests()`

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

### Git invocations hardened with sanitized args and environment

Many more git commands now run with a scrubbed environment so stray variables can't redirect them.

**What**

Many more git call sites now prepend the frozen safety arguments and run with an environment scrubbed of git overrides, closing off paths where inherited env vars could redirect a git command.

**Details**

- Newly covered call sites include rev-list, ls-files --others --ignored, worktree remove --force, and config core.hooksPath.
- Sanitized environment strips GIT_DIR, GIT_WORK_TREE, GIT_INDEX_FILE, and the GIT_CONFIG_KEY_n / GIT_CONFIG_VALUE_n family, among others.
- Two additional `git diff` call sites now also pass --no-ext-diff and --no-textconv.

**Evidence**

`core.hooksPath=/dev/null`

- Area: Git Integration
- Names: `GIT_DIR`, `GIT_WORK_TREE`, `GIT_INDEX_FILE`
- Tier: You'll notice
- Useful: 3/5
- Signal: 0/5

### Skill refusal message tells the model not to work around it

Skills you mark as user-only now tell Claude not to reimplement them by hand.

**What**

When the model tries to invoke a skill marked as user-only, the returned error now appends an explicit instruction against achieving the same thing another way.

**Details**

- Applies to skills with disable-model-invocation set.
- The added sentence closes the loophole of the model reading the skill and re-implementing its steps by hand.

**Evidence**

`Do not replicate this skill's workflow by other means`

- Area: Skills
- Names: `disable-model-invocation`
- Tier: You'll notice
- Useful: 3/5
- Signal: 0/5

### Model refusal fallback updates the shown model

When a turn falls back to a different model after a refusal, the display now shows the model actually used.

**What**

When a turn falls back after a model refusal, the agent's displayed model is rewritten to the model actually used, and the fallback message now carries a scope.

**Details**

- A new agent event kind propagates turn effort together with the turn model.
- The async-agent reader applies system/model_refusal_fallback by rewriting the agent's model rather than leaving the old one on screen.
- The optional scope field was added at three emit sites and in the parser.

**Evidence**

`agent-turn-effort`

- Area: Model Fallback
- Tier: You'll notice
- Useful: 3/5
- Signal: 0/5

### Resume snapshots carry user settings without plugin keys

Resumed sessions now carry your settings files along, with plugin registrations stripped out.

**What**

The temp directory built to resume a stored session now copies settings.json and cowork_settings.json next to .claude.json, with plugin registration stripped.

**Details**

- enabledPlugins and extraKnownMarketplaces are deleted from the copied settings.
- The projects directory inside the snapshot is created with mode 448.

**Evidence**

`cowork_settings.json`

- Area: Sessions
- Names: `settings.json`, `cowork_settings.json`, `enabledPlugins`, `extraKnownMarketplaces`
- Tier: You'll notice
- Useful: 3/5
- Signal: 0/5

### Git diffs ignore external diff drivers and textconv filters

Diffs now ignore your repo's custom diff drivers, so Claude sees the real file contents.

**What**

Diff reads now pass `--no-ext-diff` and `--no-textconv`, so a repo's own git configuration cannot change the content Claude sees when computing patches.

**Details**

- New exported constant `RAW_BLOB_DIFF_FLAGS = ["--no-ext-diff", "--no-textconv"]`.
- Spread into the `git diff` invocations used for whole patches and per-file hunks.
- Includes the `--no-optional-locks diff` calls, which previously passed no such flags.

**Evidence**

`RAW_BLOB_DIFF_FLAGS`

- Area: Git
- Names: `--no-ext-diff`, `--no-textconv`
- Tier: You'll notice
- Useful: 3/5
- Signal: 0/5

### Shell tools get an explicit worktree isolation root

Agent commands now have a concrete worktree boundary, so escapes get blocked with a clearer log line.

**What**

Bash and shell execution paths now take an `isolationRoot` so the checks that keep agent commands inside their worktree have a concrete boundary to compare against.

**Details**

- `isolationRoot` is resolved from the agent worktree, falling back to the session worktree path.
- Used by the checks that block commands whose cwd escapes the isolation worktree, and those that redirect git into the shared checkout.
- Blocked commands log with `isolationRoot=` in the message and report `tengu_agent_worktree_cwd_escape_blocked`.
- The field itself is new in this build.

**Evidence**

`isolationRoot`

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

### Turn effort display now tracks the model the turn is running on

The effort label in your status line stays correct when the model changes mid-turn.

**What**

The effort label in the status line is computed against the model actually running the turn, so it stays correct after a mid-turn model change or a fallback.

**Details**

- The agent status store gained `turnModel` alongside `turnEffort`
- `setTurnEffort(effort, model)` is called at turn start with the resolved agent model, and again on `query_model_change` and on fallback-model events
- Cleared with `setTurnEffort(null, null)` at turn end
- The spinner/status component takes the new `turnModel` prop and uses it when computing the label

**Evidence**

`setTurnEffort`

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

### Model refusal fallback banner now also shown in non-main threads

You now get told when a subagent swaps models after a refusal, not just the main thread.

**What**

A refusal-driven model swap that happens inside a subagent or other non-main thread now surfaces a banner, where previously only main-thread swaps did.

**Details**

- The refusal-fallback result gained `bannerScope`, set to "session" for main-thread swaps and "local" otherwise
- Non-main-thread turns emitting local scope now show the banner
- The emitted `model_refusal_fallback` system message uses different wording depending on the scope

**Evidence**

`bannerScope`

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

### Plugin sync list failures carry HTTP status and 403 is not retried

Plugin syncing fails fast on a forbidden response and tells you the HTTP status.

**What**

A forbidden response when listing plugins fails fast instead of burning through the retry schedule, and the failure now reports which HTTP status caused it.

**Details**

- The list-plugins error envelope returns `status` along with `success: !1`, and the status is included in the `plugins_sync_list_error` warning
- The retry wrapper returns immediately when `t.status === 403` rather than sleeping and retrying
- A sibling retry helper was simplified to test `t.ok` only

**Evidence**

`plugins_sync_list_error`

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

### Bridge recovery has a rebuild budget and richer failure telemetry

A remote bridge that cannot recover now stops retrying after a budget and tells you why.

**What**

Recovery from 401, 4091 and 4093 transport closes is now table-driven, and repeated transport rebuilds are capped so a bridge that cannot heal stops retrying and says why.

**Details**

- each close code maps to its own reconnect detail, cause, diagnostic, terminal-vs-retry fetch failure classification and recovered code
- 4093 rebuilds within a one hour window are budgeted; exceeding the budget stops recovery with an explicit message
- new presence telemetry codes: recovery_exhausted, heartbeat_budget_exhausted, recovery_no_oauth_token, recovery_reauth_required, recovery_credentials_fetch_failed, recovery_credentials_rejected, recovery_abandoned_at_teardown, bridge_connect_died_before_presence

**Evidence**

`presence heartbeats to the server kept failing after repeated reconnects`

- Area: Remote Bridge
- Tier: You'll notice
- Useful: 2/5
- Signal: 0/5

### Remote-control bridge heals a socket whose heartbeats keep failing

A remote connection with failing heartbeats now rebuilds itself instead of hanging on a half-dead socket.

**What**

The CCR v2 transport now spots heartbeats failing repeatedly while the SSE read stream still looks healthy, and closes the socket with code 4093 so the bridge rebuilds the transport instead of sitting on a half-dead connection.

**Details**

- close code 4093 has its own recovery entry reporting `reconnectingDetail: "presence heartbeats failing — reconnecting"`
- a successful rebuild emits the presence code `recovered_heartbeat_4093`
- gated on tengu_bridge_selfheal_heartbeats whose in-source fallback is true, so absent a remote value it runs
- additionally requires !outboundOnly

**Evidence**

`[bridge:repl] CCR v2: heartbeats failing while SSE healthy — closing for transport rebuild`

- Flag `tengu_bridge_selfheal_heartbeats`: Off by default, switched on for this account (read for one account on one subscription tier against v2.1.222; this account: on, anonymous baseline: on, compiled default: off) 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 Bridge
- Names: `tengu_bridge_selfheal_heartbeats`
- Tier: You'll notice
- Useful: 2/5
- Signal: 0/5

### Auto-reply stands down when lane or send-status data is unreadable

Auto-reply now stays quiet when it cannot tell who wrote a comment or whether it reached Claude.

**What**

Rather than guessing, auto-reply now defers when it cannot read who wrote a comment or whether it was sent to Claude.

**Details**

- new defer notices: `activation_read_absent` (thread no longer shows as activated), `newest_lane_unknown` (newest comment's author lane unreadable, or an unknown-lane comment follows a sent-to-Claude human comment), and `fresh_trigger_degraded`
- each reported once per thread through the existing artifact_comments_autoreact telemetry

**Evidence**

`artifact_comments_autoreact`

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

### Artifact auto-reply failure notices no longer say "@claude"

Failure replies on artifacts no longer tell viewers to mention @claude, matching how sending actually works.

**What**

The canned edit-failure replies were reworded to match the send-to-Claude mechanism instead of telling viewers to mention @claude.

**Details**

- affects the replies posted when an auto-reply edit fails

**Evidence**

`send a comment to Claude again later to retry.`

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

### Isolation refusal messages distinguish session from agent

Isolation refusal messages now say whether the session or the agent is the one confined.

**What**

The refusals you get when a shell cwd or file edit escapes a worktree now name the right subject.

**Details**

- picks between "This session is isolated…" and "This agent is isolated…" based on whether the pin is the session's own worktree, with a matching possessive
- shell cwd and file edit paths now share one escape-detection routine

**Evidence**

`possessive: "a worktree-isolated agent's"`

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

### Shutdown waits for in-flight remote-control work

Quitting waits up to 30 seconds for pending remote-control work instead of cutting it off.

**What**

Teardown now gives a pending remote-control operation time to finish before tearing down the rest of the session.

**Details**

- Waits up to 30 seconds and swallows the timeout rather than failing shutdown.
- Runs before MCP client cleanup and session state clearing.

**Evidence**

`remote_control operation still pending at teardown`

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

### Agent-proxy tunnel now times out waiting for HELLO_ACK

A stalled agent proxy tunnel now fails quickly with a clear reason instead of hanging in limbo.

**What**

A WebSocket tunnel that never gets its protocol v2 negotiation acknowledged now fails fast instead of continuing in an undefined state.

**Details**

- Connecting arms a `helloAckDeadline`; if the server does not acknowledge protocol v2 within the open timeout, negotiation fails with "tunnel protocol v2 negotiation failed" and a reason built from the fragment "no HELLO_ACK within" plus the elapsed limit.
- An ack carrying an unexpected version is rejected, and the connection is closed with a 502 "Bad Gateway".
- The deadline is cleared both on receiving HELLO_ACK and on teardown.
- A new telemetry event `agent_proxy_request_v2_not_acked` fires on the failure path.

**Evidence**

`requires tunnel protocol v2; the server did not acknowledge the negotiation (`, `agent_proxy_request_v2_not_acked`

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

### Connectivity check rewritten on fetch, with a timeout and proxy awareness

Connection checks now time out properly and record whether your proxy was used.

**What**

The hello-endpoint probe used by the connection diagnostics now runs on fetch with an explicit timeout and reports whether a proxy was involved.

**Details**

- Uses `AbortSignal.timeout` rather than the previous HTTP client, and cancels the response body.
- A timed-out probe is reported as a distinct error rather than a generic failure.
- The result records `usedProxy` so the failure UI can tell whether a configured proxy was actually in play.

**Evidence**

`usedProxy`

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

### model_refusal_fallback events gain a scope field

Model fallback events now say whether the swap affects your whole session or just one subagent.

**What**

Fallback events now distinguish a session-wide model swap from a one-off fallback in a subagent or side question, so consumers can tell how far the change reaches.

**Details**

- The SDK schema adds an optional `scope` enum with values "session" and "local".
- "session" means the main thread swapped models for the rest of the session; "local" means only a subagent, a /btw side question or a background fork fell back.
- The field is absent from older CLIs and should be treated as "session" when missing.
- The event builder sets `scope` and selects different wording for local fallbacks.

**Evidence**

`'local': a subagent / side-question (/btw) / background fork fell back`

- Area: Elsewhere
- Names: `/btw`
- Tier: Under the hood
- Useful: 2/5
- Signal: 0/5

### Artifact comments use server delivery timestamps

Artifact comments track when the server delivered them, so you should stop seeing the same comment twice.

**What**

Comment state now records when the server actually handed a comment to Claude, and the client uses that instead of local bookkeeping to decide what has already been shown.

**Details**

- The comment schema gained sent_to_claude, sent_to_claude_degraded and awaiting_reply.
- The client maps the server's to_claude_at into toClaudeAt and toClaudeAtDegraded.
- A helper parses the timestamp and returns null when it cannot be parsed, so an unreadable value is treated as not-yet-delivered rather than crashing.

**Evidence**

`to_claude_at`

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

### Teammate summaries are trimmed, truncated and colour-checked

Long teammate summaries get trimmed instead of breaking the status line or rejecting the message.

**What**

Summary and failureReason text from teammates now passes through a trim-and-truncate helper before it reaches the teammate XML tag or the status line.

**Details**

- The tag's color attribute is validated against the known colour list rather than escaped and passed through.
- SendMessage input repair gained a case that truncates an over-long summary instead of rejecting the call.

**Evidence**

`truncate_summary`

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

### Codex import rejects oversized skills and risky command bodies earlier

Importing from Codex now rejects oversized skills and risky command bodies up front instead of creating broken entries.

**What**

The Codex importer now refuses more source files before conversion instead of producing entries Claude Code would not load.

**Details**

- SKILL.md files above the size Claude Code will load are rejected, checked both raw and after utf-8 re-encoding.
- A new dangling-shell-exec-marker check runs over command bodies.
- The [features] block goes through a shared helper before being listed as unmappable.

**Evidence**

`Claude Code skips skills over`

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

### Codex import explains shell-exec marker refusals

Codex import now tells you exactly why a command was skipped and to port it manually.

**What**

Three new refusal strings spell out why a command was not imported when its body mixes shell execution markers with argument placeholders.

**Details**

- Covers dangling markers, placeholders combined with backticks or `!`, and shell blocks containing `!` next to argument placeholders.
- Each reason tells the user to port the command manually rather than silently dropping it.

**Evidence**

`Contains a dangling shell-exec marker that argument substitution could complete into a live command at invocation time. Port it manually.`

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

### Bridge attestation warnings are reported once per event

A failing device attestation now warns you once instead of spamming repeated notices.

**What**

The attestation notice emitter now keys on event_id and skips events it has already reported, so a single bad event no longer produces repeated warnings.

**Details**

- The set of seen event ids is bounded.
- The hint about re-pairing a device is no longer restricted to control_request payloads and now shows for any attestation failure.
- A drop-sender notice writer is registered on the remote-io transport and unregistered when it closes.

**Evidence**

`Re-pair the sending device in Trusted Devices.`

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

### Design OAuth refresh lock holds longer and reports compromise

Slow token refreshes no longer trip the lock early, cutting spurious login hiccups.

**What**

The lock guarding OAuth token refresh tolerates slower refreshes and lets the holder find out if its lock was taken away mid-flight.

**Details**

- Stale window raised from 10s to 60s, with a 5s update interval.
- The callback now receives an object exposing `isCompromised()`.
- Lock contention throws a dedicated error class rather than a bare `Error`.

**Evidence**

`Design OAuth refresh lock compromised`

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

### Screen-reader rendering erases trailing characters in place

Screen-reader mode repaints less, so assistive tech hears less redundant output.

**What**

In screen-reader mode the renderer can now erase trailing characters instead of repainting the line, cutting redundant output for assistive tech.

**Details**

- Diffing tracks a new `prevScreenReaderParkDeclared` flag.
- The flag guards a new branch taken when the previous frame had an explicitly declared cursor park.
- Only active in screen-reader mode, under `INK_SCREEN_READER` / `CLAUDE_CODE_ACCESSIBILITY`.

**Evidence**

`prevScreenReaderParkDeclared`

- Area: Elsewhere
- Names: `INK_SCREEN_READER`, `CLAUDE_CODE_ACCESSIBILITY`
- Tier: You'll notice
- Useful: 2/5
- Signal: 0/5

### Ultraplan notification text shortened

The ultraplan banner now just tells you it's ready and which key to press.

**What**

The web-ultraplan banner now says the plan is ready and tells you which key to press, replacing the older "Monitor progress in Claude Code on the web" wording.

**Details**

- Old text was retired rather than kept alongside the new form.

**Evidence**

`ultraplan ready`

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

### Worktree git guard message adapts its wording

The refusal message for git commands escaping a worktree now words itself to fit the context.

**What**

The refusal shown when a git command would escape a worktree now derives the noun and possessive it uses rather than always saying "This agent" and "a worktree-isolated agent's".

**Details**

- Same refusal behaviour, only the phrasing is now computed from context.

**Evidence**

`Refusing to run it — `

- Area: Git Integration
- Tier: Under the hood
- Useful: 1/5
- Signal: 0/5

### Teammate count falls back to a generic label

Collapsed teammate panels say "teammate" instead of showing a blank label.

**What**

Collapsed teammate status panels now show a generic label when no specific one is available, instead of rendering an empty descriptor.

**Details**

- Two collapsed-panel renderers pass `fallbackLabel: "teammate"` to the count component.

**Evidence**

`fallbackLabel: "teammate"`

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

### Telemetry model-name sanitizer accepts a bracketed context suffix

Telemetry now records long-context model names properly instead of labelling them nonconforming.

**What**

Dated model ids that carry a 1m-context suffix are now reported verbatim in telemetry instead of being replaced with "nonconforming".

**Details**

- The gate regex widened from `^[A-Za-z0-9._:[\]-]{1,91}@\d{8}$` to allow an optional trailing `[<digits>m]` group
- Model identifiers that still fail the pattern continue to report as "nonconforming"

**Evidence**

`nonconforming`

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

## Bug Fixes

### File watcher no longer emits errors nobody is listening for

File watching errors no longer blow up as unhandled crashes when nothing is listening for them.

**What**

The watcher checks the error listener count before emitting, so a watch error or an error during a pending write can no longer surface as an unhandled 'error' event.

**Details**

- Check sits alongside the existing ENOENT/ENOTDIR filtering and the `ignorePermissionErrors` option.
- In the pending-write path, when no error listeners are registered the pending write is discarded rather than emitting an ERROR event.

**Evidence**

`_pendingWrites`, `ignorePermissionErrors`

- Area: File Watcher
- Names: `ignorePermissionErrors`
- Tier: You'll notice
- Useful: 3/5
- Signal: 0/5

### Mismatched permission replies are logged and dropped

A permission answer meant for one tool can no longer accidentally approve a different tool call.

**What**

A permission response whose tool name does not match the pending request is now rejected with a warning instead of resolving the wrong tool call.

**Details**

- New helper hasCanUseToolNameMismatch guards three paths: injectControlResponse, the resolve path, and the transcript-side matcher.

**Evidence**

`does not match pending`

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

### Permanently refused agent resumes stop retrying

An agent that can never resume now fails fast instead of retrying forever in the background.

**What**

A new error class marks a resume that will never succeed, so the retry machinery gives up instead of looping.

**Details**

- AgentResumePermanentlyRefusedError sits alongside AgentResumeTransientError.
- The observer restarts fresh on it and logs the error name.
- The wake router drops queued events for the affected agent.
- The resume subscriber no longer swallows the error silently.

**Evidence**

`AgentResumePermanentlyRefusedError`

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

### Worktree cleanup will not chdir into a network path

Cleaning up a worktree no longer tries to switch into a network drive path, falling back to your home directory.

**What**

The keep/restore-cwd path now detects a UNC or network-spelled original working directory and throws instead of changing into it.

**Details**

- The recovery loop falls back to the home directory when the original cwd is network-spelled.
- The path is screened and never touched.

**Evidence**

`network-spelled original cwd (screened, never touched)`

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

### Dismissed usage credit requests no longer block the flow

If you dismissed a usage credit request, you can now ask again instead of being told one was already sent.

**What**

The check for an outstanding admin request now looks only at pending requests.

**Details**

- Previously a request that had already been dismissed also triggered the "already sent" notice, leaving no way to ask again.

**Evidence**

`You've already sent a usage credit request to your admin.`

- Area: Usage & Limits
- Tier: You'll notice
- Useful: 3/5
- Signal: 0/5

### Agent records remember that their worktree was cleanly removed

Resuming an agent whose worktree you deliberately removed no longer warns about missing worktree state.

**What**

An isolated agent whose worktree was deliberately removed no longer warns on resume that the worktree "is not recorded for this isolated agent". The removal is recorded in the agent's metadata instead of looking like missing state.

**Details**

- Agent metadata gained `worktreeCleanlyRemoved: !0`, written at the point the worktree is removed
- The flag is inherited from the parent agent record, so child agents do not re-trigger the warning
- Consulted on resume, and suppresses the `git_worktree_resume_binding_missing` telemetry event
- Applies to everyone, no gate

**Evidence**

`worktreeCleanlyRemoved`

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

### Worktree sessions record a launch anchor and use it when leaving

Leaving a worktree session now reliably drops you somewhere sensible even if the original directory is gone.

**What**

Leaving or cleaning up a worktree session now returns you to a directory resolved from a recorded anchor, rather than potentially failing when the original cwd is gone or unsafe.

**Details**

- Active worktree sessions carry `liveLaunchAnchor`, stripped out before the session is persisted into app state
- On leave/cleanup, a new helper resolves the chdir target; when the original cwd is unsafe it falls back to a repo root inferred from a `.claude`/`worktrees` path shape, and failing that to the home directory
- The anchor is also part of the accepted path set when validating changed files

**Evidence**

`liveLaunchAnchor`

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

### Blank message summaries are ignored

Messages with a blank summary now show their full text instead of collapsing into an empty line.

**What**

A message whose summary is only whitespace is no longer treated as having a summary, so it renders in full instead of collapsing to an empty summarized form.

**Details**

- Message rendering and filtering now pass `summary` through a trim/normalize helper rather than testing truthiness
- A whitespace-only value no longer counts as present and no longer forces the summarized render path

**Evidence**

`summary`

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

### Active MCP server/tool markers are cleared on conversation reset

Errors after a conversation reset are no longer mislabelled with the MCP server and tool from a previous turn.

**What**

Stale MCP attribution no longer survives a conversation reset, so a later error or event is not tagged with the server and tool from a previous turn.

**Details**

- The query loop's tool-state object gained an `onReset` hook
- The hook calls a new helper that clears `activeMcpServer` and `activeMcpTool` on the options object, guarded by try/catch
- The same helper is also invoked at the start of the API loop

**Evidence**

`activeMcpServer`

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

### Design OAuth refresh distinguishes lock contention from failure

Design sign-in refreshes no longer report a failure when another process just got there first.

**What**

A refresh that loses the cross-process lock race is now reported as contention rather than as a failed refresh, and a compromised lock can still succeed.

**Details**

- New `DesignOauthLockContendedError` marks the case where another process holds the refresh lock.
- On a compromised lock the stored token is re-read and accepted if it is still fresh.
- New telemetry codes: `design_oauth_refresh_lock_contention`, `_lock_compromised`, `_lock_error`.

**Evidence**

`Design OAuth lock contention: another process is holding the refresh lock`

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

### File watcher teardown no longer assumes a watcher exists

A crash when unwatching a file that was never actually being watched is fixed.

**What**

Removing the last listener for a watched entry whose watcher never started no longer throws.

**Details**

- Both unsubscribe paths check for a watcher object before calling close or unwatch.

**Evidence**

`mku(a.listeners) && a.watcher`

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

### Duplicate comment ids are dropped when building thread views

Comment threads no longer show the same comment twice.

**What**

A thread that returns the same comment id twice no longer renders that comment twice.

**Details**

- The comment-collection loop keeps a set of already-seen ids and adds each id as it is collected.
- Repeats are skipped rather than appended.

**Evidence**

`g.add(v.id)`

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

### Pending-permission cleanup respects a preserved park at shutdown

Answering the last permission prompt no longer wrongly flips your session back to running when a question is still waiting.

**What**

Resolving the last pending permission or dialog request no longer unconditionally flips the session back to "running" when a park was preserved at shutdown.

**Details**

- If a park was preserved, the session re-tees waiting-on-user and republishes the surviving pending action instead of reporting running.

**Evidence**

`hasPreservedParkAtShutdown`

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

### Tunnel sockets are destroyed on protocol violations

Misbehaving tunnel connections are torn down properly instead of leaving stuck half-open sockets behind.

**What**

The local tunnel now treats data arriving before HELLO_ACK as a protocol error and tears the socket down.

**Details**

- Error and close paths destroy the socket rather than half-closing it.
- This applies whether or not the connection negotiated v2, so a stuck half-open socket is no longer left behind.

**Evidence**

`server sent data before HELLO_ACK`

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

### Tunnel sockets are always destroyed on close

Tunnel connections that close before a response always shut down now, instead of lingering.

**What**

When an agent-proxy tunnel closes before a response arrives, the socket is now destroyed in all cases, removing a path where non-v2 connections could linger after a graceful end.

**Details**

- The branch that ended the socket gracefully for non-v2 connections was removed from the teardown path
- The socket is destroyed unconditionally after "tunnel closed before response"

**Evidence**

`tunnel closed before response`

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

## In Development

### `claude import` is stubbed out in this build

The import command exists but just prints a message instead of starting a session here.

**What**

The import argument handling was rewritten into a parser plus a rewrite decision, but the command itself is not wired up here and prints a message instead of starting a session.

**Details**

- Gated on a config-derived availability flag; when unavailable it prints the message below and exits.
- If the config cannot be read at all it prints a recovery message instead.
- The `--yes` help text now mentions a digest taken from the `/import` preview.

**Evidence**

`claude import` is not yet available in this build. Run `claude` and use /mcp or edit ~/.claude/settings.json directly.

- Area: Internals
- Names: `claude import`, `--yes`, `/import`
- Tier: Not switched on
- Useful: 2/5
- Signal: 3/5
- Present in the build but not switched on

### SDK stream shutdown can preserve parked permission requests

Pending permission prompts can survive an SDK stream closing instead of vanishing.

**What**

The control client can remember pending can_use_tool requests for a parked tool when the input stream closes, so they survive shutdown instead of being dropped.

**Details**

- Preserved ids live in a preservedParkRequestIds set.
- While any are preserved the session is not flipped back to "running".
- Ids are cleared as responses arrive or when the park is retracted.
- Guarded by two internal predicates evaluated at shutdown time that could not be resolved to a flag name.

**Evidence**

`preservedParkRequestIds`

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

### Footer can show an IDE indicator

The footer reserves room for an IDE indicator, though whether it actually shows is unresolved.

**What**

The status footer computes an indicator element rendered in the "ide" color with a lozenge glyph, and the compact-footer width maths now reserves space for it. Whether it renders in this build could not be resolved.

**Details**

- Thresholds deciding whether the compact footer fits add the indicator's width plus five.
- An extra check hides content below 48 columns when the indicator is present.

**Evidence**

`"footer-indicator"`

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

### Codex config import parses TOML with Bun's built-in parser

Importing another agent's config now parses TOML natively, reachable only through the gated import path.

**What**

Reading a foreign agent's Codex config during import now uses Bun's native TOML parser through a one-line wrapper, before the config is mapped.

**Details**

- The wrapper calls `Bun.TOML.parse(e)` on the config file contents
- Reachable only via the import path, which is behind `tengu_import`
- A filename/label normalizer was added alongside it: NFKC normalize, lowercase, strip trailing dots and spaces

**Evidence**

`Bun.TOML.parse`

- Flag `tengu_import`: On for this account, and not off by default (read for one account on one subscription tier against v2.1.222; 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: Internals
- Names: `tengu_import`
- Tier: Not switched on
- Useful: 2/5
- Signal: 2/5
- Present in the build but not switched on

### Telemetry retry batches can persist as v5 streams

Failed telemetry batches can be stored and retried differently, but only with a backend that nothing here supplies.

**What**

The first-party event exporter gained a path that stores failed-event batches as append-only v5 streams keyed by run id and retries earlier runs in the background.

**Details**

- Leftover flat JSON batches are migrated into streams using deterministic record ids of the form `flat-migration.<index>.<hash>`, and the flat file is deleted afterwards.
- Only taken when a storageV5 backend is passed to the exporter; what enables that backend could not be determined.
- Without a v5 backend the existing flat-file path is unchanged.

**Evidence**

`1P event logging: failed to persist failed-event queue to v5 stream`

- Area: Telemetry
- Tier: Under the hood
- Useful: 1/5
- Signal: 2/5
- Present in the build but not switched on

### More state gains v5-backed code paths

Themes, settings, tasks and transcripts gained alternative storage paths that stay dormant without a backend.

**What**

Several stores now have a v5 backend path sitting alongside the existing file path, taken only when a backend is supplied.

**Details**

- Covers theme listing and saving, MCP task metadata (write, delete and list under an `mcp-tasks` sidecar), scheduled-task status, user settings writes, and ranged transcript reads.
- Settings write failures now raise a distinct error naming the failure class.
- Each path is inert unless a v5 backend is passed in; what supplies it could not be determined.

**Evidence**

`settings storageV5 write failed`

- Area: Internals
- Tier: Under the hood
- Useful: 1/5
- Signal: 2/5
- Present in the build but not switched on

### Transcript writes and head/tail reads can go through the v5 storage backend

Transcript writing can be redirected to a different storage layer, but nothing turns that on here.

**What**

Transcript append, truncate and read helpers can now be pointed at a v5 storage backend instead of the filesystem, but only when a backend handle is passed in.

**Details**

- The helpers take an optional backend handle; when present they call `backend.append`, `backend.delete`, `backend.readText` and `backend.stat` with a key rather than touching files.
- Failures throw Error("transcript stream append failed") or Error("transcript stream delete failed").
- The exporter class gained a `storageV5` field.
- storageV5 existed in the previous build but only on the read path; the write path is new here and inert without a handle.

**Evidence**

`transcript stream append failed`

- Area: Internals
- Tier: Under the hood
- Useful: 1/5
- Signal: 2/5
- Present in the build but not switched on

## Internal Changes

### Settings reader computes a host model overlay

Settings can inherit a model choice from a managing host, though that path only exists when one is configured.

**What**

The parent-slice settings reader also derives a model overlay from the host-managed provider, and reports it as a source of truth.

**Details**

- The source-of-truth lookup returns "parent" when either the parent slice or the `hostModelOverlay` is present.
- Only reachable when a host-managed provider is configured; state otherwise unresolved.

**Evidence**

`hostModelOverlay`

- Area: Settings
- Tier: Under the hood
- Useful: 2/5
- Signal: 3/5
- Present in the build but not switched on

### Transcript conversion dropped its viewerOnly/convertToolResults mode

Transcripts now show tool results the same way everywhere, instead of differing in viewer-only sessions.

**What**

The message converter no longer branches on a viewer-only mode, so tool-result behaviour is consistent for every caller.

**Details**

- It now always converts user messages carrying `tool_result`, and always honours `convertUserTextMessages`.
- The several callers that passed `viewerOnly` or `convertToolResults` were simplified to drop the option.

**Evidence**

`convertUserTextMessages`

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

### Agent state writes preserve worktree provenance

Agent records keep their worktree history across saves, so lineage info no longer gets wiped.

**What**

Persisting an agent record no longer loses the worktree lineage fields that were set by an earlier write.

**Details**

- A new persistence wrapper reloads the previous agent record before writing.
- It carries over `parentAgentId`, `inheritedWorktreePath`, `spawnedWithWorktree` and `worktreeCleanlyRemoved`.
- `worktreePath` and `worktreeBranch` are deleted when the prior record had none.

**Evidence**

`inheritedWorktreePath`

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

### Heartbeat-failure streak detection in the CCR client

Dropped connections are detected by counting repeated heartbeat failures instead of reacting to one.

**What**

Failed heartbeat POSTs are no longer fire-and-forget; the client tracks a failure streak and reports it upward once it is long enough to matter.

**Details**

- a set of statuses is ignored outright
- checks `isReadStreamRecentlyAlive()` before counting, then increments `consecutiveHeartbeatFailures`
- fires `onHeartbeatLost` only once the streak passes both a count and a duration threshold
- a successful heartbeat calls `resetHeartbeatStreak()`
- the onHeartbeatLost handler is only installed when selfHealHeartbeats is on

**Evidence**

`this.onHeartbeatLost?.()`

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

### Tool-input string extraction hardened against hostile objects

Reading tool inputs is now hardened so a crafted object cannot run code during extraction.

**What**

The helper that pulls values like command, file_path, prompt and args out of tool input no longer trusts the shape of the object it is given.

**Details**

- Rejects Proxy objects outright.
- Reads own property descriptors rather than invoking getters, so extraction cannot run attacker code.
- Bails on array-like values whose length is not a safe integer or exceeds the cap.

**Evidence**

`OYa.types.isProxy(e)`

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

### Shell output handoff can be re-rooted for adoption

Background shell output can be relocated so another process adopting the task can still read it.

**What**

Detached shell handoff accepts a root to re-anchor output files, so an adopting process can read outputs that would otherwise sit outside its tree.

**Details**

- Takes a `rerootOutputsTo` root; outputs resolving outside the task-output tree are dropped and the shell is killed.
- Escaping but known outputs are hardlinked, or copied when linking fails, into `<root>/rerooted/<id>.output`, with inode identity checks.
- Task output paths and the handoff payload gained an adopt root plus `mergeShellOutputReadRoots`.

**Evidence**

`mergeShellOutputReadRoots`

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

### Status-line notices moved from JSX to a segments array

Status-line notices are now built as plain text pieces, so they render more consistently.

**What**

Notification producers now hand back plain data instead of React elements, so the notification layer owns rendering.

**Details**

- Each producer emits a list of `{text, color, dim, bold}` segments.
- Affected producers: the option-as-Meta hint, the idle /clear hint, the Remote Control disconnect and unsigned-attestation warnings, the closed-issue notice and the prefilled-prompt warning.

**Evidence**

`idle-return-hint`

- Area: Terminal UI
- Tier: Under the hood
- Useful: 2/5
- Signal: 0/5

### Marketplace schema loosened for features and doc fallbacks

Marketplace entries with odd values in two fields now pass parsing instead of being silently discarded.

**What**

Two marketplace entry fields no longer validate their shape, so malformed values now pass parsing instead of being coerced away.

**Details**

- `features` moved from a typed record with `.catch(void 0)` to `w.unknown().optional()`.
- `project_doc_fallback_filenames` moved from a string array with `.catch(void 0)` to `w.unknown().optional()`.
- Consumers no longer get a guaranteed shape from the parser for either field.

**Evidence**

`project_doc_fallback_filenames: w.unknown().optional()`

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

### Workflow prompt interpolates the parallel/pipeline item cap

The workflow prompt now quotes the real item cap, so the stated limit matches the enforced one.

**What**

The agent-workflow system prompt now substitutes the cap constant into the sentence describing how many items one parallel() or pipeline() call accepts, instead of hardcoding the number in prose.

**Details**

- Keeps the prompt text and the enforced limit from drifting apart.

**Evidence**

`A single parallel()/pipeline() call accepts at most ${soe} items`

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

### Atomic writer can publish without clobbering

File writing gained a mode that refuses to overwrite an existing file instead of replacing it.

**What**

The temp-file writer gained a create-only mode that publishes via link plus unlink instead of rename.

**Details**

- In this mode an existing destination file is left intact and the write fails rather than overwriting.
- Rename remains the default publish path.

**Evidence**

`create_only`

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

### Model enforcement stays inactive for the parent settings source

Model restrictions won't kick in when your settings come from a trusted parent source.

**What**

A policy view with no model keys sourced from "parent" is treated as cascade-trusted instead of triggering enforcement.

**Details**

- The `availableModels` enforcement check previously accepted only the "hkcu" source in this case.
- It now also accepts "parent", returning `{ state: "inactive", cascadeTrusted: !0 }`.

**Evidence**

`cascadeTrusted`

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

### Task output files use a memoized path and lazy directory creation

Task output files skip repeated path work and only create folders when actually writing.

**What**

Task output writing resolves its file path once per task and creates the containing directory only when the file is opened, replacing an upfront directory-creation step.

**Details**

- `<taskId>.output` is resolved through a per-task path cache
- The parent directory is created at open time with `mkdir(dirname, { recursive: true })`
- The previous upfront directory creation helper was removed
- The cache entry is dropped when the task output is flushed, cleared or cleaned up

**Evidence**

`.output`

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

### Peer-message envelope text moved into constants

Wording around messages from other sessions moved to one place, with an extra old header now stripped.

**What**

The wrapper text applied to messages from peer and coordinator sessions, including the permission-laundering warning, now lives in a module of constants rather than inline.

**Details**

- The wording of the guidance itself is unchanged.
- The list of legacy prefixes stripped before re-wrapping now also matches an alternate "A peer session sent a message…" header.

**Evidence**

`A peer session sent a message while you were working:`

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

### Version bumped to 2.1.222

The version number moves to 2.1.222, which is what you'll see in /status and --version.

**What**

The embedded build-metadata blocks move to 2.1.222 with a fresh build time and git sha.

**Details**

- VERSION goes from 2.1.221 to 2.1.222 across all four embedded copies of the version banner.
- BUILD_TIME is 2026-08-04T01:24:05Z, with a new GIT_SHA.
- This accounts for the bulk of changed strings in this area: user-agent strings, /status, --version, updater messages and telemetry payloads.

**Evidence**

`BUILD_TIME: "2026-08-04T01:24:05Z"`, `"2.1.222"`

- Area: Elsewhere
- Names: `/status`, `--version`
- Tier: Under the hood
- Useful: 1/5
- Signal: 0/5

### Code-review, /simplify and batch orchestration prompts moved between modules

Three prompt texts moved between files with their wording intact, so nothing changes for you.

**What**

Three prompt bodies were relocated into different lazily-initialised modules with no visible change to their wording.

**Details**

- The multi-agent code-review angle text, the /simplify cleanup prompt and the Batch parallel-work orchestration prompt all moved.
- One call site now returns a short bullet line rather than the full batch prompt.
- Prompt wording itself appears unchanged; only placement and one call site differ.

**Evidence**

`# Batch: Parallel Work Orchestration`

- Area: Elsewhere
- Names: `/simplify`
- Tier: Under the hood
- Useful: 1/5
- Signal: 0/5

### New telemetry when a remote session renders a tool result

Remote sessions now report once when they first show a tool result.

**What**

The first user message containing a tool_result in a remote session now emits a dedicated event.

**Details**

- Fires once, on the first such message, per remote session.
- Tagged with whether the session is viewer-only.

**Evidence**

`tengu_remote_tool_result_rendered`

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

### Bridge connect timeouts are classified for recovery reporting

Remote connection timeouts now record whether they happened during recovery or a token refresh.

**What**

A repl-bridge connect timeout now records which path triggered the attempt, so timeouts during recovery can be told apart from those during a token refresh.

**Details**

- recovery_connect_timeout is used for the auth-401, init-4091, and heartbeat-4093 recovery paths.
- refresh_connect_timeout is used for a proactive token refresh.
- The existing tengu_bridge_repl_connect_timeout event still fires.

**Evidence**

`recovery_connect_timeout`

- Area: Remote Control
- Tier: Under the hood
- Useful: 1/5
- Signal: 0/5

### Artifact comment auto-scan tracks its in-flight promise

Artifact comment scans now track their running work so overlapping scans can be spotted.

**What**

The debounced comment scan stores its running promise and clears it on completion, so overlapping scans can be detected rather than the scan being fire-and-forget.

**Details**

- Promise is held in inFlightScan for the duration of the scan.

**Evidence**

`inFlightScan`

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

### Remote-io warns when the drop sender notice cannot be written

A failed remote notice write now logs a warning instead of disappearing silently.

**What**

The remote-io transport writes an attestation drop notice through the CCR client and now logs a warning if that write fails, rather than dropping the failure silently.

**Details**

- New handler on the transport; the warning text is "[remote-io] drop sender-notice write failed".

**Evidence**

`[remote-io] drop sender-notice write failed`

- Area: Remote Control
- Tier: Under the hood
- Useful: 1/5
- Signal: 0/5

### Shell adoption accepts an extra output root

Shell sessions can hand off through more than one output location internally.

**What**

Shell adoption picks its schema based on a new option, and exit handoff now works with multiple merge roots.

**Details**

- The adoption schema is chosen from an `extraShellOutputRoot` option.
- Exit handoff passes a list of merge roots rather than a single one.

**Evidence**

`extraShellOutputRoot`

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

### Named ANSI colors backed by an object literal

Terminal color names are stored differently; nothing changes for you.

**What**

The terminal color name list is now an object literal, with the lookup Set derived from its keys.

**Details**

- Moved into a separate module init.

**Evidence**

`magentaBright`

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

### Turn-effort updates can skip creating an entry

Empty effort updates no longer leave junk state behind for each agent.

**What**

The per-agent UI state updater no longer materializes a store entry for a no-op write.

**Details**

- The updater takes a `materialize` option.
- It skips creating an entry when both the effort and the model being set are null.

**Evidence**

`materialize`

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

### Prefilled-prompt warning notification rendered from text segments

The external-link prompt warning is built differently but looks exactly the same.

**What**

The pinned warning shown for pre-filled and deep-link prompts is now built as plain text segments rather than a rendered element. Nothing visible changed.

**Details**

- The notification is pushed as `segments` produced by a small helper instead of a JSX element
- A JSX segment-list component was removed
- User-visible strings are unchanged, including "Prompt from an external link" and the scroll/review suffixes

**Evidence**

`Prompt from an external link`

- Area: Terminal UI
- Tier: Under the hood
- Useful: 1/5
- Signal: 0/5

## Removed

### "Permission denied" system messages are no longer surfaced

You no longer see a "permission denied" line in output when a tool is blocked.

**What**

The stream converter no longer emits a visible system message when a permission_denied event arrives, so that line disappears from output.

**Details**

- Previously the event became a warning-level system message naming the tool and the reason.
- The handler now returns an ignored result unconditionally, with no mode or flag that restores the old line.

**Evidence**

`Permission denied: `

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

### Resume no longer validates or touches the worktree directory

Resuming a session skips worktree checks, so no fallback to the parent folder or log line.

**What**

Resuming a session no longer stats the recorded worktree, no longer falls back to the parent cwd with a log line when it is missing, and no longer bumps the directory's mtime.

**Details**

- The fallback block and the associated worktree chdir and restore helpers are gone from this build.

**Evidence**

`no longer exists; falling back to parent cwd`

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