# Claude Code v2.1.235

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

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

A large batch of unreachable work sits in this build, 22 entries in all. A complete "account on hold" sign-in path exists, covering login, setup tokens, API retries and token refresh with its own OAuth failure kind carrying an appeal URL, but every entry point checks the `tengu_lively_beaver` flag, which falls back to false. The `plugin eval` harness gained an `--eval-dir <dir>` flag and an `experimental.evals` key in plugin.json to choose the case directory, though both eval commands remain behind `tengu_walnut_spire` or `CLAUDE_CODE_WALNUT_SPIRE` and otherwise report "currently in early access". Also dark: a Bash reminder telling the model you only see a few lines of command output, remote settings primed from local storage behind `tengu_hover_rest`, threaded artifact comment replies behind `tengu_medlar_quoin`, and a second storage backend for compaction, history purges and session moves whose factory has an empty body and returns nothing.

The built-in artifact MCP server is now on by default and turned off with `CLAUDE_CODE_ARTIFACT_MCP`, while artifact comments stay opt-in behind `CLAUDE_CODE_ARTIFACT_COMMENTS`. Permission prompts now withhold approval when they cannot render a tool's input in full: oversized or truncated commands and URLs show a placeholder and offer one-time approval only, with no setting to opt out. Spell checking arrives for the prompt input via `"spellcheck": { "enabled": true }` in settings, using aspell, hunspell or ispell from PATH and staying silent if none is found. Setting `CLAUDE_CODE_HOLD_UNANSWERED_PARKED_PERMISSION=1` keeps an unanswered permission request answerable across a resume, and `policyHelpers` can now arrive from remote managed settings but refuses to run until an admin approves the exact payload. Another 54 shipped entries are listed below.

Choosing "yes, don't ask again" no longer writes a blanket allow rule for the whole tool, taking its rules from a single validated suggestion row and falling back to a one-shot allow that saves nothing. Approvals for database and artifact tool calls now record which action and project they were granted for and re-check at execution time, failing with `db_target_changed` or `asset_target_changed` if the target moved. Template substitution of `{{name}}` now requires the key to be defined directly on the supplied values, so `constructor` and `toString` stay literal text rather than expanding to function source. Multiple-choice questions identify options by stable key instead of display text, and malformed messages from a worker or remote session are dropped rather than crashing the client.

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

### Artifact MCP is on by default

The built-in artifact MCP server is now on for everyone unless you set the env var to disable it.

**What**

The built-in artifact MCP server no longer waits on a remote flag. It is enabled unless you set the environment variable `CLAUDE_CODE_ARTIFACT_MCP` to turn it off. The related artifact-comments feature stays off by default behind its own env var `CLAUDE_CODE_ARTIFACT_COMMENTS` and the flag `tengu_teal_corbel`.

**Details**

- Enablement now reads `CLAUDE_CODE_ARTIFACT_MCP ?? true`, so absent the variable it is on; the previous remote flag `tengu_cloth_snorkel` no longer gates it.
- The separate reporter that records where an artifact came from moved to the new env var and flag name, whose fallback is false.

**Evidence**

`Xd.CLAUDE_CODE_ARTIFACT_MCP ?? !0`

- Flag `tengu_teal_corbel`: On for this account, and not off by default (read for one account on one subscription tier against v2.1.235; this account: on, anonymous baseline: on, compiled default: on)
- Flag `tengu_cloth_snorkel`: On for this account, and not off by default (read for one account on one subscription tier against v2.1.235; this account: on, anonymous baseline: on, compiled default: not a boolean we can read)
- Area: Artifacts
- Names: `CLAUDE_CODE_ARTIFACT_MCP`, `CLAUDE_CODE_ARTIFACT_COMMENTS`
- Tier: Use it now
- Useful: 4/5
- Signal: 4/5

### Canned acknowledgements for artifact comment threads

Canned short acknowledgements let Claude reply instantly on a comment thread before doing the work.

**What**

A table of short acknowledgement replies, each marked as being for an edit or not, plus a small prompt that picks one by number, lets Claude post an immediate reply on an artifact comment thread before doing the work. Which artifact-monitor setting turns this on is not determined by the code paths around it.

**Details**

- The picker prompt states that thread content is untrusted viewer data and that the reply must be a single digit.

**Evidence**

`You choose one acknowledgement for an artifact comment thread from a numbered list.`

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

### Full "account on hold" sign-in path built but switched off

A complete sign-in flow for accounts on hold, with appeal links, sits in the build switched off.

**What**

An end-to-end path for accounts placed on hold is present and unreachable in this build. It covers the login flow, the setup-token flow, API request retries and token refresh, with a login screen reading "Your account is on hold and can't use Claude Code. View details or appeal: " and a browser page reading "Your account is on hold. You can close this window." Every entry point goes through one detector that returns nothing unless the `tengu_lively_beaver` flag reads true; it is read with fallback false, so without a server value none of it runs.

**Details**

- The local OAuth callback listener gained handling for `?error=` redirects, which it previously ignored in favour of `code` only, serving a plain-text page. That branch also requires the gate.
- The appeal URL is validated hard: https only, no port, userinfo or fragment, host must be claude.ai or anthropic.com or a subdomain. Anything else falls back to `https://claude.ai/restricted`.
- Related events exist but stay unfired: `api_request_account_on_hold`, `tengu_oauth_refresh_token_account_on_hold`, and a `tengu_oauth_error` carrying `account_on_hold: true`.
- The gate also decides whether `"account_on_hold"` is injected into the API error-kind list and the StopFailure hook matcher values, so with the gate off the hook matcher does not advertise it at all.

**Evidence**

`Your account is on hold and can't use Claude Code. View details or appeal: `, `Your account is on hold. You can close this window.`, `tengu_lively_beaver`

- Flag `tengu_lively_beaver`: Off in both readings (read for one account on one subscription tier against v2.1.235; this account: off, anonymous baseline: off, compiled default: not a boolean we can read)
- Area: Auth
- Tier: Not switched on
- Useful: 2/5
- Signal: 5/5
- Present in the build but not switched on

### `plugin eval` can be pointed at a different case directory

Point plugin eval at any case directory with --eval-dir, still behind an early-access gate or env var.

**What**

The plugin evaluation harness no longer hard-codes `evals/`. A new `--eval-dir <dir>` flag, or an `experimental.evals` key in plugin.json, chooses the case directory, and results land in `<plugin>/<dir>/results/`. A list is accepted and its first entry wins. Both `plugin eval` and `plugin eval init` stay behind early access: the gate `tengu_walnut_spire` (fallback false) or the env var `CLAUDE_CODE_WALNUT_SPIRE`. Without it the commands are filtered from the command list and report "currently in early access".

**Details**

- Manifest values are validated hard: relative only, no `..`, at most 200 characters and 8 path segments, plain segment names, not a bare filename, no reserved Windows device names, trailing dot or space, or `:`.
- The path may not sit inside a loaded component directory or route through `node_modules`, `.claude` or `results`.
- A top-level `"evals"` key is detected and reported as misplaced rather than used.

**Evidence**

`must stay inside the plugin root (no ..)`, `Directory of eval cases for the plugin evaluation harness, relative to the plugin root (default: evals/). A list is accepted; its first entry is the case directory.`

- Flag `tengu_walnut_spire`: Off in both readings (read for one account on one subscription tier against v2.1.235; this account: off, anonymous baseline: off, compiled default: on)
- Area: Plugins
- Names: `--eval-dir`, `CLAUDE_CODE_WALNUT_SPIRE`, `plugin eval`, `plugin eval init`
- Tier: Use it now
- Useful: 3/5
- Signal: 4/5

### Unanswered permission prompts can be held open across a resume

Set an env var so a resumed print session keeps an unanswered permission request still answerable.

**What**

Setting `CLAUDE_CODE_HOLD_UNANSWERED_PARKED_PERMISSION=1` makes a resumed print/SDK session keep a still-unanswered permission request answerable instead of retiring it. The session comes back in `requires_action` with the original tool details, the turn is not re-run and the request is not discarded. The variable is a plain boolean with no default, so this is off unless you set it; without it the older `CLAUDE_CODE_RETIRE_UNANSWERED_PARKED_PERMISSION` behaviour of retiring the request unanswered applies.

**Details**

- Logs "holding it answerable" and a `tengu_resume_parked_permission` event with `outcome: held`, or `held_superseded` if new input arrives while the request is being held.
- Control responses that arrive for a different tool use than the held one are dropped with a warning rather than applied.

**Evidence**

`CLAUDE_CODE_HOLD_UNANSWERED_PARKED_PERMISSION`, `CLAUDE_CODE_HOLD_UNANSWERED_PARKED_PERMISSION); not re-running, not retiring`

- Area: Permissions
- Names: `CLAUDE_CODE_HOLD_UNANSWERED_PARKED_PERMISSION`, `CLAUDE_CODE_RETIRE_UNANSWERED_PARKED_PERMISSION`
- Tier: Use it now
- Useful: 3/5
- Signal: 4/5

### Bash output audience note, present but off by default

Claude can be told you only see a few lines of command output, so it should repeat what matters.

**What**

Claude Code can attach a reminder to Bash tool results telling the model that you only see a few lines of the command output in your terminal, so anything you need must be repeated in its reply. It is registered as its own reminder kind, `bash_output_audience_note`, injected as a system reminder after qualifying Bash results, and nothing is added unless it is explicitly switched on.

**Details**

- The exact text is "Only you see that command's output — the user's terminal shows at most a few lines of it. If the user needs to read any of it, put it in your reply."
- Only emitted for Bash-style tool results whose `stdout` is a string; other results are untouched.
- The gate checks the environment variable `CLAUDE_CODE_BASH_OUTPUT_AUDIENCE_NOTE` first, then a model capability check, then a client-supplied data flag of the same name, `bash_output_audience_note`; with none of them set the helper returns false and no note is attached.
- Run `CLAUDE_CODE_BASH_OUTPUT_AUDIENCE_NOTE=1 claude` to turn it on locally.
- Because it sits in the reminder-accounting list with the existing reminder types, it draws on the same per-conversation reminder budget rather than being injected unconditionally.

**Evidence**

`CLAUDE_CODE_BASH_OUTPUT_AUDIENCE_NOTE`, `Only you see that command's output \u2014 the user's terminal shows at most a few lines of it. If the user needs to read any of it, put it in your reply.`, `bash_output_audience_note`

- Area: Bash Tool
- Names: `CLAUDE_CODE_BASH_OUTPUT_AUDIENCE_NOTE`
- Tier: Not switched on
- Useful: 3/5
- Signal: 4/5
- Present in the build but not switched on

### Background work handoff on exit can be switched off

Set CLAUDE_CODE_DISABLE_BG_EXIT_HANDOFF to stop Claude handing off background work when you exit.

**What**

Setting `CLAUDE_CODE_DISABLE_BG_EXIT_HANDOFF` stops Claude Code handing background work off when you exit. The handoff stays on unless the variable is set.

**Evidence**

`CLAUDE_CODE_DISABLE_BG_EXIT_HANDOFF`

- Area: Background Tasks
- Names: `CLAUDE_CODE_DISABLE_BG_EXIT_HANDOFF`
- Tier: Use it now
- Useful: 4/5
- Signal: 3/5

### Opt-in spell checking for the prompt input

Turn on spellcheck to underline misspelled words as you type in the prompt.

**What**

Claude Code can now underline misspelled words as you type in the prompt, using a spell checker already installed on your machine. Turn it on with `"spellcheck": { "enabled": true }` in your user or managed settings; the default is false. `checker` defaults to `"auto"`, which picks the first of aspell, hunspell or ispell found on PATH, and if none is found spell checking stays off silently. `language` and `color` are also configurable.

**Details**

- The setting is only read from user settings, managed settings and command-line flags. A `spellcheck` block in a project's `.claude/settings.json` or `.claude/settings.local.json` is ignored, with a warning.
- `language` must be a plain dictionary name; the value is validated before being passed to the checker.
- The driver shells out to the installed checker and parses its ispell-protocol output; usage is recorded under an `input_spellcheck` telemetry event.

**Usage**

`{ "spellcheck": { "enabled": true, "checker": "auto", "language": "en_US" } }` **Evidence** `Turn on spell checking of the prompt input (default: false)`

- Area: Prompt Input
- Names: `spellcheck`, `enabled`, `checker`, `language`, `color`
- Tier: Use it now
- Useful: 4/5
- Signal: 3/5

### "Don't ask again" saves only the rule that was actually suggested

"Yes, don't ask again" now saves only the suggested rule, not a blanket allow for the tool.

**What**

Choosing "yes, don't ask again" no longer writes a blanket allow rule for the whole tool into your local settings, and "apply suggestions" no longer builds its own updates. Both now take the rules from a single validated suggestion row. If that row is missing or fails validation, the choice falls back to a one-shot allow that saves nothing.

**Details**

- The dialog's option list was changed to match, so an option only appears when the row actually carries rules.
- Unconditional on this build.

**Evidence**

`permissionUpdates: r.applies,`

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

### Login refresh tells "account on hold" apart from an expired token

An account on hold no longer logs you out; you get a distinct message and a link.

**What**

When refreshing your login fails with a 400, 401 or 403 carrying a recognised on-hold payload, Claude Code now treats it as a held account rather than a dead token: it remembers the refresh token, stores a URL to send you to, and returns a distinct outcome instead of logging you out.

**Details**

- Fires a new telemetry event on that path, `tengu_oauth_refresh_token_account_on_hold`.
- Other refresh failures keep the existing dead-token and generic-failure handling.

**Evidence**

`tengu_oauth_refresh_token_account_on_hold`

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

### Permission prompts withhold approval when the input cannot be shown in full

If a tool's input is too big to show fully, you only get one-time approval, no always-allow.

**What**

Approval dialogs no longer show a best-effort, partially rendered version of what a tool wants to do. If the command, URL or other input is too large or would render with parts cut out, the dialog shows a placeholder instead and offers only one-time approval; the "always allow" and suggested-rule options disappear. This is on for everyone, with no setting to turn it off.

**Details**

- Withholding triggers when the value exceeds the character budget, exceeds the limit on rendered elements, or the rendered result would itself contain a truncation marker.
- The marker shown in place of the value reads "(value cannot be shown in full — approval withheld; one-time options only)".
- Renderers for unknown tools and placeholder tools now fail outright rather than render half the input, with internal errors "stub preview cannot render an over-bound parameter" and "unknown-tool stub cannot render parameters".
- The previous renderer that swallowed failures and logged "Error rendering tool use line" is gone, so a preview either renders completely or is withheld.

**Evidence**

`(value cannot be shown in full \uFFFD approval withheld; one-time options only)`

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

### Permission rules can only be written from a dialog row the dialog itself created

Persistent permission rules only save when the dialog itself built the option; untrusted text hides them entirely.

**What**

Bash prefix and exact, Chrome host, Skill exact and prefix, plan-mode keep-context and MCP elicitation prompts now refuse to persist a permission rule unless the option came from the dialog's own constructor, falling back to a one-time allow that writes nothing. Untrusted text that cannot be displayed safely suppresses the persistent options outright. This applies to everyone on this build.

**Details**

- Options are produced by a constructor guarded by a private token; combining or applying rows checks that provenance first.
- MCP server and tool names, elicitation URLs, commands, skill and cell names and question labels are rendered through a sanitizer that can mark a value withheld, showing "approval withheld; one-time options only".
- When a value is withheld, the URL elicitation dialog also starts on "decline" and refuses to open the URL.

**Evidence**

`combineRows accepts only constructor-produced ConsentRows`

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

### Remote and cloud session creation carries an effort level

Your chosen effort level now travels with remote and cloud sessions, when the model supports it.

**What**

Creating a remote session or cloud task now sends an `effort_level` alongside the model, so the chosen effort applies to the work that runs remotely. A helper checks whether the effort value is meaningful for the selected model and omits it when it is not.

**Details**

- The CLI create path supplies the effort value plus a resolved permission mode fallback for teleport.
- Creation telemetry records the value as `create_effort_level` / `effortLevel`.
- The GitHub source descriptor used for remote work gained a `pinned` flag.

**Evidence**

`effort_level`, `create_effort_level`

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

### More subsystems routed through an alternative storage backend that this build never creates

Compaction, history purge, session moves, recordings and skill reads all gained storage-backed twins that never run.

**What**

Transcript compaction, history-purge counting and rewriting, session-directory moves, asciicast recording writes, a deep-link registration marker and skill-as-plugin SKILL.md reads all gained a second implementation that goes through an optional storage layer. None of it can run here: the factory that would build that layer has an empty body and returns nothing, so every one of these branches falls back to the normal filesystem code.

**Details**

- Each branch is selected only when the gate `tengu_hover_rest` (fallback off) is on *and* a backend object exists, and the gate value is read once per process.
- The alternative compaction path carries its own failure reasons `snapshot_mid_line`, `shared_inode` and `source_changed`, reported under `tengu_transcript_compact_failed`.
- The session-move path includes a warning for leftovers stranded by a partial move.

**Evidence**

`performCompactTranscriptV5`

- Flag `tengu_hover_rest`: Off in both readings (read for one account on one subscription tier against v2.1.235; this account: off, anonymous baseline: off, compiled default: on)
- Area: Storage Backend
- Tier: Not switched on
- Useful: 1/5
- Signal: 5/5
- Present in the build but not switched on

### New error for delegating to a subagent without naming one

A new error says a subagent type is required because no general-purpose agent is available.

**What**

A message has been added for the case where a subagent task is started without a subagent type: "subagent_type is required: the general-purpose agent is not available in this session". Nothing in the build shows what triggers it, so whether sessions actually reach this state depends on which agents are configured.

**Evidence**

`subagent_type is required: the general-purpose agent is not available in this session`

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

### Policy helpers can be delivered by remote managed settings, after explicit approval

Org policy helpers can now arrive via remote settings, but only run after an admin approves the exact payload.

**What**

`policyHelpers`, an org-configured program Claude Code runs to resolve policy, was previously accepted only from MDM or local managed settings. It can now be armed from remote settings, but it refuses to run until an admin approves the exact payload in the managed-settings dialog. Without approval it logs "not yet approved in the managed-settings dialog" and runs nothing. Approval is stored separately, is revoked whenever the payload changes, and the helper then runs in a stripped-down environment.

**Details**

- The consent record is kept as `remote-settings-helper-consent` and was added to the data purge list. Changing the remote payload revokes it; revocation part-way through a run discards the helper's output.
- Declined or missing consent emits `settings_policy_helpers_per_os` with `remote_consent_missing`.
- A remote-armed helper runs with known secret variables deleted (`ANTHROPIC_API_KEY`, `AWS_SESSION_TOKEN`, `GOOGLE_APPLICATION_CREDENTIALS` and similar) and loader-style prefixes removed (`LD_`, `DYLD_`, `NODE_OPTIONS`, `GIT_`), with `PYTHONNOUSERSITE=1`, `GIT_CONFIG_NOSYSTEM=1`, and PATH/HOME-like variables pinned under `/var/empty/claude-code-policy-helper` (or `C:\Windows\claude-code-policy-helper`).
- No feature flag is involved: this only affects orgs whose remote settings carry a per-OS `policyHelpers` entry and who approve it.

**Evidence**

`not yet approved in the managed-settings dialog`

- Area: Managed Settings
- Names: `policyHelpers`
- Tier: You'll notice
- Useful: 2/5
- Signal: 4/5

### Account skills sync gating refactored, still off

Syncing skills from your claude.ai account stays off, but its checks were reshaped for other synced resources too.

**What**

The checks that decide whether skills sync from your claude.ai account are now expressed as a single description of the requirement, reusable for other synced resources. The requirements are unchanged: the flag `tengu_account_skills_sync_enabled` defaults off, and the feature also needs the `allow_account_skills_sync` org policy and a claude.ai account.

**Evidence**

`tengu_account_skills_sync_enabled`

- Flag `tengu_account_skills_sync_enabled`: Not enough to say (read for one account on one subscription tier against v2.1.235; this account: no value returned, anonymous baseline: no value returned, compiled default: not a boolean we can read)
- Area: Skills
- Tier: Not switched on
- Useful: 2/5
- Signal: 4/5
- Present in the build but not switched on

### On-hold accounts get their own auth failure kind, behind the same gate

Accounts on hold would get a dedicated auth error with an appeal link, but the gate is off.

**What**

An OAuth response carrying an `account_on_hold` error is classified as its own outcome instead of being treated as a generic invalid grant, so token refresh stops retrying and a dedicated error is raised carrying the appeal URL. The classification short-circuits unless `tengu_lively_beaver` reads true, and it is read with fallback false, so on this build refreshes still fail the old way.

**Details**

- Refresh classification returns `account_on_hold`, the retry loop stops, and an `OAuthAccountOnHoldError` with message "OAuth account is on hold" is thrown.
- `account_on_hold` is spliced into the API-error union and the login-screen error list only when the gate is on.

**Evidence**

`account_on_hold`

- Flag `tengu_lively_beaver`: Off in both readings (read for one account on one subscription tier against v2.1.235; this account: off, anonymous baseline: off, compiled default: not a boolean we can read)
- Area: Auth
- Tier: Not switched on
- Useful: 2/5
- Signal: 4/5
- Present in the build but not switched on

### Remote settings can be primed from local storage instead of probing disk

Managed settings could be read from cached storage instead of disk, but the backend is off.

**What**

Behind the `tengu_hover_rest` flag, which falls back to off, managed settings can be read from a storage-backed view that refreshes on updates instead of re-reading a file from disk. It stands down and returns to the disk probe if the watch fails, the cached copy is oversized, the cache file is a symlink, or the account changes mid-read. Setting `CLAUDE_CODE_REMOTE_SETTINGS_PATH` skips the priming entirely.

**Details**

- The read is size-capped, and each stand-down is logged.
- Also in this change: permission helper entries armed from remote config are revoked if that arming is withdrawn while a command is running, and a status warning is shown for a remote entry that never ran.

**Evidence**

`Remote settings: storage view stood down (`

- Flag `tengu_hover_rest`: Off in both readings (read for one account on one subscription tier against v2.1.235; this account: off, anonymous baseline: off, compiled default: on)
- Area: Settings
- Names: `CLAUDE_CODE_REMOTE_SETTINGS_PATH`
- Tier: Not switched on
- Useful: 2/5
- Signal: 4/5
- Present in the build but not switched on

### Artifact relay allows subscribe and unsubscribe calls

The artifact relay now forwards subscribe and unsubscribe calls, and answers can carry a reply id.

**What**

The list of routes the artifact relay will forward gains a subscriptions family, `POST /subscribe/{slug}` and `POST /unsubscribe/{slug}`. Alongside it, artifact responses must now carry a strict ISO-8601 `created_at` rather than any non-empty string, and the summon-answer check returns a structured result that can carry a follow-up reply id.

**Evidence**

`POST /subscribe/{slug}`

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

### Conversations pin an `atis` client-config value and persist it in the transcript

A client-config value is now pinned for a whole conversation and saved in the transcript, surviving fork and resume.

**What**

The client-config value named `atis` is now latched once per conversation and preferred over the live config for the remainder of it, so a config change mid-session cannot shift behaviour partway through. The pinned value is recorded in the session state file and in the session transcript, and it survives fork, resume and relocation of a session.

**Details**

- Gated by `tengu_kestrel_moor`, which falls back to on; with the gate off, the live client-config value is read on each use instead of the pinned one.
- Held on the conversation next to the sticky beta flags and the per-turn effort pins, and merged using the same "always" and "last-wins" rules as the existing isolation latch.
- Carried through fork and resume, and readable through the session-state snapshot APIs.
- Written to the transcript as an entry of the form `{ type: "atis-latch", atis: <value>, sessionId: <id> }` whenever the pinned value changes, and re-emitted when a session is forked or moved to a new location.
- The transcript write itself has no gate found on it and happens wherever transcripts are written, including when the latch feature is otherwise off.
- On read, the entry is accepted only if its recorded session id matches the file being read and the value is printable ASCII, so a transcript copied from elsewhere cannot inject a pinned value.

**Evidence**

`atis-latch`, `{ type: "atis-latch", atis: g, sessionId: f },`

- Flag `tengu_kestrel_moor`: Not enough to say (read for one account on one subscription tier against v2.1.235; this account: no value returned, anonymous baseline: no value returned, compiled default: off)
- Area: Client Config
- Tier: Under the hood
- Useful: 2/5
- Signal: 4/5

### `plugin eval` now tells you when a nearby plugin is not being loaded

Plugin eval now tells you when it deliberately skipped loading a nearby plugin it doesn't trust.

**What**

The early-access eval command resolves the target, the plugin root and any untrusted or adopted manifest directory before running, and prints a notice whenever a plugin it found is deliberately not loaded: one above the working directory, one outside the scope it consulted, or one whose ownership it cannot verify. Those cases run against baseline Claude instead of quietly loading someone else's plugin.

**Details**

- `--eval-dir` is validated and vetted with its own refusal codes `eval_dir_refused` and `eval_dir_unvettable`.
- The eval directory may come from the plugin manifest.
- The "no results" help text now names the directory and manifest it actually consulted.

**Evidence**

`eval_dir_unvettable`

- Area: Plugins
- Names: `plugin eval`, `--eval-dir`
- Tier: Use it now
- Useful: 3/5
- Signal: 3/5

### "Always allow" options are validated before they are offered

Always-allow options only appear if their rule validates, and fall back to one-time allow otherwise.

**What**

Bash, skill, workflow, file-edit, web-domain and plan-mode consent prompts no longer build their permission rules when you click. Each option now carries a pre-checked list of permission updates, and accepting just replays that list. If the rule cannot be validated locally the option does not appear at all, and an accept with a missing or invalid entry degrades to a plain one-time allow.

**Details**

- Suggestions arriving from the backend as arrays are run through a length-capped, trap-safe validator before they can become an option.
- The prompt gained a `yes-apply-suggestions` choice; callers thread through persist, suggestion and veto state.
- Permission-mode changes are asserted when the option is built, logging `setModeRow: schema rejected a designed setMode update` on rejection.
- Old inline builders were deleted, including the skill exact/prefix rule builder and the file-edit `.claude` folder scope branch.

**Evidence**

`yes-apply-suggestions`, `setModeRow: schema rejected a designed setMode update`

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

### "Yes, and don't ask again" is hidden when the request cannot be fully displayed

The always-allow option disappears whenever Claude can't show you the full command or URL.

**What**

If Claude Code cannot show the whole command, arguments or URL it is asking about, the always-allow option disappears and approval becomes one-time only.

**Details**

- The dialog computes an "unrenderable" flag from the parameter rows, a failed tool-use render, or a withheld command, args or url; the always-allow option is offered only when that flag is false.
- A companion check hides suggested rules when the decision reason is not classifier-approvable or the ask is capped by org policy.
- The rule label itself is width-checked: a label wider than the dialog is dropped rather than truncated.

**Evidence**

`Yes, and don't ask again for `

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

### Edit, Write and NotebookEdit approvals withhold the diff when it cannot be reviewed

Edit and Write approvals withhold unreviewable diffs and downgrade to one-time approval.

**What**

Instead of showing a truncated or empty diff, these permission prompts now display a message saying the change cannot be reviewed and that approval is therefore one-time only.

**Details**

- Triggered when the old or new content exceeds the size cap or contains control characters, when the existing file is too large to preview, when the path is a network path, or when a notebook cell cannot be parsed or found.
- The payload carries a `contentWithheld` marker and a "no changes" panel in place of the diff.
- The Bash `sed` simulated-edit preview follows the same rule.
- Message text: "Proposed edit is too large to show — cannot be reviewed, so approval is one-time only (deny unless expected)."
- Not gated.

**Evidence**

`Proposed edit is too large to show \u2014 cannot be reviewed, so approval is one-time only (deny unless expected).`

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

### Permission prompts say "deny unless expected" when they cannot show what you are approving

When a tool's parameters can't be rendered, the prompt warns you to deny unless expected.

**What**

When the tool-permission dialog cannot render a tool's parameters, it now replaces the preview with an explicit warning instead of showing a partial or empty one, so an approval prompt never looks complete when it is not.

**Details**

- Triggers: parameters that are not an object, too many parameters, values over the size budget, and values that exceed the depth or element budget.
- Wording examples: "(parameters are not an object — deny unless expected)" and "(value too large or too deeply nested to render — deny unless expected)".
- Not gated; this is on the normal dialog path in this build.

**Evidence**

`deny unless expected`

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

### Permission prompts survive tool parameters that cannot be displayed

A tool whose parameters can't be rendered still gives you a prompt instead of erroring away your choice.

**What**

When Claude Code asks you to approve a tool call, it now guards the step that turns the tool's input into readable text. If the input is not a plain object, or if building the display throws, the prompt still appears with its body replaced by a note that the parameters could not be rendered and advice to deny unless you were expecting the call. Previously such an error propagated instead of producing a prompt, so you could lose the chance to approve or deny.

**Details**

- The tool input is checked for being a plain object before the dialog is built, and any error raised while rendering it is caught rather than thrown onward.
- The dialog is marked internally as a render failure, which is what makes it show the warning text instead of an empty parameter block.
- The message shown is `parameters could not be rendered — deny unless expected`.
- Always on; there is no setting, flag or environment variable that turns it off.

**Evidence**

`parameters could not be rendered \u2014 deny unless expected`, `parameters could not be rendered`

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

### Slash-command arguments are redacted before they appear in the transcript

Slash-command arguments are now redacted for tokens and keys before appearing in the transcript.

**What**

Arguments echoed back as `/command args` now go through the same secret redaction used elsewhere, covering Bearer tokens, Basic auth, `sk-ant-` keys and Slack webhook URLs, including the "Args from unknown skill" warning. An unknown command whose arguments change under redaction is now treated as sensitive.

**Details**

- Plugin names, granted-scope display names, added workspace directory paths and the IDE name in "Opened changes in …" now pass through a display sanitizer
- That sanitizer blanks any string with zero visible width, so invisible characters cannot be used to fake a name

**Evidence**

`Bearer <token>`

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

### Usage breakdown now attributes usage to subagents

Your usage breakdown now shows what share subagents consumed under each parent.

**What**

The usage breakdown adds a line attributing a share of your usage to subagents run under a named parent, shown as a percentage alongside the existing per-MCP-server, per-plugin and per-command lines.

**Evidence**

`% of your usage came from subagents under`

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

### Device bridge shuts down mid-session when its flag is turned off

The device bridge now stops itself mid-session if its switch flips off, but it's off by default.

**What**

The device bridge, which is gated behind the flag `tengu_violin_wood` (default off), now watches for that flag flipping off during a session and shuts itself down. Stop reasons are distinguished: `gate_off` when the feature flag turns off, `egress_denied` when the network-egress gate refuses.

**Details**

- The default liveness check treats a flag value served from the payload, an override, or an explicit disable as "now off"; a mere absence of any served value does not stop it.
- Two injection points, `isStillEnabled` and `onEnabledChange`, are read at the default site but nothing in this build supplies them, so they are seams for tests or an embedding host rather than a live path.
- The bridge also now reports whether it has unsent changes, how many calls are in flight, and an idle signal to the session.

**Evidence**

`[deviceBridge] stopping: the gate turned off`

- Flag `tengu_violin_wood`: Off in both readings (read for one account on one subscription tier against v2.1.235; this account: off, anonymous baseline: off, compiled default: on)
- Area: Device Bridge
- Tier: Not switched on
- Useful: 1/5
- Signal: 4/5
- Present in the build but not switched on

### Fixed-menu fast acknowledgment for artifact comments, behind two nested off-by-default gates

A cheaper canned way to acknowledge artifact comments exists but needs two env vars switched on.

**What**

A second way to produce the quick "on it" reply to an artifact comment: instead of writing one freely, the model picks a numbered line from a fixed list and is told to `output only its number — a single digit, nothing else`, capped at 5 output tokens with a deadline. It runs only when `CLAUDE_CODE_ARTIFACT_COMMENT_FAST_ACK_FIXED` (or `tengu_gorse_sill`) is on, itself inside `CLAUDE_CODE_ARTIFACT_COMMENT_FAST_ACK` (or `tengu_gorse_pylon`). Both fall back to false, so two opt-ins are needed.

**Details**

- Failure telemetry is separate from the free-form path: `fast_ack_select_error`, `fast_ack_select_unparsed`, `fast_ack_select_timeout`, `fast_ack_select_threw`
- Ships with a permission shortcut, `artifact_comment_fixed_ack_on_probe_verdict`, that reuses a verdict from an earlier permission probe instead of asking again
- Nothing in this build turns either gate on, so whether it runs is decided by server-side config or by setting the environment variables yourself

**Usage**

`export CLAUDE_CODE_ARTIFACT_COMMENT_FAST_ACK=1 export CLAUDE_CODE_ARTIFACT_COMMENT_FAST_ACK_FIXED=1` **Evidence** `CLAUDE_CODE_ARTIFACT_COMMENT_FAST_ACK_FIXED`

- Flag `tengu_gorse_sill`: Not enough to say (read for one account on one subscription tier against v2.1.235; this account: no value returned, anonymous baseline: no value returned, compiled default: on)
- Flag `tengu_gorse_pylon`: Not enough to say (read for one account on one subscription tier against v2.1.235; this account: no value returned, anonymous baseline: no value returned, compiled default: on)
- Area: Artifacts
- Names: `CLAUDE_CODE_ARTIFACT_COMMENT_FAST_ACK_FIXED`, `CLAUDE_CODE_ARTIFACT_COMMENT_FAST_ACK`
- Tier: Not switched on
- Useful: 1/5
- Signal: 4/5
- Present in the build but not switched on

### Replies to artifact comments can say which earlier reply they continue

Claude's replies on artifact comment threads could link back to its earlier reply, but that's off.

**What**

A reply Claude posts to a comment thread can now declare the id of its own previous reply in that thread, so follow-ups are threaded rather than looking like fresh answers. It is off unless the server turns on `tengu_medlar_quoin`, whose fallback is off, so replies post exactly as before.

**Details**

- The id is taken from the agent's own last posted reply, with an account-epoch check so an id minted under a different account is not reused.
- The "already answered" duplicate guard also consults the set of reply ids this session posted.
- Two telemetry reasons exist for the gated path: `continues_reply_id_malformed` and `follow_up_marker_unconsumed`.

**Evidence**

`continues_reply_id`

- Flag `tengu_medlar_quoin`: Not enough to say (read for one account on one subscription tier against v2.1.235; this account: off, anonymous baseline: on, compiled default: on)
- Area: Artifacts
- Tier: Not switched on
- Useful: 1/5
- Signal: 4/5
- Present in the build but not switched on

### Reply bookkeeping for comment follow-ups, gated off

Bookkeeping that tracks Claude's own posted comment replies is present but entirely switched off.

**What**

Supporting machinery for threading comment replies is present and inert: the reply POST body can carry a `continues_reply_id`, posted reply ids are kept in a size-capped set, and the duplicate guard consults that set instead of trusting the server's `acknowledge_duplicate` alone. Every branch sits behind `tengu_medlar_quoin` with fallback off.

**Details**

- Reply records became account-epoch aware; a reply recorded under a different account epoch is ignored.
- `continues_reply_id_malformed` and `follow_up_marker_unconsumed` only fire once the gate is on.

**Evidence**

`continues_reply_id_malformed`

- Flag `tengu_medlar_quoin`: Not enough to say (read for one account on one subscription tier against v2.1.235; this account: off, anonymous baseline: on, compiled default: on)
- Area: Artifacts
- Tier: Not switched on
- Useful: 1/5
- Signal: 4/5
- Present in the build but not switched on

### Enter no longer submits a text field in a select list, and arrows can leave the list

Enter no longer accidentally submits a text field in a list, and arrows can leave the list.

**What**

In lists that mix pickable options with a text-entry option, pressing Enter while on the text field no longer accepts it as a selection, and the accepted value is read at the moment you press Enter so a stale highlight cannot be submitted. Pressing down on the last option or up on the first now moves focus out of the list instead of sticking.

**Details**

- Applies to down/up and to ctrl-n/ctrl-p.
- Edge navigation calls the list's exit-downward and exit-upward handlers and stops the key from being handled further.

**Evidence**

`select:accept`

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

### Language-server startup stops holding back tool calls after the first connect

Tool calls no longer stall waiting on language-server startup once any server has connected.

**What**

The check that suppresses work while a language server is "still starting" now returns false as soon as any server has connected, tracked by a new `hasEverConnected` flag. Previously a later pending or not-started status could stall tool calls again.

**Evidence**

`hasEverConnected`

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

### MCP tool permission prompts show parameters as a readable table

MCP permission prompts now show parameters as a readable table with links and real dates.

**What**

When an MCP tool asks for permission, its parameters are now laid out as key/value entries rather than raw JSON, with syntax-highlighted code, clickable http(s) links and epoch timestamps rendered as readable dates.

**Details**

- A `code` parameter is highlighted when a sibling `language` parameter is a short string. Highlighting is skipped for values over 50000 characters.
- Timestamps are annotated when the tool's JSON Schema declares `format: date-time`, or for Slack-style `ts` / `thread_ts` epoch strings.
- Rendering is bounded: 80 columns for the inline layout, 200000 code units per key or value, and 64 levels of nesting before the value is reported as exceeding the display depth bound. Larger structures fall back to a bordered block.

**Evidence**

`value exceeds the display depth bound`

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

### Vim cursor position survives re-renders and editor round trips

Your vim cursor position no longer jumps when the prompt input redraws or returns from an editor.

**What**

Vim mode and the cursor offset now live in a shared store, so the prompt input restores where your cursor was instead of jumping when the input is re-created. Vim mode starts in INSERT and the cursor offset starts unset.

**Details**

- The input seeds its cursor from the saved offset, saves it again on unmount, and re-clamps it in NORMAL mode using NFC-normalized text so multi-byte characters do not shift it.
- Turning vim mode off forces the mode back to INSERT.
- Returning from an external editor also re-clamps the cursor for NORMAL mode.

**Evidence**

`savedCursorOffset`

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

### `--eval-dir` on the eval command, with warnings that name the fix

The eval command reads its directory from a flag or the manifest, warning clearly when the value is malformed.

**What**

The early-access `plugin eval` command takes `--eval-dir <dir>`, or reads the directory from the plugin manifest, with `evals/` as the fallback; a mis-shaped manifest value produces a warning that says what to change.

**Details**

- Case discovery checks the target path for trust before walking it, and resolves relative paths against the working directory.
- The error shown when an ablation run has no plugin now suggests targeting the plugin directory itself.

**Evidence**

`--eval-dir <dir>`

- Area: Plugins
- Names: `--eval-dir`, `plugin eval`
- Tier: Use it now
- Useful: 2/5
- Signal: 2/5

### Eval results written inside a plugin warn that they may load as components

Eval results saved inside a plugin now warn they might get loaded as plugin components.

**What**

When results go under the plugin because no `--output-dir` was given, the command compares the destination against the plugin's layout and warns that files written there may be picked up as plugin components, pointing at `--output-dir`.

**Details**

- Case discovery refuses symlinked directories.
- A directory with no prompt.md or case.yaml is logged and scanned for cases rather than treated as one.

**Evidence**

`files written there may be loaded as plugin components; pass --output-dir to write elsewhere`

- Area: Plugins
- Names: `--output-dir`
- Tier: Use it now
- Useful: 2/5
- Signal: 2/5

### Eval runs record their output directory and show per-case advisories

Eval runs record which directory they used and show per-case warning notes in the HTML report.

**What**

`plugin eval` and `plugin eval init` accept `--eval-dir`; the value is verified (failing with "--eval-dir cannot be verified: "), passed into the generated interview prompt and the child process that runs the cases, and stored as `out_dir` in the run metadata. The scored HTML report renders per-case advisories as warning notes.

**Details**

- Both commands stay behind the `pluginEval` early-access gate.

**Evidence**

`--eval-dir cannot be verified: `

- Area: Plugins
- Names: `--eval-dir`, `plugin eval`, `plugin eval init`
- Tier: Use it now
- Useful: 2/5
- Signal: 2/5

## New Features

### Document artifacts keep comment threads pinned to blocks that get edited

Comments on a document artifact now stay attached to the right block after you edit the document.

**What**

The document artifact template now stamps every commentable block (headings, paragraphs, list items, table cells) with a short stable id, so comments stay attached to the block they were left on when the document is edited.

**Details**

- Ids authored in the file are kept; duplicates created by pasting or splitting a block are shed, and duplicate ids are stripped again on save.
- The script re-runs on later DOM changes via a MutationObserver, so blocks added after load also get ids.
- The skill instructions now tell Claude to preserve existing block ids when editing, because renaming or dropping one detaches that block's comment thread.

**Evidence**

`DOC:anchors:begin`

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

### Plugin manifests are ignored in directories Claude Code cannot vouch for

Plugin folders that other users can write to are now refused instead of loaded.

**What**

Before reading a plugin's manifest, Claude Code now checks that the plugin directory is actually owned by you and not writable by anyone else. Trees that fail the check are refused, so a plugin folder someone else can edit no longer gets consulted.

**Details**

- Refusal conditions include: owner uid that is not the real user, other-writable or group-writable modes, non-sticky writable ancestors, hard links, symlinks pointing outside the tree, and `.git` metadata that was not created locally.
- The walk is capped by a maximum entry count and depth, so a huge or deeply nested tree stops the scan rather than hanging it.
- Group membership is resolved by reading /etc/group, /etc/passwd and /etc/nsswitch.conf. If nsswitch lists a non-file source for groups, membership is treated as unverifiable and the refusal says so.
- Running under sudo is handled: SUDO_UID, SUDO_GID and SUDO_USER are cross-checked against /usr/bin/id. Container evidence (/run/.containerenv, /proc/1/cgroup) relaxes the uid rules.
- macOS skips the group-writable check, and the whole layer needs process.getuid to be available.

**Evidence**

`is group-writable, and group membership cannot be verified on this system (`

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

### Very large plans cannot be approved

Plans over 200,000 characters or with cut-out content can no longer be approved, only sent back for more planning.

**What**

When Claude finishes planning and asks to proceed, the plan text is now checked against a 200,000-character cap and a test for elided content first. A plan that fails either check offers no approval option at all: every "Yes" row is removed and only "No, keep planning" remains, with an explanation in the dialog. Unconditional in this build.

**Details**

- The dropped options are the clear-context, bypass-permissions, auto mode, accept-edits and manually-approve variants, plus the shift+tab hint.
- The dialog reads "(the plan is too large to be shown in full — approval is withheld; send feedback asking for a shorter plan, or press Esc)".
- The way out is to reply asking for a shorter plan, or press Esc.

**Evidence**

`(the plan is too large to be shown in full \uFFFD approval is withheld; send feedback asking for a shorter plan, or press Esc)`

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

### Context-limit banner tells you auto-compact is switched off

Hitting the context limit now tells you auto-compact is off and where to switch it back on.

**What**

When you hit the context limit and auto-compact is off, the banner now says "auto-compact is off" and points at `/config` to turn it on. The hint only appears when you turned it off yourself: the `autoCompactEnabled` setting (default true) resolving to false in user settings, or in legacy global config while user settings are in play.

**Details**

- Setting `DISABLE_COMPACT` or `DISABLE_AUTO_COMPACT` in the environment suppresses the hint; `DISABLE_COMPACT` still selects the "/clear to continue" wording instead.
- The inline status-line message was pulled out into its own component, which appends the hint when there is no remote autocompact state.
- Reachable for everyone on this build, no flag.

**Evidence**

`auto-compact is off`, `/config to turn it on`

- Area: Compaction
- Names: `/config`, `autoCompactEnabled`
- Tier: You'll notice
- Useful: 3/5
- Signal: 1/5

## Improvements

### Cloud session bundling treats "behind remote" as its own outcome

Packaging work for a cloud session now recognises "behind remote" and can start from your local commit.

**What**

When packaging your work for a cloud session, being behind the remote is now distinguished from having otherwise diverged from it, using a merge-base check, and the caller can start from your local commit in that case instead of refusing. When ancestry cannot be determined it warns instead.

**Details**

- The message shown when a session could not be armed is now assembled from a table of reasons covering bundling being unavailable, the size limit being exceeded, and the fact that syncing files back to your machine is not supported on Windows yet, replacing one fixed sentence.
- No gate on the verdict change itself.

**Evidence**

`syncing files back to this machine is not supported on Windows yet`

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

### Stricter rules for policy helpers arriving over remote settings

Policy helpers pushed via remote settings must be real local executables; network paths and inline scripts are refused.

**What**

A policy helper delivered through remote managed settings must now be a real local executable path. UNC paths, network automounts (`/net`, `/Network/Servers`) and kernel magic links (`/proc`, `/dev/fd`) are rejected, and inline scripts are refused outright.

**Details**

- Any `claudeMd` field inside a remote-delivered `managedSettings` payload is deleted before the settings are used.
- These checks apply only on the remote-delivery path; locally installed managed settings are validated as before.

**Evidence**

`path must not be a UNC, network-automount (/net, /Network/Servers) or kernel magic-link (/proc, /dev/fd) path when delivered via remote managed settings: `

- Area: Policy Helpers
- Tier: Under the hood
- Useful: 3/5
- Signal: 3/5

### Container arming handles a checkout that is behind its remote

Container sessions from a checkout behind its remote can now seed from the remote ref.

**What**

When starting a container-backed session from a repo whose checkout is behind the remote, a pinned session now seeds from the remote ref using the tree it probed; otherwise it reports as not armed through the same builder used for uncommitted tracked changes and other diverged states.

**Evidence**

`case "head_behind_remote":`

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

### Git status can report a checkout that is behind its remote

Git status now names when your checkout is behind its remote and caps untracked files seeded remotely.

**What**

Branch and worktree comparisons now have a distinct "is behind" result naming the remote, with an explanation that work will start from this checkout's commit rather than the remote tip, as of the last fetch. A separate new message caps how many untracked files can be seeded into a remote session.

**Details**

- The untracked-file message names a seed budget limit that the file count exceeded.
- Not gated.

**Evidence**

`head_behind_remote`

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

### The auto-mode environment setup prompt now waits until you have declined it several times

The auto-mode environment setup offer only shows the fuller flow after you've declined several times.

**What**

Declining the prompt that offers to set up an environment for auto mode now increments a stored `denials` counter, capped at 5, and the fuller onboarding flow is only offered once those denials have accrued. Choosing later or dismissing records the dismissal instead.

**Details**

- Other conditions for offering it: enough startups have happened, no environment configuration already exists, and it has not been dismissed before.
- It is skipped entirely if the auto-mode-setup skill is turned off via skill overrides.
- Choosing later or dismiss stores a timestamp and dismissal flag and reports the event `tengu_auto_mode_env_onboarding_later`.

**Evidence**

`autoModeEnvSetup`

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

### Clearer refusals when resuming automatic replies to a watched artifact

Resuming artifact auto-replies now explains precisely which stop is blocking you.

**What**

Resuming automatic replies on an artifact now distinguishes two situations that previously fell together. A live watch connection that began before the watch was stopped and is still winding down cannot be attached to by a resume, and replies being held by the artifact's own watch stop rather than by a separate stop on the replies are refused with a message that there is no separate auto-reply stop to resume and that only an approved re-watch releases them.

**Details**

- The tool's permission description now discloses that resuming will also restart watching the artifact, so the reader knows a re-watch is coming before approving.
- The still-winding-down connection case is tracked internally as `arm_in_flight`, the marker for a watch connection that opened before the stop and has not yet finished closing.
- Two new explanations cover wake subscriptions, the mechanism that lets an artifact wake a session, failing to be set up, including `Wake subscriptions are not provisioned in this deployment; retrying will not help.` which tells the reader a retry is pointless.
- Unconditional and part of the artifact tool surface, with no flag, setting or environment variable gating it.

**Evidence**

`Wake subscriptions are not provisioned in this deployment; retrying will not help.`, `arm_in_flight`

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

### Artifact asset reads say where they can run

Refused artifact asset reads now say where such reads work, and public assets skip the gate.

**What**

The refusal shown when an artifact asset cannot be read now states exactly where such reads work: a local session, or an Anthropic-hosted cloud session with its gateway relay enabled. Reads of public assets are no longer subject to the same access gate as private ones.

**Details**

- The message ends "retrying from here will not help", so there is no point re-running.
- Detection of a remote session moved from an inline environment-variable test to a shared helper.

**Evidence**

`asset reads run only from a local session or an Anthropic-hosted cloud session with its gateway relay enabled; retrying from here will not help`

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

### Auto-reply arming explains an in-flight wind-down

Arming auto-replies during a winding-down connection now says so plainly instead of silently skipping.

**What**

Trying to arm auto-replies while an earlier connection is still closing now says so plainly: "auto-replies not resumed (an earlier connection is still winding down; the stop stays)", instead of a vague skip.

**Evidence**

`auto-replies not resumed (an earlier connection is still winding down; the stop stays)`

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

### Asset store failure explains what is actually required

Asset upload and delete failures now name the setups that work and say retrying won't help.

**What**

When asset uploads, listings or deletes cannot run, the message now names the supported setups (a local session, or an Anthropic-hosted cloud session with its gateway relay enabled) and says retrying will not help, instead of only reporting a missing route.

**Evidence**

`asset uploads, listing, and deletes run only from a local session or an Anthropic-hosted cloud session with its gateway relay enabled; retrying from here will not help`

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

### Long or hostile text in permission prompts is truncated rather than shown raw

Long or hostile text in permission prompts is now truncated and unnamed servers show a placeholder.

**What**

MCP elicitation text, MCP and WebSocket tool descriptors, Bash commands, skill names and descriptions, notebook cell ids and artifact labels now go through display sanitizers with a length cap, and missing MCP server or tool names show a placeholder such as "(unnamed server)".

**Details**

- The URL elicitation dialog only preselects the accept option when both the message and the URL survive sanitizing unchanged and contain no newlines; otherwise the accept handler refuses to open the URL.
- An option preview that is too long is marked withheld, and its always-allow row is dropped.

**Evidence**

`(unnamed server)`

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

### Launching a subagent without naming one now fails with the list of agents

Launching a subagent without naming one now errors and lists the agents you can pick.

**What**

Omitting `subagent_type` on the Task tool used to fall through to a default agent. It is now rejected, unless your agent set leaves only one possibility, with an error naming the available agents, or "none" when there are none.

**Details**

- Reported as `subagent_type_missing`, with a `tengu_subagent_type_miss` event recording how many agents were available.
- The refusals specific to forking (remote isolation, recursive fork) moved into one shared check, so the same wording is reused when that agent list is built.

**Evidence**

`subagent_type_missing`

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

### The MCP open-a-URL prompt refuses URLs it cannot show exactly

If an MCP server's requested URL can't be shown in full, you can only decline it.

**What**

When an MCP server asks to open a URL, the confirmation dialog now renders it through a preview that reports whether the full URL can be displayed. If it cannot, Accept is not offered at all and Decline is the only choice.

**Details**

- The explanatory line reads: "This URL cannot be shown exactly as it would open, so opening it is disabled. Decline to continue."
- This blocks approving a link whose displayed form differs from what would actually open.

**Evidence**

`This URL cannot be shown exactly as it would open, so opening it is disabled. Decline to continue.`

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

### Plans over 200,000 characters cannot be approved

Plans longer than 200,000 characters can't be approved; you're asked to request a shorter one.

**What**

An extremely long plan is now shown truncated and the approval option is withheld, with a message telling you to send feedback asking for a shorter plan or press Esc.

**Details**

- The threshold is a fixed 200,000 characters of plan text.
- Below the limit, plan approval behaves as before.

**Evidence**

`approval is withheld; send feedback asking for a shorter plan, or press Esc`

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

### Malformed permission and control messages are rejected instead of acted on

Malformed permission requests from workers or SDK clients are denied immediately instead of reaching your dialog.

**What**

Permission requests arriving from a worker, an SDK client or a direct socket connection are now checked for shape first. A structurally malformed permission request gets an immediate deny reading "Malformed permission request from worker" rather than reaching the approval dialog.

**Details**

- A `can_use_tool` message whose request id is not a string is dropped and logged, since there is no way to reply to it.
- The direct socket handler rejects control requests that carry no request object or no string request id.
- The SDK message adapter drops conversation-reset messages without a string new conversation id, and tolerates a result whose errors field is not an array instead of trying to render it.
- Unconditional, no flag.

**Evidence**

`Malformed permission request from worker`, `[DirectConnect] Dropping control_request without a request object / request_id`

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

### Permission prompts refuse to render unbounded tool arguments

For unknown or remote tools the prompt shows just the tool name, not sprawling arguments.

**What**

For tools this process does not know about, such as remote ones, the permission prompt now shows the tool name rather than a rendering of its arguments. The stand-in preview refuses to draw anything unbounded: more than three parameters, over-long keys or values, or oversized arrays and objects.

**Details**

- The old pre-render helpers, and their catch-all fallback that reported render errors, were removed.
- The refusal path is a thrown error carrying `stub preview cannot render an over-bound parameter`, so nothing attacker-influenced is drawn.
- No gate; reachable on this build.

**Evidence**

`stub preview cannot render an over-bound parameter`

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

### Oversized cross-session messages fail with a specific error

Oversized messages between sessions now fail with the actual size, the limit, and advice to use a file.

**What**

Sending a message between sessions that exceeds the size cap now raises a dedicated `message_too_large` error naming the actual character count and the limit, and suggesting you put bulk content in a file instead.

**Details**

- Applies to serialized messages on the cross-session delivery path.
- No gate; always active on that path.

**Evidence**

`cross-session message exceeds the line cap`

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

### Artifact asset approvals and messages reworded

Approving an artifact asset listing now also covers saving those assets, so you aren't asked twice.

**What**

Approving a read-only listing of an artifact's assets now also covers saving that artifact's assets locally for the rest of the conversation, so you are not asked twice.

**Details**

- The denial text for reading an asset was rewritten to distinguish a bad asset id from an output directory that cannot be resolved.
- The "upload target changed" error now also triggers when the action changes, not only when the URL changes.

**Evidence**

`list an artifact's assets (read-only; a first approval also covers saving this artifact's assets locally for the rest of the conversation)`

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

### Question prompts adapt when a screen reader is in use

Multiple-choice questions now adapt to screen-reader mode and explain when a preview can't be shown.

**What**

The multiple-choice question flow now knows whether screen-reader mode is on and builds its prompts and responses accordingly. When an option preview cannot be shown it prints "(preview cannot be shown in full — compare the option labels and descriptions instead)". Previews are now offered only for single-select questions that define one.

**Evidence**

`isScreenReader`

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

### Oversized file content is withheld from permission dialogs rather than partially shown

File consent prompts now say content was withheld rather than showing a partial or empty preview.

**What**

File-consent prompts, including ones answered remotely, now carry a flag saying the content was withheld, and a notebook past the byte cap resolves to an explicit "too large for preview" result the dialog reads instead of assuming content is there.

**Details**

- The `contentWithheld` flag travels through the remote permission dialog path, so a remotely answered prompt knows the preview was incomplete.
- Notebook previews over the cap return no notebook plus `tooLargeForPreview` set, and the dialog branches on that flag.
- Not gated.

**Evidence**

`tooLargeForPreview`

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

### File paths shown in the terminal are stripped of control characters

File paths in tool lines and consent dialogs are stripped of control characters so filenames can't fake output.

**What**

Paths displayed in tool-use lines and file access consent dialogs now pass through a sanitiser that removes or replaces control and non-printable characters before rendering, so a crafted filename cannot rewrite what you see when deciding whether to approve something.

**Details**

- When the sanitiser changed the string, a replacement character (U+FFFD) is appended so the display is visibly marked as altered.
- Diff line rendering runs its output through the same escaping pass.
- A separate escaper neutralises `\uXXXX` escape sequences and non-ASCII bytes.
- Not gated; applies to everyone.

**Evidence**

`\uFFFD`

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

### Wider environment scrubbing before a policy helper is spawned

Policy helper processes now start from a scrubbed environment, so loader and interpreter tricks can't hijack them.

**What**

Helpers now start from a locked-down environment: credential variables are stripped as before, and loader and interpreter hijack variables (`LD_`, `DYLD_`, `NODE_OPTIONS`, `BASH_ENV`, `CLASSPATH`, `OPENSSL_CONF`) are dropped too, with `PATH`, `COMSPEC`, `ZDOTDIR`, `XDG_CONFIG_HOME` and the git config paths pinned at fixed values pointing into an empty directory.

**Details**

- The credential list (`ANTHROPIC_API_KEY`, `AWS_SECRET_ACCESS_KEY`, `GOOGLE_APPLICATION_CREDENTIALS` and others) and the TOKEN/SECRET/PASSWORD name pattern moved into a dedicated module next to the new sanitizer.
- No flag gates this; it applies whenever a policy helper is spawned.

**Evidence**

`/var/empty/claude-code-policy-helper`

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

### "Don't ask again" options carry a pre-checked rule instead of building one on the spot

"Don't ask again" buttons now carry a pre-checked rule, so what you approve is what gets saved.

**What**

Chrome/domain, Skill, Bash-prefix, plan-mode and MCP permission dialogs no longer assemble a permission rule when you click the button. Each option is created up front as a row holding both its label and the exact rule it would write, and that rule is frozen and checked for being plain data before anything is saved.

**Details**

- The check rejects functions, accessors, symbol keys and non-plain prototypes in the rule payload.
- If the row is missing or fails its check at answer time, the dialog falls back to a plain one-time allow or a deny, and writes no rule.
- The always-allow row is withheld entirely when the request came from a safety check that a classifier cannot approve, when org policy caps it, or when the host contains a wildcard.

**Evidence**

`deepFreezePlainData: accessor property in mint input`

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

### Messages from a cloud session are validated before being used

Messages arriving from a cloud session are checked field by field so bad frames can't corrupt your transcript.

**What**

The layer that converts messages arriving from a remote Claude Code session into local ones now checks every field instead of trusting it, so a malformed frame is dropped or repaired and logged rather than corrupting the local transcript.

**Details**

- A missing or non-string message id gets a freshly generated one, logged at error level.
- Frames whose `content` is not a string are dropped, as are tool-progress frames with a non-string tool name or a non-finite elapsed time, and compaction-boundary frames with no metadata.
- On the session-init frame, each of model, working directory, slash commands, MCP servers and tools is ignored with a log line when it arrives in the wrong shape, rather than being adopted.

**Evidence**

`[sdkMessageAdapter] init frame tools is not an array`

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

### Ask-user questions carry structured display text and optional previews

Questions asked of you now carry structured text and optional previews, with multiline questions shown with a gutter.

**What**

Questions asked of you are now keyed by an identifier rather than by the question text, and carry a display object with the text plus a flag that renders multiline questions with a gutter. Option previews became tagged objects, so a preview can be withheld rather than always rendered.

**Details**

- A preview marked full carries markdown; anything else renders no preview.
- Preview width measurement, submission and the answer summary view were all moved to the new shape.
- Screen-reader mode suppresses the note and preview path entirely.

**Evidence**

`displayQuestion`

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

### Git commands hardened against graft files

Git commands now ignore graft files, so a repo can't fake the commit history Claude sees.

**What**

Every git subprocess on this path now runs with `-c advice.graftFileDeprecated=false` and `GIT_GRAFT_FILE` pointed at `/dev/null`, so a graft file in a repository cannot rewrite the commit history Claude Code sees.

**Details**

- Joins the existing environment hardening: `GIT_ALLOW_PROTOCOL=none`, `GIT_NO_LAZY_FETCH`, `GIT_NO_REPLACE_OBJECTS`.
- Unconditional.

**Evidence**

`advice.graftFileDeprecated=false`

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

### Better labels for org-disabled and stalled-stream errors

Org-disabled and stalled-stream failures now get accurate error labels, including a second auth class.

**What**

"Session webhooks are not enabled for this organization" is now classified as `org_not_enabled` like the equivalent tool message, a stalled response stream is classified as `api_timeout` whenever the error is a stream idle timeout rather than only when the text starts that way, and a second auth failure class now reports as `auth_error`.

**Evidence**

`Session webhooks are not enabled for this organization`

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

### Requests to the frame host stop following redirects

Requests to the frame host no longer follow redirects, so credentials can't be passed onward.

**What**

Calls to the "frame" API host now set `maxRedirects: 0`, and the shared request helper defaults to 0 for that host. Redirects used to be followed, which could pass credentials on to whatever the redirect pointed at.

**Evidence**

`maxRedirects`

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

### Bug reports are trimmed to fit an 8 MiB upload limit

Bug reports from long sessions get trimmed to fit the upload limit instead of failing outright.

**What**

Feedback submissions are now measured before upload and trimmed to 8388608 bytes (with 65536 bytes of headroom) instead of failing, so long sessions can still file a report.

**Details**

- Trimming drops the oldest transcript messages first, removes individually oversized messages, and shrinks or omits the raw session log tail.
- A short machine-readable note summarising what was dropped is appended to the report description.
- If the payload is still over budget after trimming, an error-level log entry is written.
- The `feedback_draft_submit` failure event gained a `from_this_session` dimension.

**Evidence**

`(trimmed client-side to fit the upload size limit)`

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

### Notebook edit approvals say why a cell preview is blank

Notebook edit approvals now tell you why the cell preview is blank instead of showing nothing.

**What**

When a NotebookEdit approval cannot show the cell being changed, it now prints the reason inline instead of an empty diff: the notebook is too large for preview, the preview is unavailable, or the cell was not found. The message notes that approving still applies the edit to the real cell.

**Evidence**

`notebook too large for preview`

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

### Several finished background tasks now report as one line

A batch of finished background tasks now reads as one summary line instead of many.

**What**

Consecutive completion notices are collapsed by kind, so a batch of finished remote tasks reads as a single "remote tasks completed" summary rather than one line each. Only bash completions are registered in the background-command map.

**Details**

- Streamed remote session events are now accumulated per task: event count, tool call count, agent spawn count, last tool used and a bounded tail of recent events.
- That per-task log is cleared on reset and dropped when the task is evicted.

**Evidence**

`remote tasks completed`

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

### Session-scoped permission answers describe the rules they actually add

"Yes, during this session" now describes exactly the rules and directories it grants.

**What**

The "Yes, during this session" choice on a permission prompt is now built from the rules, mode and directories the answer would grant, rather than a hand-written label per case, so the wording matches what is really being allowed.

**Details**

- Adds a label for the read-only rule family, for example "Yes, allow reading from ".
- A dedicated predicate marks the read rule family that carries no shell tool, so read grants are not described as command permissions.

**Evidence**

`Yes, allow reading from `

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

### Nested ordered lists use letters and roman numerals, and wrap under the marker

Nested numbered lists now use letters and roman numerals, and wrapped lines line up properly.

**What**

Markdown output now numbers nested ordered lists by depth: a/b/c at depth 2 and roman numerals at depth 3. Continuation lines of a list item are indented to line up under the marker.

**Details**

- Roman numerals are used only while the last item number is 3999 or below; past that the level falls back to plain digits.
- The hanging indent is skipped when an item leads with a code block, blockquote, horizontal rule, table or nested list.

**Evidence**

`listIndent`

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

### Notebook diffs say when a file is too big to preview

Notebook diffs now say a file is too big to preview instead of looking empty.

**What**

Loading a notebook for an edit preview now returns a structured result, so an over-size notebook is marked as too large rather than coming back empty. The diff view can distinguish "too big to preview" from "there was no previous content".

**Details**

- The too-large case is flagged explicitly instead of returning a null notebook, which previously looked identical to an absent one.

**Evidence**

`tooLargeForPreview: !0`

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

### Cloud session detail panel stops rescanning the transcript

The cloud session detail panel is much faster on long sessions since it reads stored counts.

**What**

The session view for Claude Code on the web used to walk every assistant message and tool-use block on each render to count agents and tool calls. It now reads those counts and the last tool used from a stored per-session stats record. Same display, far less work on long sessions.

**Evidence**

`Waiting for the server to confirm completion…`

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

### LSP tool says why it failed to start

The language-server tool now reports the actual startup failure instead of an unexplained error.

**What**

When language-server support is unavailable, the tool now checks whether startup actually failed and returns the recorded failure text instead of an unexplained internal error.

**Details**

- The message is built at runtime from a fixed prefix plus the underlying error.
- The failure is also written to the error log.

**Evidence**

`LSP initialization failed: `

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

### Bug reports that are too large drop the transcript automatically

A too-large bug report now unchecks the transcript for you and returns you to review.

**What**

If a bug report fails to submit because the payload is too large, the transcript attachment is unchecked for you and you are returned to the review screen to send it without.

**Details**

- Feedback telemetry gained a `transcript_trimmed` dimension recording when this happened.
- Unconditional on the submission failure path.

**Evidence**

`transcript_trimmed`

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

### A remote-armed policy helper's output is discarded if it is revoked mid-run

A policy helper revoked mid-run has its result thrown away with a warning.

**What**

If the remote arming of a policy helper changes while the helper is still executing, its result is thrown away rather than applied, and a warning appears prefixed "remote policyHelpers entry not run: ".

**Details**

- The warning is cleared once the remote settings subject changes back.
- The discard is recorded on the `settings_policy_helpers_per_os` telemetry event with reason `deactivated_during_exec`.

**Evidence**

`remote policyHelpers entry not run: `

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

### Two prompt wording corrections

Two prompt wordings were corrected around subagent types and artifact user identity.

**What**

Subagent guidance no longer claims that leaving the subagent type out starts a fresh agent; it now says only that other values do. The artifact tool description attributes the current user id to the page's user capability rather than to a specific function call.

**Evidence**

`Other subagent_type values start fresh agents with no context.`

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

### Purging history refuses outright when the file cannot be safely rewritten

Purging history now fails loudly if the history file can't be safely rewritten.

**What**

Counting and rewriting `history.jsonl` now raise an error saying the file was not rewritten when the history file is a symlink or not a regular file, or when the read or rewrite is declined, instead of failing quietly.

**Details**

- The code prefers an optional storage backend for the paged scan and rewrite; that backend is never constructed in this build, so the existing line-by-line path is what actually runs.
- Failure telemetry is now reported per failure code, with `multiple_failures` when more than one code occurs, rather than a blanket `config_write_failed`.

**Evidence**

`purge history rewrite refused`

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

### Clearer wording when a remote session refuses a permission-mode switch

A refused permission-mode switch now says "Remote session" and names the mode as you see it.

**What**

The notice now reads "Remote session couldn't switch to" rather than "Cloud session", and names the mode the way it appears in the UI instead of showing its internal id.

**Evidence**

`Remote session couldn't switch to `

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

### Oversized-line error now states the actual limit

The oversized-line socket error now reports the real configured limit rather than a hardcoded figure.

**What**

The local socket messaging error no longer hardcodes a 1 MiB buffer figure; it reports the configured limit and describes it as a line length.

**Evidence**

`[uds-messaging] Line exceeded `

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

## Bug Fixes

### An approved database read or write is bound to the exact target

Approving a database read or write now locks it to that exact action and project.

**What**

When you approve a Claude Design `read_db`, `write_db` or `watch` call, the approval now records which action and which project it was for and re-checks that at execution time. If the action or the target URL changed after you approved, the call is refused with a `db_target_changed` error instead of running against a different database.

**Details**

- Consent is stored as an action plus project slug pair in tool state.
- The refusal message states that nothing was written and asks you to retry so the call is checked again.
- No flag involved; this is active in this build.

**Evidence**

`` `action` or `url` no longer names the database write that was approved — nothing was written; retry so it is checked again ``

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

### Artifact tool approvals cannot be swapped to a different target

An approved artifact tool call can no longer be redirected to a different project or action.

**What**

Approving an artifact tool call now attaches a hidden marker recording exactly which action and which project were approved. Before the privileged step runs, that marker is read back and must match the action and project actually being executed, otherwise the call fails with `asset_target_changed`.

**Details**

- Covers `list_assets`, `read_page_data`, `watch`, `read_db` and `write_db`.
- The marker is stored as a non-enumerable property and read with `Reflect.get`, so it cannot be spoofed by ordinary input fields.
- An input carrying any of the other four consent markers is rejected outright.
- Unconditional; there is no flag to turn this off.

**Evidence**

`__artifactConsentPin`

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

### Bad messages from a worker or remote session no longer take the client down

A malformed message from a worker or remote session is dropped instead of crashing your client.

**What**

Three guards land together so a malformed or handler-crashing message is dropped rather than crashing or producing an empty error.

**Details**

- A worker control response with a non-string or unrecognized error now yields a described Error instead of `Error(undefined)`
- Every incoming message in the SDK and sessions client goes through a wrapper that catches handler exceptions, logs the dropped message's type and subtype, continues, and reports `remote_connect_frame_handler_threw`
- Thin-client control replies are validated against their schema; a mismatch returns null and logs the first schema problem instead of the payload being trusted
- History prefetch converts each message in its own try/catch and skips the ones that fail

**Evidence**

`Malformed control_response from worker`

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

### Unloaded MCP tools are recognised as MCP in permission prompts

Permission prompts now recognise MCP tools by name even when the server isn't loaded.

**What**

When Claude Code builds a placeholder for a tool name it does not have loaded, it now decides whether the tool is an MCP tool from the `mcp__` name prefix instead of always answering no. Permission rendering and rule matching now treat those names as MCP tools even when the real tool is unavailable.

**Evidence**

`mcp__`

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

### Template placeholders no longer pick up built-in property names

Template placeholders like {{constructor}} stay as text instead of expanding into junk.

**What**

Substituting `{{name}}` in a prompt or command template now requires the key to be defined directly on the supplied values. Names inherited from JavaScript's base object, such as `constructor` or `toString`, are left as literal text instead of being replaced with function source.

**Evidence**

`Object.hasOwn(t, i) ? (t[i] ?? o) : o`

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

### Question options with identical text are no longer confused with each other

Multiple-choice questions with two identically worded options no longer mix up your answer.

**What**

Multiple-choice questions now identify each question and option by a stable key rather than by the text shown on screen, with display text carried separately. Selecting, restoring focus and looking up answers all use the key, so two options that read the same no longer behave ambiguously.

**Evidence**

`displayLabel`

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

### Remote plugin reload survives a partial response

Reloading plugins on a remote session no longer errors when counts are missing from the reply.

**What**

`/plugin` reload against a remote session no longer fails when the remote reply omits some counts. Each of plugins, commands, agents and MCP servers is optional, and if none are present the command reports "Reloaded on remote." rather than erroring.

**Details**

- Remote session startup reads slash commands, tools and MCP server counts the same defensive way and logs "no" where a count is unknown.
- Only reachable in remote-control sessions.

**Evidence**

`Reloaded on remote.`

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

### Undecodable remote replies are reported, not silently mishandled

You get a clear message when a remote reply can't be read instead of a blank screen.

**What**

The context-usage command and the feedback/review path now validate what the remote sent back and tell you the reply cannot be displayed by this version, instead of rendering blank or proceeding with malformed data.

**Details**

- Unconditional on both paths.

**Evidence**

`Couldn't show context usage: the remote sent a reply this version can't display`

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

### Vim mode never starts in VISUAL

Vim mode always opens in NORMAL, never stuck in VISUAL.

**What**

If the editor is handed an initial mode of VISUAL or VISUAL LINE, it starts in NORMAL instead. Mode changes made after the editor is mounted still apply exactly as given.

**Evidence**

`e.initialMode === "VISUAL LINE"`

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

### Assorted robustness and wording fixes

Small wording and reliability corrections across messaging, feedback and file handling.

**What**

A batch of small corrections across messaging, feedback and file handling.

**Details**

- Messages arriving without a valid id get one generated, with an error logged, instead of passing an undefined id along
- The feedback "payload too large" error now reads "turn the transcript toggle off, then Send again." and marks the result as too large
- A storage purge failure is logged before the older code path is tried
- The plugin cache post-install warning dropped its "(or no longer)" hedge
- Temporary files ending in `.tmp~` plus eight characters are now excluded
- A session-directory check reports a distinct "junk" outcome when it finds a regular file where a directory was expected
- Reading enum values from a JSON schema tolerates a non-array `oneOf` or `enum`
- The local IPC connect path sizes the payload first and refuses to connect if it exceeds the cap

**Evidence**

`turn the transcript toggle off, then Send again.`

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

## In Development

### A flag served as false is now distinguished from a flag never set

The tool can now tell a feature switched off remotely from one that was never set at all.

**What**

A new check reports whether a remote feature flag was actively served as off, as opposed to simply having no value and falling back to a local default. The device bridge uses it to decide whether to keep running, logging `stopping: the gate turned off` with reason `gate_off`.

**Details**

- Returns true only when the off value came from the served payload, a local override, or an explicit disable; a value read from a cached file on disk counts as false, and a pure local fallback defers to a separate check.
- Applied to the flag `tengu_violin_wood`, which is read with a fallback of false.

**Evidence**

`isViolinWoodServedOff`

- Flag `tengu_violin_wood`: Off in both readings (read for one account on one subscription tier against v2.1.235; this account: off, anonymous baseline: off, compiled default: on)
- Area: Internals
- Tier: Under the hood
- Useful: 1/5
- Signal: 3/5

### Terminal recordings can be kept in the storage backend instead of loose files

Terminal recordings could live in a storage backend instead of loose files, but that backend is off.

**What**

Asciicast terminal recordings gain a path that stores them under a `recording` key namespace keyed by project, session and timestamp, with the recording list merging stored entries and on-disk `.cast` files. Only taken when the storage backend flag `tengu_hover_rest` is on and a storage handle exists; otherwise recordings stay plain files.

**Details**

- Renaming a stored recording goes through a callback held on the recorder rather than a filesystem rename.
- The old `<sessionId>-<timestamp>.cast` file rename remains the fallback path.

**Evidence**

`namespace: "recording"`

- Flag `tengu_hover_rest`: Off in both readings (read for one account on one subscription tier against v2.1.235; this account: off, anonymous baseline: off, compiled default: on)
- Area: Recordings
- Tier: Not switched on
- Useful: 1/5
- Signal: 3/5
- Present in the build but not switched on

### Plugin version cache gets a staged publish path, behind the storage flag

Plugin version caching could stage and atomically move entries, but only with the off-by-default storage backend.

**What**

Caching a plugin version can now build the entry in a staging directory and move it into place atomically, with named handling for interrupted or concurrent writes. It runs only when the storage backend flag `tengu_hover_rest` is on, which is off by default; otherwise the old copy-in-place behaviour is used.

**Details**

- Explicit cases handled: a stray symlink sitting at the cache path, leftover debris with no payload, another session recorded as using the entry, and an archive write that fails after the copy succeeded (the completed directory is kept rather than deleted).
- Failure paths carry distinct diagnostic labels, such as "plugin cache staged publish could not complete".

**Evidence**

`plugin cache staged tree swapped before publishing`

- Flag `tengu_hover_rest`: Off in both readings (read for one account on one subscription tier against v2.1.235; this account: off, anonymous baseline: off, compiled default: on)
- Area: Plugins
- Tier: Not switched on
- Useful: 1/5
- Signal: 3/5
- Present in the build but not switched on

### Eval setup refuses directories it cannot vouch for

Eval setup refuses directories with unverifiable ownership or symlinks, and cleans up after a failed setup.

**What**

When setting up evals, the command takes the directory from `--eval-dir` or the plugin manifest's `experimental.evals`, refuses to read a manifest in a directory whose ownership it cannot confirm, refuses symlinked or foreign-owned output locations, and removes any directories it created if setup fails.

**Details**

- The bundled plugin-eval reference documents were updated to match: the notes calling `--eval-dir` an upcoming release are gone and the flag is listed among the options.
- The eval command itself remains early-access gated.

**Evidence**

`pass --eval-dir to name the directory`

- Area: Plugins
- Names: `--eval-dir`
- Tier: You'll notice
- Useful: 2/5
- Signal: 2/5

### Eval warns when its directory overlaps the plugin's own component paths

Eval warns when your eval directory overlaps paths the plugin already loads components from.

**What**

Before running, the eval command reads the component paths declared in plugin.json (commands, agents, skills, outputStyles, themes, workflows, monitors, mcpServers, lspServers, hooks), follows symlinks into the real tree, and reports when the eval directory sits inside one, because files written there could be loaded as plugin components.

**Details**

- With `--eval-dir` this is a hard error, unless the run writes nothing into the plugin.
- When the directory came from the manifest or the default, it is a warning that suggests `--output-dir` or running from the plugin root.

**Evidence**

`files written there may be loaded as plugin components`

- Area: Plugins
- Names: `--eval-dir`
- Tier: You'll notice
- Useful: 2/5
- Signal: 2/5

## Internal Changes

### Artifact sandbox sanitizes host-call inputs inside the sandbox

Artifact sandbox inputs are cleaned inside the sandbox, so host checks never read through sandbox-controlled traps.

**What**

Calls out of the artifact sandbox into Claude Code can now run a sanitize step inside the sandbox before the host side sees the value, so host code never reads through sandbox-controlled property traps while validating or checking consent.

**Details**

- Sanitize failures, including length caps and unreadable values, take the wrapper's normal error path.
- Marked in source as a security-hardening change.

**Evidence**

`INTAKE sanitize, VM-side`

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

### Per-session state replaces process-wide globals, and the render root takes explicit dependencies

Session state moved off process-wide globals into per-session holders, so two sessions in one process stay separate.

**What**

Mutable state that used to sit at module level, shared by everything running in one process, now lives in lazily created holders keyed on the session, reached through getters rather than read directly. Two sessions in the same process no longer share it. Alongside that, the top-level UI provider and the message list take their dependencies explicitly instead of pulling them from ambient state or from long prop lists.

**Details**

- The top-level UI provider now receives the session, storage, the message queue, initial state and a `keybindings` flag; transcript export renders with keybindings switched off.
- The message list takes one combined search object plus a pause-animation flag, replacing five separate props.
- State moved per session covers background daemon beacon tracking, raw stdin capture, the sync and retry engine, background job state, in-flight monitor state, the plan file cache and plan slugs, the design tool's plan store, approved plans, the design-consent cache, verified project grants, the background rendezvous socket server, marketplace and checkout state, and the OAuth callback listener, submitter and active-flow maps.
- Functions that previously read one of these globals now take the session or its state as their first argument.

**Evidence**

`keybindings`, `verifiedProjectGrants`

- Area: Session State
- Tier: Under the hood
- Useful: 3/5
- Signal: 3/5

### Stale session claims keep their live entries

A stale session claim now keeps its entries, marked stale, instead of throwing them away.

**What**

When a claim on an adopted session is found stale, its live entries are now returned marked stale with a backfilled write timestamp instead of being discarded. Only a stale claim with no entries at all still returns nothing.

**Details**

- Shell and cron lists come back empty in that case.
- The `tengu_adopt_claim` event gained a `frame_live_stale` field.

**Evidence**

`frame_live_stale`

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

### Design tool and cache state scoped to the session

Design consent, grants, OAuth callbacks and key caches are now per session, which matters for background and multi-session runs.

**What**

Design-tool consent, plan and grant state is now looked up per session and passed through permission checks, the grants fetch, the plan store and the project-grant observer, which takes it as a second argument. The MCP OAuth callback submitter map, artifact auto-react store, timer registries, remote-settings verdict cache and API key cache moved into per-instance holders too. This matters for background and multi-session runs, where these used to be shared.

**Evidence**

`oauthCallbackSubmitters`

- Area: Session State
- Tier: Under the hood
- Useful: 3/5
- Signal: 3/5

### Process-wide caches are now scoped per host

Two sessions running in one process no longer share caches with each other.

**What**

A broad sweep replaced caches that were shared across the whole process with ones looked up per host, so two sessions running inside one process no longer share them. Single-host runs behave exactly as before.

**Details**

- Covers the API key helper cache and its in-flight request, quota fetching, credential ownership, plugin-install listing, subagent token budget accounting, campaign eligibility, the stdin capture buffer, first-paint metrics and several UI stores.
- The subagent token budget became a class with private fields tracking rolled-over context, task budget re-anchoring and cumulative usage.

**Evidence**

`reanchorTaskBudget`

- Area: Session State
- Tier: Under the hood
- Useful: 2/5
- Signal: 3/5

### Claude Design state moved off module globals onto a per-session object

Claude Design approvals and consent now live per session instead of process-wide, and reset cleanly on a dropped connection.

**What**

Approval tokens, approved plans, served catalog hashes, project grants and the consent cache for Claude Design now live on one object carried through each permission and dispatch call instead of process-wide variables, and a single reset clears the cached MCP session when the server returns 404.

**Details**

- The served catalog hash set is capped as a least-recently-used cache.
- The reset also cancels any discovery request already in flight.

**Evidence**

`mintDisplayedUpdates: non-plain array in mint input`

- Area: Claude Design
- Tier: Under the hood
- Useful: 2/5
- Signal: 3/5

### Managed settings track whether a policy helper was armed remotely

Settings can now show "Enterprise managed settings (remote helper)" when a policy helper was armed remotely.

**What**

The settings machinery now distinguishes a policy helper armed by remote settings from a locally configured one, and the settings-source label can read "Enterprise managed settings (remote helper)".

**Details**

- The resolver returns the helper plus a `remoteArmed` flag, defaulting to true unless the source says otherwise, and the reported tier becomes "remote" rather than "helper" in that case.
- The helper store gained fields tracking the remote arming generation, its config and the notice subject; tier changes are announced through a dedicated method rather than an ad-hoc callback.
- `policyHelpers` joins the recognised helper settings alongside `gcpAuthRefresh`, `otelHeadersHelper`, `processWrapper`, `proxyAuthHelper`, `statusLine` and `subagentStatusLine`, and origin is now derived from the settings source.
- The shell-environment fingerprint expands `policyHelpers` into per-OS entries with a hashed value rather than listing the bare key.

**Evidence**

`Enterprise managed settings (remote helper)`, `policyHelpers.`, `policyHelpers`

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

### Remote sessions fetch managed settings once instead of retrying

Remote sessions fetch managed settings once instead of retrying, reusing salvaged settings if the fetch is empty.

**What**

In remote mode the managed-settings pull runs as a single attempt rather than the retrying variant, and can carry previously salvaged settings forward when the fetch returns nothing. Local runs keep the retrying path.

**Evidence**

`singleAttempt: !0`

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

### Measurement-only check for hardlinked files in read permissions

Claude Code now quietly notes when a file you read has more than one name, without changing what's allowed.

**What**

When a file read is auto-allowed, Claude Code now quietly checks whether the target is a hardlink (more than one name pointing at the same file) and reports it. The permission decision is unchanged either way. The flag `tengu_playful_lobster` gates the reporting and falls back to on, so the measurement runs for everyone while no enforcement exists yet.

**Details**

- The check stats the resolved path lazily and tests for a regular file with a link count above one.
- It fires `tengu_playful_lobster_fired` with `step` (`editImpliesRead` or `workingDir`), the permission mode, and a hardcoded `mode: shadow`.
- Firing is deduped per mode and path, with a cap.

**Evidence**

`tengu_playful_lobster_fired`

- Flag `tengu_playful_lobster`: Not enough to say (read for one account on one subscription tier against v2.1.235; this account: no value returned, anonymous baseline: no value returned, compiled default: off)
- Area: Permissions
- Tier: Under the hood
- Useful: 1/5
- Signal: 3/5

### Update-blocked telemetry records whether a comment monitor was carried

Blocked-update reports now note whether an auto-reply comment monitor was carried across the respawn.

**What**

When an update respawns background work, it now reports whether an auto-reply comment monitor came along, so blocked-update data can be split by that case.

**Evidence**

`carried_comment_monitor`

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

### The atis latch is seeded once and only from printable ASCII

The pinned `atis` value is set once per conversation and only accepts printable ASCII.

**What**

The latch that pins the client `atis` value is seeded a single time per conversation and only accepts a value that is printable ASCII, which keeps stray bytes out of it.

**Details**

- Gated on `tengu_kestrel_moor`, a remote flag whose fallback is on; with the gate off, each read goes straight to client data as before.

**Evidence**

`tengu_kestrel_moor`

- Flag `tengu_kestrel_moor`: Not enough to say (read for one account on one subscription tier against v2.1.235; this account: no value returned, anonymous baseline: no value returned, compiled default: off)
- Area: Client Config
- Tier: Under the hood
- Useful: 1/5
- Signal: 3/5

### Cowork categories accepted by the classifier

Six cowork category names are now accepted, so cowork confirmations get attributed instead of dropped as unknown.

**What**

Six cowork entries were added to the allowlist of category names the classifier accepts from model output, so cowork confirmations are attributed rather than discarded as unknown.

**Evidence**

`cowork_scheduled_task_write`

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

### Transcript compaction rewritten against the new session storage layer

Compacting a long session now rewrites transcripts through the storage layer atomically instead of editing the file in place.

**What**

Compacting a long session transcript now goes through the storage backend, replacing records under a precondition and publishing atomically, instead of editing the transcript file in place. Failures are reported with named reasons.

**Details**

- Bail reasons include `snapshot_mid_line` for a partially written tail, `shared_inode` for a hardlinked target, and `source_changed` when another writer wins the race; these arrive on `tengu_transcript_compact_failed`.
- Reads page forward rather than loading the whole transcript.
- When a compaction reclaims little space, the threshold that triggers the next one doubles. Successful runs report byte counts before and after.
- Applies only on the newer storage path; no user-facing setting.

**Evidence**

`tengu_transcript_compact_failed`

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

### Plugin downloads are re-checked while being moved into the version cache

Plugin downloads are re-verified as they move into the cache, and a failed move cleans up after itself.

**What**

Moving a downloaded plugin into the version cache now re-verifies the destination before, during and after the move under an internal strict-cache mode, refuses a path whose current occupant cannot be examined, and deletes the staged copy if the move throws. The permissive rename and copy helpers it replaces were removed.

**Evidence**

`plugin cache version path occupant could not be examined`

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

### Command queue became an instance, and queued commands can be passive

Queued commands can be marked passive, and Esc now lets other handlers run when it didn't cancel anything.

**What**

The queue of pending commands is now an object consumers subscribe to rather than a set of process-wide variables. Queued commands gained a "passive" marker, and the Esc/cancel handler now reports whether it actually consumed the key so other handlers can run when it did not.

**Details**

- Consumers read state through a snapshot accessor and a subscribe call, and trigger a re-check through a method rather than a free function.
- Separate predicates select passive and non-passive queued commands, and drains can request only the passive ones.
- The variant of the queue backed by version 5 storage is created only when the pinned flag `tengu_hover_rest` is on.

**Evidence**

`getCommandQueueSnapshot`

- Flag `tengu_hover_rest`: Off in both readings (read for one account on one subscription tier against v2.1.235; this account: off, anonymous baseline: off, compiled default: on)
- Area: Elsewhere
- Tier: Under the hood
- Useful: 2/5
- Signal: 2/5

### Durable-watch failures log the server's reason and mark relayed subscribes

Failed artifact watch subscriptions now log the server's actual reason, and expected failures stop shouting at you.

**What**

Failed artifact watch subscriptions now log the message the server sent, with expected failures dropped to debug level and only unexpected ones warning.

**Details**

- Subscribe telemetry carries a relay marker when the request went over the relay route.
- Server messages are mapped to close codes, and a redaction pass strips words and URLs before anything is logged or reported.

**Evidence**

`[artifact] durable watch failed: reason=`

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

### MCP OAuth callback listeners are now session-scoped

MCP sign-in callbacks are tracked per session, so a new session can't inherit another's pending listeners.

**What**

The two MCP OAuth flows no longer keep their in-flight callback listeners in process-wide maps; registration and abort go through a per-session object, so a new session cannot inherit another one's pending listeners.

**Evidence**

`oauthCallbackListeners`

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

### Account records keep a full name

Your account record now stores a full name, and a name change alone counts as a changed account.

**What**

Signed-in account data now stores a full name when the server supplies one, and the comparison that detects an account change looks at it, so a name change on its own counts as a changed account.

**Evidence**

`fullName: s.account.full_name || void 0,`

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

### Assorted smaller additions

Slack channel ids become clickable links, `pkg` counts as a known top-level directory, and trial length is exposed.

**What**

Slack channel ids are now turned into `https://slack.com/app_redirect?channel=` links, `pkg` joins the recognised top-level directory names used to classify paths, the account's trial length is exposed to the client, and the two compact-disabling environment variables are consolidated into one check.

**Details**

- `DISABLE_COMPACT` and `DISABLE_AUTO_COMPACT` are now read through a single predicate consulted by both the auto-compact enable check and the nudge that suggests compacting.

**Evidence**

`claudeCodeTrialDurationDays`

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

### Device bridge re-announce telemetry

The device bridge now reports re-announce attempts, drain timing and collisions with an already-running re-announce.

**What**

The device bridge now emits `tengu_device_bridge_reannounce` carrying reason, outcome, in-flight count at start, drain duration and a drain-timeout flag, plus `tengu_device_bridge_reannounce_contended` when a re-announce collides with one already running. The stop event gained a reason field.

**Evidence**

`tengu_device_bridge_reannounce_contended`

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

### Remote session creation records where its permission mode came from

Remote session creation now records whether your permission mode came from a flag, settings, or nothing.

**What**

The success event for creating a remote session now reports the permission mode and whether it came from a command-line flag, from settings, was withheld in either case, or was absent.

**Details**

- The new dimension is `permission_mode_source`, with values flag, flag_withheld, settings, settings_withheld and none.

**Evidence**

`permission_mode_source`

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

### More file-scope variables replaced by accessor-backed containers

More cached state moved behind accessors so it can be reset per session or account.

**What**

Promo campaign parsing, tmux colour application, a classifier warning latch, design MCP session and initialize caches, the passes eligibility request, the plan file cache and the plugin approved-plan map now read their state through getters, and the wake router and monitor socket task take their dependencies as arguments. Nothing changes for a user; these caches can now be reset per session or account.

**Evidence**

`e.initializeInFlight = n;`

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

### One shared path for resuming interrupted tool calls

Resuming an interrupted permission request and a hook-deferred tool now follow the same single code path.

**What**

Resuming an orphaned permission request and resuming a tool a hook deferred were two inlined blocks that each built their own early result. Both now go through a single routine, so the availability check and result shaping live in one place.

**Evidence**

`tool_deferred_unavailable`

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

### Version and build stamp

Version stamped to 2.1.235, shown in update messages, doctor output and the SDK version reply.

**What**

Build identifiers moved to 2.1.235, built 2026-08-18T01:27:41Z, from git sha ba01fa45e3d1d888c1c23caed775ddd06192448b. These appear in update messages, doctor output, the auto-updater and the SDK binary-version reply.

**Evidence**

`BUILD_TIME: "2026-08-18T01:27:41Z"`

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

### Undefined margins and paddings are passed through instead of becoming zero

Undefined spacing values now unset rather than snapping to zero, so some layouts may shift slightly.

**What**

The layout engine no longer coerces an undefined margin or padding to 0, so passing undefined now unsets the value rather than pinning it. Spacing may shift slightly in components that compute those values conditionally.

**Evidence**

`setMarginAuto`

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

### Loading and timing state moved into a subscribable store class

Loading and turn-timing state moved into a subscribable store, which is also how timing reaches the transcript.

**What**

Query loading and timing state now lives in a dedicated class with subscribers rather than React state plus refs, which is also where turn timing reaches the transcript.

**Details**

- Holds `isQueryActive`, `isExternalLoading`, `isLoading`, `submitCount`, `lastQueryCompletionTime` and the abort controller.
- Turn start and paused time are mirrored into the transcript, and resetting the token count now writes through to transcript progress.

**Evidence**

`lastQueryCompletionTime`

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

### Policy tier changes emitted from one named method

Policy tier change events now fire from a single named place instead of an inline timer closure.

**What**

The inline closure in the settings refresh timer that emitted a tier change became a named method, so the policy-settings event now fires from a single place.

**Evidence**

`announceTierChange`

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

### Truncation returns the two ends separately

Text truncation now returns both ends separately, and long lines compute highlight positions once instead of repeatedly.

**What**

The text truncation helper now hands back the leading and trailing pieces for callers to join, and the renderer routes truncate-start, truncate-middle and middle modes through it. Measured lines precompute their displayed text and offset so highlight positions are worked out once per line instead of per row.

**Evidence**

`displayStartOffset`

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

### Chat loop loading and abort state consolidated into one turn object

The chat loop's loading flags, abort controller and completion timestamp are now one turn object.

**What**

The scattered loading flags, abort controller and query-completion timestamp in the main chat loop are now members of a single turn handle passed around instead of separate props.

**Details**

- Replaced: `queryGuard`, `setIsLoading`, `setAbortController`, `abortController` and the completion-timestamp state.
- The turn handle exposes `setAbortController`, `setExternalLoading`, `isExternalLoading`, `abortController`, `markQueryComplete`, `resetTiming` and `guard`; the standalone `setAbortController` prop was removed from the REPL context.
- Input handlers, background-session and background-query hooks, and the interrupt path all take the turn object.
- The pending message queue moved onto the session as `getCommandQueue`, `clearCommandQueue`, `recheckCommandQueue` and `enqueuePendingNotification`.

**Evidence**

`markQueryComplete`, `setAbortController`

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

### Symlink resolution rewritten to share one traversal for sync and async

Symlink path resolution now uses one shared routine, so blocking and non-blocking callers can't drift apart.

**What**

The code that walks a path resolving symlinks was rebuilt so a single traversal serves both the blocking and non-blocking callers, removing two copies that could drift apart.

**Details**

- The traversal yields filesystem requests that either a sync or an async driver fulfils.
- A new unreadable-ancestry mode treats any error other than "does not exist" or "not a directory" as unresolvable, instead of ignoring it.

**Evidence**

`unreadableAncestry`

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