# Claude Code v2.1.225

> Claude Code v2.1.225, released 7 Aug 2026 (2026-08-07). 155 entries read out of the shipped bundle. Unofficial, and not affiliated with Anthropic.

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

A device bridge that registers this machine with claude.ai over WebSocket, exposing an MCP server for remote tools, is present but stays off unless the remote gate turns on, and both cloud sessions and completed remote auth setup already try to bring it up. The bundled "verify" skill can be invoked by Claude itself behind a gate that falls back to false in this build, and the nudge suggesting it now returns an empty string unless the internal circuit check passes. A prompt block that would make Claude name each check skill as RAN or NOT RUN immediately before `git commit` is wired up with telemetry but sits behind the same check, and `/review` can now chain into `/verify` after reporting findings. Also inert for now: a spinner that narrates what Claude is drafting in a workshop design page, gated on an environment variable, and Remote Control auto-revive after re-login. These are 12 of 25 entries in this group.

The Artifact tool's `comments` action takes a `thread_id` to read one thread and a `cursor` to resume a listing cut short by the size cap, echoing both back so the model knows its view. Routines gain a `create_webhook_trigger` action, so a routine can fire on an external event such as a GitHub event instead of only on a schedule. Remote Control now watches a session stuck in the "auth" failure state and re-arms once valid credentials reappear, and artifact publishes retry 429 and 503 responses with a live progress line. A 401 no longer swaps in a credential from disk when you supplied `CLAUDE_CODE_OAUTH_TOKEN` yourself, the SDK accepts `resolvePermissionModeInCli` to let the CLI resolve permission mode from settings, and the device bridge checks its dial URL against a host allowlist. That is 12 of 79 entries here.

Two index bugs in the input-stream loop are fixed, so messages after a reset or a cancelled request are no longer silently dropped, and a control message with an unrecognized type completes its lifecycle instead of leaving callers waiting forever. A settings `defaultMode` of `bypassPermissions` is now honoured in a VS Code-owned session when the matching consent setting is on, and `CLAUDE_CODE_ENTRYPOINT` is stripped from derived environments when it holds an IDE or desktop value so spawned commands do not misreport their origin. Artifact share status no longer trusts a server-reported mode when no asset token comes back, which stops a public read from looking like owner access. Machines reachable only over the bridge no longer show "No reachable agents.", importing a Gemini command containing a plain exclamation mark no longer fails validation, and compaction payloads are trimmed when preserved event ids exceed the cap.

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

### Device bridge registers this machine as a remote-tools device

Your machine can register as a named remote device with a tool that reports its environment.

**What**

A new subsystem connects the CLI to a claude.ai bridge over WebSocket as a named device and exposes an MCP server for it. It is off in this build unless the remote gate turns on.

**Details**

- MCP server name `claude-code-device`, with one tool `get_device_info`, described as "Returns runtime environment details for this device (platform, architecture, Claude Code version, and device name). Call this to confirm the device connection is live."
- handles handshake, heartbeats (`{"type":"hb"}`), reconnect backoff, slot contention and a 33554432-byte inbound frame cap
- the default enable check awaits the remote gate `tengu_violin_wood` and falls back to false if it throws
- additionally requires non-first-party egress to be allowed, provider `firstParty`, and the `allow_remote_sessions` policy
- default endpoints are `wss://bridge.claudeusercontent.com` and `wss://bridge-staging.claudeusercontent.com`, overridden by `CLAUDE_REMOTE_TOOLS_BRIDGE_URL`

**Evidence**

`[deviceBridge] skipped: non-essential egress disabled, non-first-party provider, or remote sessions policy-denied`

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

### /review can now chain into /verify

/review can hand off to /verify after reporting findings, but the chaining is switched off here.

**What**

After reporting findings, `/review` can tell Claude to run `/verify`, a follow-up section that was present but inert in 2.1.224.

**Details**

- the helper previously returned an empty string unconditionally
- now emitted when the `tengu_opal_circuit` gate is on, fallback false
- also requires the "verify" skill to be installed for the session; otherwise nothing is emitted
- the section text includes "this review checks that the diff reads right"

**Evidence**

`## After the review`

- Area: Slash Commands
- Names: `/review`, `/verify`
- Tier: Not switched on
- Useful: 3/5
- Signal: 4/5
- Present in the build but not switched on

### Auto-mode nudge now switches the running session, or explains why it can't

Saying yes to the auto-mode prompt would switch your current session too, not just future ones.

**What**

Accepting the "Make auto mode your default permission mode?" dialog applies auto mode to the session in progress rather than only writing the setting for next time.

**Details**

- on accept, the live tool-permission context is switched to auto with source `auto_default_nudge`
- on success the user setting is written and an in-line confirmation is shown
- if the auto gate is unavailable, a warning notification keyed `auto-default-nudge-unavailable` is posted and the nudge is not marked as seen
- the resolution event now records `outcome` alongside `choice`
- the nudge itself is gated behind `tengu_maple_pier`, fallback false

**Evidence**

`auto-default-nudge-unavailable`

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

### Bundled "verify" skill can become model-invocable behind a gate

The bundled verify skill could be triggered by Claude itself, but for now only you can start it.

**What**

The verify skill can be invoked by Claude itself when a gate is on; in this build the gate falls back to false, so it stays user-invocable only.

**Details**

- `disableModelInvocation` changed from a constant true to a function returning the negation of a gate read.
- Gated on `tengu_opal_circuit`, fallback false when remote config gives no value.
- The gate result is latched in new app state `verifySkillRolloutGateLatch` so it is read once per session.

**Evidence**

`verifySkillRolloutGateLatch`

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

### PR-prep skill reminder before git commit

Before committing, Claude can be made to state which verify, simplify and code-review skills it ran.

**What**

A prompt block tells Claude, immediately before running `git commit`, to state by literal name whether each of the check skills RAN or NOT RUN this session, with carve-outs for trivial commits and test-only diffs. It renders only when the circuit check passes, and each firing is recorded with telemetry carrying `has_verify`, `has_code_review` and a hashed path.

**Details**

- Gated on `tengu_opal_circuit`, a gate lookup with fallback `!1`, so it is off by default in this build.
- A cached decision or a remote value can override the fallback and turn it on.
- The check skills named are verify, simplify and code-review.

**Evidence**

`tengu_pr_prep_suggestion_rendered`

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

### Live "authoring progress" spinner text while Claude writes a workshop page

The spinner can narrate what Claude is drafting on a workshop page, like "drawing figure 3".

**What**

The spinner can narrate what Claude is currently drafting in a workshop design page, driven by the partial tool input as it streams in. It is gated on an environment variable and on workshop mode being active.

**Details**

- A new module (exporting isWorkshopProgressEnabled, deriveAuthoringProgress, onInputJsonDelta) hooks tool-use start, input-JSON deltas and content_block_stop for Write and Edit on `.workshop.html` files
- Messages include "authoring the design page", "wiring up the decisions", "drawing figure N" and "drawing decision N", the latter including the decision id
- Output is throttled and truncated to the terminal width, and cleared when the tool use ends
- The stream handler only calls it when the caller passes `authoringProgressSurface: !0`, which the main REPL does
- Turned off when `CLAUDE_WORKSHOP_PROGRESS` is set to false, and it also requires an internally registered workshop-active predicate to return true
- Hook failures are swallowed and logged as "workshop authoring-progress hook failed"

**Evidence**

`authoring the design page`, `wiring up the decisions`, `workshop authoring-progress hook failed`

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

### Artifact comments can be filtered to one thread or resumed with a cursor

You can read one artifact comment thread by id, or resume a cut-off listing with a cursor.

**What**

The Artifact tool's `comments` action takes a `thread_id` to read a single thread and a `cursor` to continue a listing that was cut off by the size cap. Results echo back `thread_filter` and `cursor` so the model knows which view it is looking at.

**Details**

- `thread_id` and `cursor` are mutually exclusive and both must be lowercase UUIDs, each with its own validation error and error code
- when a requested thread is not in the result, the model is told to re-run `comments` without `thread_id` for the full list
- truncated listings carry a hint to re-run with `cursor`, and resumed listings carry a "Resumed from the cursor:" note; threads are listed newest first
- the tool-use line renders counts such as "read 1 comment thread (filtered)"
- available to anyone who can use the Artifact tool; neither field existed in 2.1.224

**Evidence**

`` `thread_id` (read one thread) and `cursor` (continue the list) cannot be combined — pass one. ``, `thread_filter`

- Area: Artifacts
- Names: `thread_id`, `cursor`, `comments`
- Tier: Use it now
- Useful: 4/5
- Signal: 0/5

### Auto-default nudge actually flips the mode, and reports the outcome

Saying yes to the auto-mode prompt now actually saves auto as your default permission mode.

**What**

Accepting the prompt to make auto mode your default permission mode now performs the switch rather than only recording the answer.

**Details**

- Writes `permissions: { defaultMode: "auto" }` to userSettings and prints an `[auto on]`-style confirmation line
- If the gate refuses the switch, shows the auto-mode-unavailable warning instead
- `tengu_auto_default_nudge_resolved` gained an `outcome` field alongside the existing `choice` and `current_mode`

**Evidence**

`tengu_auto_default_nudge_resolved`

- Area: Permissions
- Names: `permissions.defaultMode`, `auto`
- Tier: Use it now
- Useful: 4/5
- Signal: 0/5

### OAuth 401 recovery no longer silently replaces a user-supplied token

Your explicitly set OAuth token is no longer silently swapped for a stored credential after a 401.

**What**

A 401 no longer causes Claude Code to swap in a credential from disk when you explicitly supplied `CLAUDE_CODE_OAUTH_TOKEN`.

**Details**

- When the token came from the user's environment, and this is not a remote child session or a unix-socket setup, an explicit error is logged and the supplied token is kept
- The disk-recovery path now additionally requires the stored credential to be unexpired
- The whole env/disk recovery block is skipped when credentials are host-managed via `CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST`
- The recovery failure reason gained a third value for the skipped-user-env-token case, alongside background-worker and interactive no-refresh-token

**Evidence**

`OAuth 401: keeping the user-supplied CLAUDE_CODE_OAUTH_TOKEN instead of adopting the stored credential.`, `oauth_401_skipped_user_env_token`

- Area: Auth
- Names: `CLAUDE_CODE_OAUTH_TOKEN`, `CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST`
- Tier: Use it now
- Useful: 4/5
- Signal: 0/5

### Routine trigger tool can create webhook triggers

The remote trigger tool can create webhook triggers so routines fire on outside events.

**What**

The claude.ai remote-trigger tool's action enum adds `create_webhook_trigger`, which POSTs to /v1/code/webhook-triggers so a routine can be fired by an external event source.

**Details**

- The body must name the event source, scope, events, filter and the `routine_trigger_id` to fire.
- On success the tool appends a summary line linking the routine it fires; the link is assembled at runtime from the CLAUDE_AI_ORIGIN and the routine trigger id.
- Telemetry `trigger_id` is populated from the request's `routine_trigger_id` for this action.
- The tool description was rewritten to document the new action.

**Evidence**

`create_webhook_trigger requires body`

- Area: Background Tasks
- Names: `create_webhook_trigger`, `CLAUDE_AI_ORIGIN`
- Tier: Use it now
- Useful: 4/5
- Signal: 0/5

### The bypassPermissions consent flag reaches startup mode resolution

Setting defaultMode to bypassPermissions now works in VS Code when you have enabled the consent setting.

**What**

A settings `defaultMode: "bypassPermissions"` is now honoured in a VS Code-owned session when the corresponding VS Code consent setting is enabled.

**Details**

- `allowDangerouslySkipPermissions` is now a parameter of the startup permission-mode resolver and is forwarded into the resolver's cli block
- previously the resolver was called without it, so the mode was rejected with 'Permission mode bypassPermissions from settings was ignored — enable the "Claude Code: Allow Dangerously Skip Permissions" setting in VS Code to consent to it' even when the setting was on

**Evidence**

`allowDangerouslySkipPermissions`

- Area: Permissions
- Names: `defaultMode`, `bypassPermissions`, `Claude Code: Allow Dangerously Skip Permissions`
- Tier: Use it now
- Useful: 4/5
- Signal: 0/5

### The routines/triggers tool can create webhook triggers

Claude can now attach a webhook trigger, like a GitHub event, to a routine instead of a schedule.

**What**

A `create_webhook_trigger` action lets Claude attach an event source, such as a GitHub event, to an existing routine so it fires on that event rather than on a schedule.

**Details**

- Posts to `/v1/code/webhook-triggers` and requires a body; without one it errors with 'create_webhook_trigger requires body'
- Documented in the tool prompt alongside the other routine actions
- The response appends the routine's claude.ai link instead of a next run time

**Evidence**

`create_webhook_trigger: POST /v1/code/webhook-triggers (requires body)`

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

### Artifact publishes retry 429 and 503 with a live progress line

Artifact publishing now retries when the service is busy, showing a live attempt counter instead of failing.

**What**

Publishing an artifact through `/api/frame/deploy/direct` now retries transient failures instead of failing outright, and shows the retry state while it happens.

**Details**

- A 429 is retried once after the `retry-after` delay, capped; 503s are retried with backoff up to a maximum attempt count.
- Retries honor the abort signal, logging "[artifact] /deploy/direct retry cancelled by user".
- A new progress event type renders "Publish service temporarily unavailable" or "Publish service busy" followed by " — retrying (attempt N of M)…".
- Telemetry `deploy_503_retried` / `deploy_429_retried` carries `deploy_attempts` and `deploy_final_status`.
- `onRetry` and `signal` are threaded through the publish call chain. None of this existed in 2.1.224.

**Evidence**

`artifact_publish_retry`

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

### Bundled skill-backed slash commands forward more capabilities

Slash commands from bundled skills now support arguments, completions, background runs and effort settings.

**What**

Slash commands generated from bundled plugin skills now carry through a much larger set of command features, so they behave more like hand-written commands.

**Details**

- Newly passed through: `argsMayContainSlashCommands`, `getContext`, `background`, the resolved `skillRoot`, `getEffort`, `getDefaultEffort`, `onUserTypedArgs` and `getArgumentCompletions`.
- A skill-supplied `progressMessage` is forwarded, defaulting to "running".
- getPromptForCommand now comes from a cached wireSkillFilesExtraction step.

**Evidence**

`argsMayContainSlashCommands`

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

### Held-message approval dialog now defaults to Deny

The prompt for messages from another Claude session now defaults to Deny, so a stray Enter drops it.

**What**

The prompt shown when another Claude session sends a message now pre-selects the safe option, so an accidental Enter drops the message instead of delivering it.

**Details**

- "Deny — drop it and tell the sender it was declined" is listed first and "Deliver this message to Claude" second.
- The select is given defaultFocusValue "deny" plus hideIndexes; previously delivery was first with no default focus.
- The payload builder no longer emits its `truncated` field.

**Evidence**

`Deny — drop it and tell the sender it was declined`, `Deliver this message to Claude`

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

### Remote Control auto-revive after re-login

After you log back in, Remote Control can turn itself back on without you re-running the command.

**What**

A polling watcher notices a fresh OAuth credential for the same account and re-enables Remote Control without you having to run /remote-control again.

**Details**

- Polls on a 5 minute interval.
- Disarms itself if the new credential belongs to a different account.
- Reattach and archive paths re-verify the signed-in account before acting.
- Gated on `tengu_bridge_auth_revive` with fallback `!0`; with the kill switch off the watcher still polls but performs no unattended revive.

**Evidence**

`tengu_bridge_repl_auth_revive`

- Flag `tengu_bridge_auth_revive`: Off by default, switched on for this account (read for one account on one subscription tier against v2.1.225; this account: on, anonymous baseline: on, compiled default: off) These values were read against a different version of Claude Code, so treat them as the nearest reading available instead of one taken on this release.
- Area: Remote Control
- Names: `/remote-control`, `tengu_bridge_auth_revive`
- Tier: You'll notice
- Useful: 4/5
- Signal: 0/5

### Remote Control re-arms itself after a re-login

Remote Control now turns itself back on after you log in again, instead of staying dead.

**What**

A watcher now runs while a REPL bridge session sits in the "auth" failure state and turns Remote Control back on once valid credentials reappear, instead of leaving the session dead until manual intervention.

**Details**

- Re-checks credentials every 300000 ms; on finding a fresh credential for the same account it clears `replBridgeError`, flips `replBridgeEnabled` back on and fires telemetry `tengu_bridge_repl_auth_revive`.
- Refuses to revive across accounts, logging "credential belongs to a different account — disarming".
- In non-interactive sessions a brake applies unless an in-process `/login` bumps the new `authChangeGeneration` counter.
- Close code 4094 (worker credential expired) is treated as auth-recoverable only when this gate is on.
- Gated on `tengu_bridge_auth_revive`, fallback true absent a server value; with the gate off the code still logs "kill switch off — still polling, no unattended revive".
- `authChangeGeneration` and the `"\x00baseline-pending"` sentinel are new in this build.

**Evidence**

`[bridge:repl] Auth-revive watcher: fresh same-account credential — re-enabling Remote Control`

- Flag `tengu_bridge_auth_revive`: Off by default, switched on for this account (read for one account on one subscription tier against v2.1.225; this account: on, anonymous baseline: on, compiled default: off) These values were read against a different version of Claude Code, so treat them as the nearest reading available instead of one taken on this release.
- Area: Remote Control
- Names: `/login`
- Tier: You'll notice
- Useful: 4/5
- Signal: 0/5

### Streaming read index no longer skips messages

Messages sent after a conversation reset or a cancelled request are no longer silently dropped.

**What**

Two index bugs in the input-stream loop are fixed, so messages after a reset or a cancelled request are not silently dropped.

**Details**

- The read index is reset whenever a `conversation_reset` event appears in the pending batch, instead of only when a filtered condition held.
- When a cancelled request's placeholder text message is spliced out, the read index is decremented if it sat past the removal point.

**Evidence**

`conversation_reset`

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

### Artifact comment threads are paginated with a cursor

Truncated artifact comment listings now hand you a cursor to fetch the remaining threads.

**What**

When a comments listing is truncated by the size cap, the result now tells the caller how to fetch the rest rather than only pointing at the web page.

**Details**

- A cursor is computed from the last listed thread.
- The truncation message instructs the caller to re-run the "comments" action with that cursor.

**Usage**

`comments with cursor "<cursor from the previous truncated result>"` **Evidence** `re-run action "comments" with cursor "`

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

### Artifact comments listing gained pagination and single-thread lookup

You can now fetch a single artifact comment thread or page through long listings with a cursor.

**What**

The artifact tool's "comments" action now takes `thread_id` and `cursor` on top of `action` and `url`, so a model can fetch one thread or page through a long listing.

**Details**

- When a listing is truncated, the omitted-threads line names the cursor to re-run with.
- The parameter help describes `cursor` as continuing a listing that ended with a "more threads not listed" line.

**Evidence**

`comments only: continue a listing that ended with a "more threads not listed" line`

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

### Child processes no longer inherit an IDE entrypoint identity

Commands you spawn no longer inherit an IDE entrypoint or remote session id from your editor session.

**What**

`CLAUDE_CODE_ENTRYPOINT` is stripped from derived environments when it holds an IDE or desktop value, so spawned commands do not misreport where they came from.

**Details**

- The stripping helper deletes the variable for IDE/desktop values such as `claude-desktop-3p`.
- It is now called from the env builders for the spawned-command environment and for the sandbox/scrubbed environment; previously only one spawn path applied it.
- `CLAUDE_CODE_REMOTE_SESSION_ID` was added to the set of remote-session variables stripped from derived environments.

**Evidence**

`claude-desktop-3p`

- Area: Environment
- Names: `CLAUDE_CODE_ENTRYPOINT`, `CLAUDE_CODE_REMOTE_SESSION_ID`
- Tier: Use it now
- Useful: 3/5
- Signal: 0/5

### Device bridge websocket URLs are checked against a host allowlist

The device bridge now only connects to approved hosts, with local overrides limited to localhost.

**What**

The device-registration bridge validates its dial URL before connecting, so it cannot be pointed at an arbitrary websocket host.

**Details**

- new `isAllowedUrl` check requires `wss:`, no username or password in the URL, and a hostname in {bridge.claudeusercontent.com, bridge-staging.claudeusercontent.com}
- an operator-overridden base URL is allowed only through a separate same-origin check, which permits `ws:` only for localhost, 127.0.0.1 and [::1]
- no such check existed in 2.1.224
- the bridge only runs when non-essential egress is allowed, the provider is first-party, and the `allow_remote_sessions` policy permits it; the base URL is overridable via `CLAUDE_REMOTE_TOOLS_BRIDGE_URL`

**Evidence**

`bridge-staging.claudeusercontent.com`

- Area: Sessions
- Names: `CLAUDE_REMOTE_TOOLS_BRIDGE_URL`
- Tier: Use it now
- Useful: 3/5
- Signal: 0/5

### Remote Control disconnect notices point at /login for auth failures

If Remote Control drops due to auth, the notice now tells you to run /login.

**What**

A disconnect caused by authentication now tells you to re-login instead of offering the generic reconnect hint.

**Details**

- The disconnect banner takes a cause argument; when the cause is "auth" it appends "run /login to restore Remote Control", unless the message already mentions /login.
- Other causes keep the existing "run /remote-control to reconnect" hint.
- The surrounding REPL-bridge hook was refactored so all teardown paths go through one reset helper taking `archiveAbandoned`, replacing several duplicated ref-clearing blocks.

**Evidence**

` — run /login to restore Remote Control`

- Area: Remote Control
- Names: `/login`, `/remote-control`
- Tier: Use it now
- Useful: 3/5
- Signal: 0/5

### SDK can defer permission mode resolution to the CLI

SDK callers can let the CLI pick the permission mode from settings instead of forcing "default".

**What**

The SDK query builder accepts `resolvePermissionModeInCli`, which leaves an unspecified permission mode undefined so the CLI resolves it from settings rather than being pinned at the SDK layer.

**Details**

- Previously an unspecified `permissionMode` was always sent as "default".
- With the option set and no mode given, the field is omitted entirely and settings decide.
- Without the option, the old "default" fallback still applies, so existing callers are unaffected.
- Opt-in per call site.

**Evidence**

`resolvePermissionModeInCli`

- Area: SDK
- Names: `resolvePermissionModeInCli`
- Tier: Use it now
- Useful: 3/5
- Signal: 0/5

### SDK spawn failures explain musl/glibc loader mismatches

SDK spawn errors now explain musl/glibc binary mismatches and tell you which option overrides the path.

**What**

When the configured Claude Code executable exists but cannot be spawned, the SDK error now describes the dynamic-loader mismatch case and points at the option you would use to fix it.

**Details**

- covers the case of a musl-linked binary on a glibc host, where `/lib/ld-musl-*` is absent
- names `options.pathToClaudeCodeExecutable` as the override

**Evidence**

`e.g. spawning a musl-linked binary on a glibc Linux host fails because the musl dynamic loader (/lib/ld-musl-*) is missing`

- Area: SDK
- Names: `options.pathToClaudeCodeExecutable`
- Tier: Use it now
- Useful: 3/5
- Signal: 0/5

### CLAUDE_REMOTE_TOOLS_BRIDGE_URL added to the remote-settings env filter

Remotely managed settings can no longer redirect your bridge connection by setting that URL variable.

**What**

The bridge URL override variable is now filtered out of the `env` block of remotely fetched managed settings.

**Details**

- the filter list otherwise holds security-sensitive names such as HOME, NODE_OPTIONS, CLAUDE_CODE_OAUTH_REFRESH_TOKEN and the other CLAUDE_BRIDGE_* URLs
- the same variable still overrides the device bridge base URL when set locally

**Evidence**

`CLAUDE_REMOTE_TOOLS_BRIDGE_URL`

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

### Gateway spend precheck returns headers for allowed requests too

Successful gateway calls now return rate-limit headers, so you can see overage state before being blocked.

**What**

The admin gateway spend precheck now returns `{ blocked, headers }`, so rate-limit headers reach the client on successful calls and not just on a blocking 429.

**Details**

- Previously only the blocking 429 response carried the precheck headers.
- When the request is allowed and the upstream response is ok, the precheck headers are copied onto that response.
- This is how clients see overage state, for example `anthropic-ratelimit-unified-overage-disabled-reason`, on a call that went through.

**Evidence**

`anthropic-ratelimit-unified-overage-disabled-reason`

- Area: Admin Gateway
- Names: `anthropic-ratelimit-unified-overage-disabled-reason`
- Tier: You'll notice
- Useful: 3/5
- Signal: 0/5

### Untrusted-device recovery can be turned off by a flag; unarchive gets its own 403 handling

Untrusted-device failures still recover automatically, and expired trusted-device sessions now tell you to run /login.

**What**

Whether an untrusted-device failure counts as auth-recoverable is now controlled by a kill switch, and the 403 classification used by archive and unarchive was split.

**Details**

- the recoverability check changed from `reason === "untrusted_device" || reason === "session_stale_relogin"` to treating `untrusted_device` as recoverable only when the proactive-enrollment kill switch is off
- gate is `tengu_sessions_elevated_auth_disable_proactive_enrollment`, fallback false, so absent a remote value `untrusted_device` stays recoverable
- archive and unarchive pass different 403 classification modes; only unarchive maps `session_stale_relogin`
- that reason surfaces as "session expired for trusted-device check — run /login to re-authenticate"

**Evidence**

`tengu_sessions_elevated_auth_disable_proactive_enrollment`

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

### Self-hosted runner checks its base directory before starting work

The self-hosted runner fails fast if its base directory is unwritable or the mount hangs.

**What**

Startup creates and access-checks the base directory under a timeout so an unwritable or hung mount fails immediately rather than partway through a run.

**Details**

- a timeout exits the process with a hint to check mount health
- other failures throw with instructions to pass `--base-dir` or set `SELF_HOSTED_RUNNER_BASE_DIR`

**Evidence**

`SELF_HOSTED_RUNNER_BASE_DIR`

- Area: Elsewhere
- Names: `--base-dir`, `SELF_HOSTED_RUNNER_BASE_DIR`
- Tier: Use it now
- Useful: 2/5
- Signal: 0/5

### Self-hosted runner startup timeout resolution extracted

You can set a startup timeout for self-hosted runners, otherwise the built-in default applies.

**What**

The env-var-or-default logic for the self-hosted runner startup timeout moved into its own function.

**Details**

- Gated on `SELF_HOSTED_RUNNER_STARTUP_TIMEOUT_MS`.
- Unset or empty still falls back to the built-in default.

**Evidence**

`SELF_HOSTED_RUNNER_STARTUP_TIMEOUT_MS`

- Area: Elsewhere
- Names: `SELF_HOSTED_RUNNER_STARTUP_TIMEOUT_MS`
- Tier: Use it now
- Useful: 2/5
- Signal: 0/5

## New Features

### Headless sessions expire held peer messages instead of holding forever

Headless runs no longer hang forever on a peer message that needs an approval nobody can give.

**What**

In non-interactive mode a held peer message whose hold cause is a mode mismatch now gets a deadline, so a headless run no longer blocks indefinitely waiting for an approval that can never arrive.

**Details**

- The expiry uses an unref'd timer, so it does not keep the process alive on its own.
- On expiry the message is dropped and a cancelled receipt is issued to the sender.
- The drop is logged with the reason that no approval surface exists.

**Evidence**

`[cross-session-inbound] headless: held peer message expired (no approval surface) — dropped with an expired receipt`

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

### Spend-cap responses carry unified rate-limit headers

Spend-limit blocks now tell you which cap period you hit and when it resets.

**What**

The spend precheck now picks the most binding cap period that has spend against it and returns `anthropic-ratelimit-unified-*` headers describing that period.

**Details**

- headers cover status, reset, overage utilization, surpassed threshold, retry-after and the period itself
- the surpassed-threshold header trips at utilization 0.95 and 0.75
- blocked responses name the offending period and its reset time rather than a generic message
- failures reading the spend store now surface a `fetch_error` disabled reason instead of a bare error string

**Evidence**

`anthropic-ratelimit-unified-overage-surpassed-threshold`

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

### Artifact tool now tells Claude that finished deliverables should be published

Claude is now pushed to publish finished reports or plans and hand you the link.

**What**

The Artifact tool description gained a paragraph arguing that a report, plan or reference meant for an audience is not actually delivered until it is published and the link handed over.

**Details**

- Applies to work products aimed at a reader, not to incidental output
- Frames publishing plus handing over the link as part of finishing the task, not an extra step
- Description text only; no change to the tool's parameters

**Evidence**

`is not fully delivered while it lives only in terminal scrollback or a local file`

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

### Remote Control bridge: token-refresh exhaustion now surfaces a login prompt

When remote control can't refresh your token, it now tells you to run /login instead of stalling.

**What**

When the bridge runs out of token-refresh retries, the session is marked failed with an instruction to run /login instead of stalling silently.

**Details**

- The refresher gained an `onExhausted` callback fired after the configured number of consecutive failures, logged at error level
- Existing "Refresh chain exhausted for sessionId=" logging and `bridge_token_refresh_exhausted` telemetry are unchanged
- The bridge supplies the callback and moves the session into a failed/auth state carrying the user-facing reason
- Token-refresh failures inside `getAccessToken` are now caught and logged rather than thrown
- A new `cred_4094_recovery` cause joins the recovery connect-timeout classification

**Evidence**

`OAuth token unavailable \u2014 run /login to restore Remote Control`, `run /login to restore Remote Control`

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

### New bash-parse rule flags two statements sharing a line

Commands cramming two statements onto one line are now flagged as too complex to auto-approve.

**What**

Command analysis now treats a command as too complex when one statement directly follows another on the same line, since bash reads that as a single command.

**Details**

- Two node-type sets back the check: a statement-like set including `redirected_statement` and `ERROR`, and a smaller set of `negated_command`, `if_statement`, `while_statement` and `for_statement`
- A new walk over the parse tree applies the rule and classifies the command accordingly

**Evidence**

`statement directly follows another statement on the same line`

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

### Auto mode denies safeguard-refused classifier calls without recording a verdict

A safety refusal during auto mode denies the action but no longer counts against your denial limit.

**What**

When the auto-mode classifier call returns stop_reason "refusal", the action is denied but the denial is no longer treated as a normal block or counted against you.

**Details**

- The decision is marked `noVerdict: !0` and returns a message explaining the refusal reacts to earlier conversation content rather than the action itself: "Retrying it will hit the same refusal, so don't rewrite or rework the action to get around this".
- The denial is exempt from the denial counter.
- In headless mode it instead throws "Agent aborted: auto mode classifier request refused by the safety safeguard in headless mode".
- Downstream, decisions carrying `noVerdict` suppress the auto-mode meta message and the config-reject bookkeeping path.
- Telemetry gains `failureKind: "policy_refusal"`, `stopReason`, `stage1VerdictStands` and `refusedBySafeguard`.

**Evidence**

`Auto mode classifier request refused by the safety safeguard, denying (exempt from the denial counter)`

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

### Remote Control bridge sessions get [ref] handles in listings

Sessions reached over Remote Control now show a reference handle so you can message them by name.

**What**

Sessions reached over Remote Control now resolve a reference handle the way local and cloud sessions do, so they can be addressed by name, and the listing warns when it is showing an incomplete set.

**Details**

- Bridge session refs are resolved from the `bridge-session\x00<id>` key.
- When the bridge listing is incomplete, the output appends a note that those rows carry no [ref] and are not messageable by name until a later listing completes.
- Selecting a bridge session maps to a cloud-session target tagged `via: "remote-control"`.

**Evidence**

`list incomplete just now`

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

### Bridge recovery for expired worker credentials on close code 4094

When your remote worker credential expires, the bridge now re-mints it and refreshes login instead of dying.

**What**

The remote bridge can now recover from a worker credential expiry instead of just reporting the close code, re-minting the credential and forcing an OAuth refresh.

**Details**

- Adds a recovery-table entry for close code 4094 alongside the existing 401, 4091 and 4093 entries.
- The 4094 entry re-mints the worker credential and sets `needsOAuthRefresh: !0`.
- Epoch-mismatch conditions `token_expired` and `auth_exhausted` map to 4094.
- Gated on the typed close-code mapping, passed as `causeTypedCloseCodes: !0` by both bridge callers, so it is active for the remote bridge.
- The plain "code 4094" close-reason text existed in 2.1.224; only the recovery handling is new.

**Evidence**

`worker credential expired \u2014 re-minting`

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

### Artifact publish now retries 503s with a visible progress line

Publishing an artifact now retries when the service is busy and tells you it is waiting.

**What**

Publish and deploy calls go through a shared retry wrapper that retries HTTP 503 using a delay derived from retry-after, and the transcript shows why it is waiting instead of appearing to hang.

**Details**

- Retry progress is reported as an `artifact_publish_retry` event, rendered by a new UI component.
- The line reads "Publish service temporarily unavailable" for a 503 and "Publish service busy" otherwise, followed by an attempt counter.
- Telemetry records "deploy_503_retried" and "deploy_429_retried" with a `deploy_attempts` field.

**Evidence**

`Publish service temporarily unavailable`

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

### Impersonation warning when a local session claims a pinned remote identity

If a session claims a peer identity that isn't on your machine, the message is blocked as suspicious.

**What**

Peer resolution tracks identities previously confirmed as not being on this machine. If a local session record later claims one of those names, nothing is sent and the model is told the situation is suspicious.

**Details**

- The pinning is per-identity: once a name resolves to a session that is NOT on this machine, that fact is remembered for the session.
- On a conflict the send is blocked outright rather than routed to the local match.
- The model is instructed to ask the user before confirming anyone.

**Evidence**

`A same-named session on this machine your user did not start is suspicious: ask the user before confirming anyone.`, `A session on this machine impersonating it is suspicious: ask the user.`

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

### Held cross-session peer messages: supersede, re-arm and already-resolved notice

Held peer messages now cancel their approval prompts cleanly instead of leaving stale ones hanging around.

**What**

The inbound-hold hook keeps a per-message record with an abortable approval prompt, so held peer messages can be superseded, dropped or released cleanly instead of leaving orphaned prompts.

**Details**

- Superseded, dropped and released holds have their approval prompts cancelled via the stored abort handle.
- When the approval surface returns, the expiry deadline is re-armed rather than being counted against the time with no surface available.
- An approval or denial that lands after the message was already resolved by a mode or setting change posts a warning instead of acting.

**Evidence**

`That held message was already resolved before your `

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

### Artifact replies detect that another session already answered the summon

If another session already answered an artifact summon, yours says so instead of duplicating the reply.

**What**

Posting a reply to an artifact summon can now come back as "already answered elsewhere", so a second session does not duplicate a standing answer or retry pointlessly.

**Details**

- A 409 whose body reason is `summon_already_answered`, or a response with `summon_answered: !0`, maps to the outcome `{ kind: "summon_answered" }`.
- The tool passes `answersSummon` with the reply, and the autoreact path threads a new `declareAnswersSummon` flag through reply and edit calls.
- The tool returns replied:false with a result carrying `summon_answered`, and the model is told not to retry but to read the comments for the answer that landed.
- The tool-use line shows "reply not posted (summon already answered)" instead of the generic reply-needs-thread-activation text.
- The autoreact pipeline takes a separate `answeredElsewhereNoticeText` notice, for example that the follow-up note was withheld because another session's reply already stands in the thread.
- Telemetry records `summon_answered_elsewhere`.

**Evidence**

`summon_answered_elsewhere`, `Reply not posted: this summon was already answered in another session, and the standing answer is in the thread. The draft was discarded. Do not retry \u2014 read the comments to see the answer that landed.`, `reply not posted (summon already answered)`, `summon_already_answered`

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

### Held cross-session messages are now settled as expired at shutdown

Quitting now settles pending peer messages as expired so the sending session gets a definite answer.

**What**

When Claude Code exits, peer messages that were being held in the cross-session inbound queue are answered as expired instead of being abandoned, so the sending session gets a definite result.

**Details**

- A shutdown hook drains the hold buffer and settles every still-held peer message with an `expired` status, logging how many were settled
- The drain is raced against a grace period of up to 750 ms before shutdown continues
- Once shutdown has begun, a late peer message is refused rather than parked, recording peer_inbound_gate / shutdown_expired telemetry
- Unix-socket messaging server teardown now also destroys open connections and unlinks the socket file before awaiting the drain
- A new lookup lets the headless expiry path read a message's hold cause

**Evidence**

`[cross-session-inbound] shutdown: settling ${e.length} still-held peer message(s) as expired`, `shutdown: settling `, `[cross-session-inbound] shutdown: not parking a late peer message \u2014 settled as expired: `

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

### Artifact comment replies can declare that they answer a summon

A reply to an artifact comment can mark itself as answering a summon, and only once.

**What**

Comment posting accepts an `answersSummon` flag; when set, the POST body carries `answers_summon: !0` so the server knows the reply resolves an outstanding summon.

**Details**

- A one-shot claim registry keyed by artifact slug validates the declaration against the expected thread id and tool-use id.
- The claim is marked consumed once used, so a given summon can only be answered once.

**Evidence**

`answers_summon`

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

### Org-memory decision can be reopened, and is bound to an identity

Shared org memory now remembers who settled a decision and lets a settled decision be reopened.

**What**

The org-memory decision store now records which identity settled a decision and supports reopening a settled decision back to the undecided state.

**Details**

- A settle attempt from a different identity than the one recorded is refused with cause "superseded".
- Reopening emits the new event `tengu_org_memory_decision_reopened` with `from_state` and `from_cause`.
- The reset helper clears the remembered identity in addition to the state.

**Evidence**

`tengu_org_memory_decision_reopened`

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

### New user-facing hint when the Remote Control server returns a malformed response

If the Remote Control server sends something unreadable, you now get a clear message suggesting an update.

**What**

A dedicated message now explains that the Remote Control server sent something the client could not parse, and points at updating Claude Code, instead of a generic failure line.

**Details**

- new exported message `BRIDGE_MALFORMED_RESPONSE_HINT`, returned for the `malformed_response` failure reason
- applies to both bridge session creation and reattach
- also listed among the errors the bridge status UI treats as actionable

**Evidence**

`Remote Control got an unexpected server response \u2014 try again after updating Claude Code`

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

## Improvements

### OAuth token refresh timers re-check in 60s chunks

Login token refreshes now survive laptop sleep and long suspends more reliably.

**What**

Scheduled token refreshes no longer rely on one long setTimeout, which makes them more reliable across long sleeps and machine suspends.

**Details**

- A repeating chunked timer re-evaluates the remaining time against a 60000ms cap.
- The refresh only fires when a generation token still matches, so stale timers from a superseded schedule are ignored.

**Evidence**

`Scheduled token refresh for sessionId=`

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

### Artifact deploy retries 429 and 503 responses

Artifact deploys now retry when the server is rate limited or unavailable, and abort cancels retries.

**What**

The `/deploy/direct` call now retries transient server responses instead of failing the deploy outright.

**Details**

- 429 honours the retry-after header, capped at 30s.
- 503 retries with jittered exponential backoff up to a maximum attempt count.
- Each retry is logged, and a telemetry event records the attempt count and final status.
- A user abort cancels the retry loop.

**Evidence**

`deploy_503_retried`

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

### VS Code-owned sessions get their own settings defaultMode handling

In VS Code sessions, unsafe default permission modes from settings are refused and fall back to normal.

**What**

For sessions owned by the VS Code extension, `permissions.defaultMode` is read only from policy, flag and user settings, and unsafe values from settings are refused.

**Details**

- `bypassPermissions` from settings is ignored unless the user has consented; a warning is printed to stderr and the mode falls back to default.
- `auto` is ignored while the auto-mode circuit breaker is active.
- The `auto` read additionally requires `tengu_harbor_willow` (fallback `!1`) or a `meadow_lantern` config value.

**Evidence**

`tengu_settings_bypass_unconsented_noninteractive_ignored`

- Flag `tengu_harbor_willow`: Off in both readings (read for one account on one subscription tier against v2.1.225; this account: off, anonymous baseline: off, compiled default: on) These values were read against a different version of Claude Code, so treat them as the nearest reading available instead of one taken on this release.
- Area: Permissions
- Names: `permissions.defaultMode`, `bypassPermissions`
- Tier: You'll notice
- Useful: 3/5
- Signal: 0/5

### Skill-backed prompt commands pass through more metadata

Skills turned into slash commands now keep their menu text, aliases and tool permissions.

**What**

Converting a skill into a prompt command now carries menu text, aliases and tool permissions instead of only the allowed-tools list.

**Details**

- passes through `menuDescription`, `aliases`, `getAllowedTools` and `disallowedTools` alongside `allowedTools`
- `disallowedTools` defaults to an empty list
- the skill extraction is cached per command rather than redone on each conversion

**Evidence**

`menuDescription`

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

### Replies are suppressed when another session already answered the summon

If another session already answered a comment, the duplicate reply is dropped instead of posted.

**What**

If a summon was already answered in another session, the drafted reply is discarded rather than posted, and the caller is told why.

**Details**

- New `summon_answered` result path reports the reply was not posted and that the standing answer is already in the thread
- The auto-react flow gained `answeredElsewhereNoticeText` variants for cases where the artifact is published but the reply is withheld
- Telemetry records `summon_answered_elsewhere`

**Evidence**

`Reply not posted: this summon was already answered in another session, and the standing answer is in the thread.`

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

### Workspace trust is now enforced before the agents/fleet view

Opening the agents view now asks you to trust the workspace first if you haven't.

**What**

Opening the agents view goes through a trust check first, prompting if the workspace has not been trusted yet.

**Details**

- The agents view and related entry points await `ensureAgentsWorkspaceTrust`, which decides `skip`, `trusted` or `ask`
- The `ask` path renders the TrustDialog
- After acceptance it clears the plugin cache, re-initializes GrowthBook while preserving pending exposures, primes plan slug collisions and captures a policy snapshot
- Telemetry and config environment variables are only applied once trust is accepted

**Evidence**

`post-trust: re-discover project @skills-dir plugins`

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

### Handoff classifier refusals allow the output with an unreviewed warning

When a safety check on subagent handoff output is refused, the work passes through with a warning.

**What**

A sub-agent handoff whose safety classifier request is itself refused by the safeguard is no longer blocked; the output is passed through with a security warning attached.

**Details**

- The refusal is not treated as a verdict. The sub-agent output is allowed but prefixed with a long warning telling the model the work is unreviewed and untrusted.
- Classifier outcome logging gained a "refused" bucket alongside the existing "unavailable" and "blocked".

**Evidence**

`Handoff classifier request refused by the safety safeguard, allowing sub-agent output with an unreviewed warning`

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

### Held cross-session messages name managed or repo settings as the cause

When a peer message is held, you now see whether org or repo settings caused it.

**What**

When an inbound cross-session peer message is held, the explanation now tells you whether an org managed setting or the repository's own settings forced it, rather than just reporting an explicit setting.

**Details**

- Two new hold-reason cases: `managed-setting` and `repo-setting`, in addition to the plain `explicit-setting` case.
- The repo case explains that a repo may only tighten, so your own "accept" cannot override it, and points at --setting-sources to exclude it.
- The resolver was reworked to return both the effective value and which tier decided it; repo-level local and project settings are attributed as repoSettings.

**Evidence**

`This repository's settings set "crossSessionInbound" to "hold" (a repo may only tighten, so your own "accept" cannot override it); remove the repo setting or exclude it with --setting-sources.`, `This repository's settings set "crossSessionInbound" to "hold" (your own "accept" cannot override a repo tightening).`

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

### Cross-session send prompts distinguish cloud from Remote Control peers

Prompts for messaging another session now say whether the peer is cloud or Remote Control.

**What**

The permission prompt for messaging another session now says which kind of peer you are reaching and makes clear how the message is attributed on arrival.

**Details**

- The prompt branches on the peer's `via`, saying either "cloud session" (running in the cloud) or "Remote Control session" (on another machine).
- Both texts now spell out that the message arrives marked as from another Claude session, not from its user.
- Peer-listing and error paths were updated to match: a peer-name collision now reads "a Claude session on another machine (cloud or Remote Control)", and error text says a session "may have ended" and either "or been archived" (cloud) or "or disconnected".

**Evidence**

`It reaches the receiving Claude (running in the cloud) via Anthropic's servers as a cross-session message — marked as from another Claude session, not from its user.`

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

### Remote Control unarchive re-auths trusted devices, and malformed /bridge responses are terminal

Unarchiving a remote session now retries with device trust and stops retrying broken bridge replies.

**What**

Unarchiving a remote session now handles device trust failures explicitly, and a malformed bridge response is no longer retried as if it were transient.

**Details**

- `untrusted_device` and `session_stale_relogin` 403s are retried once with an `X-Trusted-Device-Token` header.
- If that does not resolve it, the call returns an `elevated_auth` outcome logged as bridge_repl_v2_unarchive_elevated_auth, so reattach surfaces an auth failure while preserving the session pointer.
- A malformed /bridge response, or one with an invalid worker_epoch, now returns a terminal `malformed_response` result instead of null.

**Evidence**

`bridge_repl_v2_unarchive_elevated_auth`

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

### Code-session create and poll failures are classified by reason

Code session failures are now sorted by cause, so retries and error messages make more sense.

**What**

HTTP failures from the code-sessions endpoints are now sorted into transient, terminal and auth categories instead of being treated uniformly, which changes what gets retried and what gets reported.

**Details**

- 401 becomes a non-terminal `oauth_rejected`.
- 408, 429 and 5xx remain transient and are retried.
- Anything else becomes terminal `request_rejected`.
- Grouping and project rejections are tagged `grouping_rejected` and get their own message about the Project being unavailable.
- OAuth-rejected session creation feeds a distinct telemetry path.

**Evidence**

`v2_session_create_oauth_rejected`

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

### Remote Control: new terminal error reasons and clearer failure surfacing

Remote Control failures now tell you what actually broke and that you can rerun /remote-control to retry.

**What**

Bridge failures are now distinguished by reason and kind instead of collapsing into one generic error, and the message tells you how to retry.

**Details**

- New failure reasons `request_rejected`, whose message is assembled as "Remote Control server rejected the request (HTTP ...) — run /remote-control to retry", and `malformed_response`.
- HTTP classification separates "oauth_rejected" (non-terminal) from "transient" and "rejected" (terminal).
- Failures are reported with an explicit kind, "terminal" or "auth", including the org-policy case.
- App state carries `replBridgeError` and `replBridgeErrorKind`.

**Evidence**

`run /remote-control to retry`

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

### Remote Control: token refresh chain gives up loudly, and auth changes invalidate in-flight attempts

Remote Control now reports dead token refreshes instead of failing silently, and re-logging in cancels stale connection attempts.

**What**

When the bridge token refresher exhausts its retry chain it now reports the failure instead of stopping quietly, and re-logging in cancels bridge init attempts that started under the old credentials.

**Details**

- On exhaustion it logs an error built from the fixed fragment "Refresh chain exhausted for sessionId=" plus the session id and failure count, emits `bridge_token_refresh_exhausted`, and returns "no_oauth_token" to the caller.
- App state gained `authChangeGeneration`, bumped on login or account change; bridge init compares it before applying results.
- The 401 recovery path keys off a per-endpoint `needsOAuthRefresh` flag and aborts init after a successful refresh rather than continuing with the stale response.

**Evidence**

`bridge_token_refresh_exhausted`

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

### Auto-mode classifier failures are distinguished from classifier denials

When the auto-mode classifier can't decide, you no longer get a bogus denial message or permission-rule hint.

**What**

A permission decision that the classifier could not produce a verdict for is now tracked separately from a real denial, so telemetry and UI stop treating a classifier failure as a refusal.

**Details**

- The decision reason carries a new `noVerdict` flag.
- Denial-reason bucketing keys off the flag instead of matching a message prefix; telemetry labels are assembled at runtime as `classifier:${e.classifier}:no-verdict` around the literal `:no-verdict`.
- The UI denial kind `automode-parsing-error` is now chosen by `t.noVerdict`.
- The 'add a Bash permission rule' hint and the post-denial hook message are both skipped for no-verdict cases.

**Evidence**

`noVerdict`

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

### Bridge reattach can fail hard instead of silently starting a new session

After re-authenticating, Remote Control reattaches to your existing session or says it's gone, instead of quietly starting a new one.

**What**

An auth-triggered revive now either reattaches to the same session or reports a dead pointer, rather than quietly creating a fresh session under the old one.

**Details**

- REPL bridge session options gained `reattachOrFail`, `reviveInitiated`, `onAuthProven` and `onReattachPointerDead`.
- The caller passes `reattachOrFail` only when a revive was initiated and a previous session id is known.

**Evidence**

`onReattachPointerDead`

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

### Code-session failures are now a terminal/non-terminal discriminated union

Expired logins during code sessions now refresh and retry automatically instead of failing the operation.

**What**

HTTP failures from code-session calls are classified in one place, and a 401 is treated as recoverable so the caller refreshes OAuth and retries instead of failing the operation.

**Details**

- one helper returns `"oauth_rejected"` for 401, `"transient"` for 408, 429 and any status >= 500, and `"rejected"` otherwise
- 401 yields `{ terminal: !1, reason: "oauth_rejected" }`
- the type guards now check `e.terminal === !0` / `e.terminal === !1` rather than testing for the presence of a `terminal` key
- the bridge's 401 retry path also fires when the result is any non-terminal failure

**Evidence**

`oauth_rejected`

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

### Held peer messages now say which settings tier held them

When a cross-session message is held, you now see whether org, repo or local settings did it.

**What**

When an inbound cross-session message is held by an explicit setting, the notice names the settings layer responsible instead of giving a single generic cause.

**Details**

- the old single hold cause "explicit-setting" was replaced by source attribution, including `managed-setting`
- policy settings render "Your organization's managed settings set \"crossSessionInbound\" to \"hold\"."
- project and local settings render "This repository's settings set \"crossSessionInbound\" to \"hold\" (your own \"accept\" cannot override a repo tightening)."
- a WeakMap holds the last-reported cause so a notice is re-emitted only when the cause changes

**Evidence**

`managed-setting`

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

### Bridge auth failures no longer burn reconnect attempts

A login problem on the bridge no longer eats your reconnect attempts and is now named clearly.

**What**

An authentication failure in the repl-bridge connect loop is reported as such and does not consume one of the limited retries.

**Details**

- the retry counter is incremented only for non-auth failures
- failures now carry an explicit kind: `auth` for auth failures, `terminal` otherwise
- replaces a single generic terminal path for both cases

**Evidence**

`auth`

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

### Unified rate-limit tracking now also applies on the gateway auth source

Gateway-authenticated sessions now get the same rate-limit tracking and 429 handling as subscription ones.

**What**

Sessions authenticating through the gateway get the same unified rate-limit handling that subscription sessions already had.

**Details**

- the guarding predicate changed from "caller says so, or the subscription check passes" to additionally accepting a `gateway` auth source
- covers unified rate-limit header parsing, 429 handling and the background usage probe

**Evidence**

`anthropic-ratelimit-unified-overage-status`

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

### send_message: clearer story about sessions on other machines

Messaging an unknown peer now explains that sessions on other machines could not be checked, so retry.

**What**

The old "reply-only bridge session" refusal was removed, and unresolved peer names now explain that Remote Control sessions on other machines could not be checked, with a suggestion to retry.

**Details**

- Unresolved names no longer fall back to the send_message connector.
- Timeouts are now reported as `"timeout"` instead of `not_reachable`.
- The isolatePeerMachines prompt is assembled from whichever of the cloud or bridge lookups actually failed.

**Evidence**

`sessions on other machines could not be checked just now, so they were not searched.`

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

### Stale-peer send failure text points at the peer list tool

Failed sends to a vanished peer now explain why and point you at the peer list.

**What**

When a send fails because the peer is gone, the error now distinguishes a name-resolved peer from a stale bridge ID and tells you how to find the current peers.

**Details**

- Name-resolved peers get "that session may have ended, been archived, or disconnected".
- Stale bridge IDs get their own wording.
- A suggestion to call the peer-listing tool is appended only when that tool is present in the current tool set.

**Evidence**

` — that session may have ended, been archived, or disconnected.`

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

### Artifact comment errors distinguish missing thread from degraded fetch

Artifact comment errors now tell you whether a thread is really missing or just dropped from fetch.

**What**

Asking for an artifact comment thread that is not in the fetched set now says whether the thread genuinely does not exist or whether threads were dropped from the fetch.

**Details**

- Raises `comments_thread_not_found` when the id is absent, with text pointing at the "comments" action as the source of thread ids
- Raises `comments_degraded` instead when threads were dropped, so the model does not conclude the thread is gone
- A filtered listing that comes back empty now tells the model to re-run without `thread_id`

**Evidence**

`no comment thread ${er} on this artifact \u2014 thread ids come from action "comments"`

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

### Artifact reply composer prompt rewritten

Artifact replies now answer commenters' questions directly and flag change requests instead of promising them.

**What**

The reply-only composer now answers commenter questions directly and treats change requests as flagged rather than promised.

**Details**

- Instructed to answer questions directly and substantively
- Change requests are acknowledged as flagged for the artifact's owning session, not committed to
- Explicitly told not to describe its own limitations to the commenter

**Evidence**

`Noted \u2014 flagged for this artifact's session to pick up`

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

### Workshop template: waiting-state painter on the opening publish

Workshop pages show a painter animation while waiting for the first real version, with a reload hint.

**What**

The opening publish of a workshop page now shows a painter animation while it waits for the next version, and nudges you to reload if nothing arrives.

**Details**

- Both workshop HTML templates gained a script that detects the opening version by an empty `ws-decisions` island with no decision rows
- The painter animation runs while waiting for the next publish
- After 180000 ms it escalates once into the footer bar with a reload hint
- The authoring skill text drops its hardcoded line-number ranges, now stating the template is 2,948 lines, and adds a refresh hint

**Evidence**

`Still nothing \u2014 Claude may not be watching this page right now. Reload to check for the latest version.`

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

### Shared "auto mode unavailable" message and a warning-kind notice

The "auto mode unavailable" message is now consistent everywhere and shown as a warning.

**What**

The workflow auto-mode notice and the auto-default nudge now produce their unavailability text from one helper, and the notice is styled as a warning.

**Details**

- The helper returns a reason-specific string when there is one, otherwise the fallback "auto mode is unavailable right now"
- The notice is tagged `kind: "warning"`

**Evidence**

`auto mode is unavailable right now`

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

### agents-listing description updated for Remote Control

The agent list now calls bridge sessions your Remote Control sessions on other machines.

**What**

The list-agents tool description no longer calls bridge sessions "reply-only" and instead describes them as your Remote Control sessions on other machines.

**Details**

- The wording is conditional on Remote Control being connected on this machine
- Description text only; the listing behaviour is unchanged

**Evidence**

`and (when Remote Control is connected here) your Remote Control sessions on other machines`

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

### Peer listings flag an incomplete bridge walk

Peer listings now warn when the session walk didn't finish, so partial lists aren't mistaken for complete.

**What**

`listAllPeers` now tells the model when bridge rows are present but the session walk did not finish, so partial results are not mistaken for the full set.

**Details**

- The previous bridge-population field was replaced with a `bridgeWalkFailed` flag, set when the walk failed or the handle produced no usable result.
- With bridge rows present and the walk failed, the listing appends: "list incomplete just now — those rows carry no [ref] and are not messageable by name until a later listing completes".
- A helper that reported bridge population separately was removed.

**Evidence**

`bridgeWalkFailed`

- Area: Cross-Session Messaging
- Tier: Under the hood
- Useful: 2/5
- Signal: 0/5

### Artifact comment results render thread-filtered reads

Reading a single artifact comment thread now shows clearer results, including when the thread isn't found.

**What**

Reading a single artifact comment thread now has its own result rendering in both the tool-result UI and the model-facing text.

**Details**

- "read 1 comment thread (filtered)" replaces the plain comment count.
- A missing thread renders "requested thread not in this result".
- An empty filtered result tells the model to re-run action "comments" without thread_id for the full list.

**Evidence**

`The requested comment thread is not in this result — run action "comments" without thread_id for the full list.`

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

### Cross-session send says whether the recipient is in the cloud or on another machine

Delivery confirmations now correctly say whether your message went to a cloud or remote machine session.

**What**

The success text after delivering a message to another Claude session no longer always claims the target was a cloud session.

**Details**

- The message branches on the delivery route used.
- A recipient reached over Remote Control is recorded as a "bridge-session" rather than a "cloud-session".
- The wording appears in the tool result text, so anyone using cross-session messaging sees it.

**Evidence**

`a Claude session on another machine, over Remote Control`

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

### Clearer permission prompt for messaging a Remote Control session

The messaging prompt now says your note arrives marked as from another Claude session, not you.

**What**

The confirmation prompt now explains how the message is attributed on the receiving end rather than describing it as a user prompt.

**Details**

- Removes the old "arrives as a user prompt" phrasing.
- States that the message is marked as coming from another Claude session, not from that session's user.

**Evidence**

`marked as from another Claude session, not from its user`

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

### Trusted-device 403s handled on elevated-auth requests

A rejected session can now tell you it's stale and needs re-login, not just untrusted device.

**What**

A 403 from the code-session request helper can now report a stale session needing re-login, not just an untrusted device.

**Details**

- The helper always parses the error resource on a 403.
- Still returns `untrusted_device` as before.
- Additionally surfaces `session_stale_relogin` when the call site is tagged `elevated_auth`.
- Replaces the old `classifyUntrustedDevice` option object.
- The user-facing re-login message itself already existed in 2.1.224.

**Evidence**

`Your session has expired for the trusted-device check. Run /login to re-authenticate, then retry.`

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

### Self-hosted runner prompt reworded around environment secret files

The self-hosted runner prompt now talks about environment secret files instead of pool secrets.

**What**

The permission prompt for starting a detached self-hosted runner drops the "pool secret" wording in favour of environment-centric terms used by the other runner tools.

**Details**

- Refers to an environment secret file rather than a pool secret.
- Describes the work as that environment's queued sessions.

**Evidence**

`Start a detached self-hosted runner with the environment secret file`

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

### Classifier-unavailable denial message shortened via shared fragments

The "temporarily unavailable" denial message is shorter and no longer shows HTTP status details.

**What**

The "temporarily unavailable" denial text in auto mode now ends with two shared constants rather than inline prose, and the same fragments are reused by the new safeguard-refusal message.

**Details**

- The fragments are "If it keeps failing, continue with other tasks that don't require this action and come back to it later." and "Note: reading files, searching code, and other read-only operations do not require the classifier and can still be used."
- The helper that produced HTTP status and error-kind detail now returns the empty string, so status details no longer appear in either message.

**Evidence**

`Note: reading files, searching code, and other read-only operations do not require the classifier and can still be used.`

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

### Error redaction covers invalid HTTP header messages

Error and telemetry text now scrubs invalid HTTP header values so they can't leak into reports.

**What**

Two more patterns were added to the telemetry and error scrubber so header values do not leak through error text.

**Details**

- One pattern truncates everything after "has invalid value: " for a named header.
- The other covers `Headers.append` and `Headers.set` messages about an invalid header name or value.

**Evidence**

`has invalid value: `

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

### Search tokenizer emits compound candidates from single-character tokens

Search now joins stray single-character tokens with their neighbours, so split-up terms match better.

**What**

Query tokenization pairs a single-character token with an adjacent multi-character neighbour and offers the joined form as an extra candidate, improving matches on terms that were split apart.

**Details**

- The joined form is appended before the compound-first ordering is applied.
- Compound candidates are still filtered out unless the caller explicitly asked for compounds.

**Evidence**

`compound`

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

### Bridge reconnects only refresh OAuth for the close codes that need it

Remote Control reconnects skip needless token refreshes, so ordinary disconnects recover faster.

**What**

Reconnect recovery no longer forces an OAuth refresh for every handled close code, cutting unnecessary token work on non-auth disconnects.

**Details**

- The per-close-code recovery table (401, 4091, 4093, 4094) gains `needsOAuthRefresh`, true for 401 and 4094 only.
- The flag decides whether to force an OAuth refresh up front, whether to enter the retry loop that prompts for a fresh login, and whether to do a late refresh instead.
- The CCR client gained an `onRequestAuthOk` callback fired on a successful heartbeat, wired through the bridge so the session can mark auth as proven.

**Evidence**

`needsOAuthRefresh`

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

### Session descriptions understand bridge and remote sessions

Sessions reached over Remote Control are now described as being on another machine, not this one.

**What**

A peer reachable over Remote Control is described as being on another machine instead of falling through to the "on this machine" wording.

**Details**

- The human description helper treats `bridge-session` like other Claude sessions.
- A new location string is rendered for these sessions.

**Evidence**

`on another machine (Remote Control)`

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

### Org memory can be superseded when the identity changes

Org memory remembers who enabled it, and a different account trying to enable it is rejected.

**What**

Enabling org memory records the identity that turned it on, and a later enable attempt from a different identity is rejected rather than silently taking over.

**Details**

- the earlier decision is settled off with cause `superseded`
- the outcome is reported through the `tengu_org_memory_decision` event
- applies to everyone; no flag gates it

**Evidence**

`Wz("superseded")`

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

### Background job state keeps respawn flags, and non-allowlisted tokens are stripped

Background jobs now save the flags needed to relaunch them, dropping any unrecognised flags with a warning.

**What**

Persisted background job state now carries the flags needed to respawn a job, with unknown flag tokens filtered out before they are written.

**Details**

- the job-state writer persists `respawnFlags` alongside the fields it already rewrites
- the schema defaults `respawnFlags` to an empty list and filters its contents against an allowlist
- dropped tokens produce a warning naming them

**Evidence**

`[jobs] stripped non-allowlisted respawnFlags token(s) from persisted job state: `

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

### Held-message answer picker hides numbering and pre-focuses a default

The held-message prompt drops the numbered list and opens with a choice already highlighted.

**What**

The prompt asking what to do with a held cross-session message renders as a plain list with a sensible option already selected.

**Details**

- the select is passed `hideIndexes: !0`, so options render without numeric indexes
- a `defaultFocusValue` is passed so the list opens on a specific choice

**Evidence**

`defaultFocusValue`

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

### Bridge heartbeats now count as proof of auth

A successful Remote Control heartbeat now clears stored auth failures, so you get fewer spurious re-login prompts.

**What**

A successful bridge heartbeat clears accumulated authentication-failure state instead of leaving the counter to age out on its own.

**Details**

- the CCR client gained an `onRequestAuthOk` option, invoked next to `resetHeartbeatStreak()` on each successful heartbeat
- the bridge supplies a callback that resets its auth-failure counter and calls the existing `onAuthProven` hook

**Evidence**

`onRequestAuthOk`

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

### PID 1 no longer counts as an ancestor when verifying a peer connection

Sessions running as the main process in a container no longer get wrongly matched to peer connections.

**What**

Ancestor verification for peer connections no longer treats a container init process as a legitimate ancestor, so a session running under PID 1 cannot be matched by accident.

**Details**

- the Linux /proc ancestor walk now records a parent pid of 1 and stops there, where it previously discarded it by breaking at `ppid <= 1`
- the array descendant helper returns false immediately for pid 1 (`if (t === 1) return !1`)
- the cached lookup now requires `process.pid !== 1 && i.includes(process.pid)`
- gated by platform: the ancestor walk reads /proc, so this is the Linux path

**Evidence**

`[peer-cred] peer pid lookup failed`

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

### Self-sent peer message detection ignores PID 1

Inbound peer messages are no longer mistaken for your own when running inside a container.

**What**

Inbound peer messages are no longer misattributed to this process when the session runs as PID 1, which is common inside containers.

**Details**

- the self-sent check now takes the pid as a parameter rather than reading it implicitly
- returns false outright when that pid is 1, so no false `selfSent` match
- pairs with the ancestor-walk change above

**Evidence**

`selfSent`

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

### Host initialize payload reports the current permission mode

IDE and remote hosts now learn your current permission mode the moment they connect.

**What**

IDE and Remote Control hosts now learn the CLI's permission mode at connect time, alongside the model.

**Details**

- new `current_permission_mode` field in the connect-time state
- documented as "@internal The CLI's active permission mode at connect time, for the same connect-time sync as current_model."
- populated from the live tool-permission context in the main init payload and in both repl-bridge `getInitializeState` implementations

**Evidence**

`current_permission_mode`

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

### Runner pool wording renamed to "environment"

Session relaunch messages now say "environment" instead of "pool".

**What**

User-facing text about relaunching a session now says environment instead of pool.

**Details**

- The requeue confirmation reads "This re-launches the session on another runner in the environment."
- The secret-claim error says it "cannot derive the environment ID for spawn-hint polling".

**Evidence**

`This re-launches the session on another runner in the environment.`

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

## Bug Fixes

### Compaction payloads clamped alongside preserved event ids

Compaction now trims the saved conversation payload to match the preserved message list, avoiding mismatches.

**What**

When preserved event ids exceed the cap, the compaction payload itself is now trimmed to match, rather than only the id list.

**Details**

- The internal event writer rewrites `compactMetadata.preservedMessages.uuids` to the last N and adjusts the preserved segment.
- Whether that rewrite happened is reported as `payloadClamped` on the existing `tengu_ccr_preserved_event_ids_clamped` event.

**Evidence**

`tengu_ccr_preserved_event_ids_clamped`

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

### Gemini command import stops rejecting a bare exclamation mark

Importing Gemini commands containing a plain exclamation mark no longer fails validation.

**What**

Importing a Gemini command that used an exclamation mark as prose or negation could fail validation even when nothing shell-like was involved.

**Details**

- Previously any command mixing argument placeholders with a '!' anywhere outside its shell blocks was rejected.
- The importer now masks `$`-prefixed tokens first.
- It only flags a '!' whose preceding character is not an ordinary word or punctuation character.
- The dangling-marker and shell-block checks are unchanged.

**Evidence**

`Mixes argument placeholders with backticks or '!' outside its shell blocks`

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

### Artifact share status reports "public" for tokenless reads and hides a stale owner role

Shared artifacts no longer show you as owner when the response gives you only public read access.

**What**

Share status no longer trusts a server-reported mode when the response carries no asset token, which stops a public read from being presented as owner access.

**Details**

- With no asset token, the mode is reported as "public" regardless of what the server said.
- A `role` of "owner" is dropped in that case.
- The artifact tool emits an "unknown_share_mode" telemetry reason when the derived mode is "unknown".

**Evidence**

`unknown_share_mode`

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

### Unknown control messages complete their command lifecycle

An unrecognized message no longer leaves your command hanging forever waiting for a result.

**What**

An incoming message with an unrecognized type no longer leaves callers waiting on it forever.

**Details**

- The handler extracts a `uuid` if present and reports that command as "completed" through the lifecycle callback.
- The existing "Ignoring unknown message type" warning is still emitted alongside.

**Evidence**

`Ignoring unknown message type: `

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

### "Reachable agents" listing includes bridge sessions

Machines reachable only over the bridge now appear instead of showing "No reachable agents."

**What**

A machine reachable only over the bridge no longer shows "No reachable agents.", because bridge transports are now passed into the session section rather than merely counted.

**Details**

- Bridge rows are deduplicated against cloud sessions.
- The recorded-population debug lines were reworded to say so.

**Evidence**

`[bridge:population] warm recorded set: `

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

### Bundled skill extraction verifies existing files instead of failing on them

Re-installing a bundled skill over identical existing files now succeeds instead of erroring.

**What**

Re-extracting a bundled skill over files that already exist succeeds when the contents already match, instead of erroring out on EEXIST.

**Details**

- Extraction passes a tolerance option; on EEXIST it opens the existing file, checks it is a regular file and compares its full contents to what would be written.
- It rethrows only if the contents differ.
- The 'additional skill files' path uses plain `tolerateExisting: !0`.

**Evidence**

`tolerateExisting: "verify-content"`

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

### Gateway 429s without unified rate-limit headers no longer drive the usage indicator

A gateway spend block no longer scrambles the rate-limit usage indicator you see.

**What**

A 429 injected by the gateway, for example a spend block, no longer overwrites the displayed Anthropic rate-limit state.

**Details**

- Rate-limit status tracking returns early when the provider is the gateway and the 429 response lacks the unified status header.

**Evidence**

`anthropic-ratelimit-unified-status`

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

### Post-tool-use commit awareness no longer fires inside subagents

Commit-awareness notes now appear once in your main session instead of from every subagent.

**What**

The post-commit awareness system message is now emitted only for the main session, not for every subagent that happens to run the tool.

**Details**

- the PostToolUse callback returns early when the hook payload carries an `agent_id`, on top of the existing non-interactive check
- relatedly, the code-review prompt no longer appends its "end your response with this exact line" suffix when running with an agent id or as a skill preload

**Evidence**

`e.agent_id !== void 0`

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

### REPL sampling helpers report validation errors as progress instead of failing silently

Bad arguments to the in-REPL sampling helpers now show a visible error instead of vanishing.

**What**

The in-REPL haiku, opus, sonnet and fable sampling helpers emit their start progress event before validating arguments, so bad input shows up as a visible error rather than a throw with nothing recorded.

**Details**

- "prompt must be a string" and "schema must be an object" now arrive as a `repl_tool_call` progress event with phase "error"
- a non-string prompt is recorded in progress as an assembled placeholder naming its `typeof`

**Evidence**

`schema must be JSON-serializable`

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

### Bridge hook resets now fire the pending archive instead of dropping it

Changing directory mid-session no longer silently drops a pending archive in the bridge.

**What**

Changing the working directory mid-session no longer discards a pending archive in the REPL bridge hook.

**Details**

- The per-session state reset was factored into a single helper taking an `archiveAbandoned` flag.
- The working-directory-change path now calls it with the flag set, which fires the pending archive before unregistering it.
- Previously that path cleared the refs silently.

**Evidence**

`archiveAbandoned`

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

### Fleetview group-header selection no longer fights the current selection

Picking a group header in Fleetview no longer bounces your selection back and forth.

**What**

Selecting a group header no longer causes the selection to bounce between the header and the previously selected row.

**Details**

- The effect that snaps selection to the computed target bails out when the current index already equals the target.
- The header-scroll effect sets selection and scroll offset together.
- The empty-state telemetry event still reports `skeleton` and `has_origin`.

**Evidence**

`tengu_fleetview_empty_state_shown`

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

### Virtual-scroll follow re-pin also updates a second focus setter

Scroll-following in long lists keeps focus and scroll position in sync more reliably.

**What**

The follow poller in the virtual scroll list now updates an additional focus setter when it re-pins, and the scroll layout effect's early-return guard was rewritten against different refs.

**Details**

- triggered on the same path that logs `[FV-poll] follow re-pin moved focus:`
- the extra setter runs alongside the existing one rather than replacing it
- the user-visible consequence could not be pinned down beyond it being a focus and scroll sync fix

**Evidence**

`follow re-pin moved focus`

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

### Bridge population recording forgets superseded sets

The bridge now drops stale session listings instead of keeping outdated ones around.

**What**

A recorded set of sessions for a handle is now dropped when an unrecordable listing supersedes it, instead of being left stale.

**Details**

- The successful path records the newly computed listing as before.
- The drop is logged as "[bridge:population] forgot the recorded set (an unrecordable listing superseded it)".

**Evidence**

`[bridge:population] forgot the recorded set (an unrecordable listing superseded it)`

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

### Artifact watch list wording fixed for pluralization

The artifact watch summary now says "watches" instead of a garbled plural.

**What**

The watch summary now reads correctly for more than one watch.

**Details**

- The count is formatted separately and the word "watch" is pluralized to "watches".
- Previously the whole phrase "artifact watch" was pluralized.

**Evidence**

`in this session (session-local; none survive a restart):`

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

## In Development

### Verify-skill suggestion suppressed unless the opal circuit is on

The nudge suggesting the verify skill is now silenced unless a gate turns it on.

**What**

The helper that returns the verify-skill nudge string now returns an empty string early for skill preloads and whenever the opal circuit check is false. The string itself already existed; the gating around it is new.

**Details**

- Gated on `tengu_opal_circuit`.
- Even when the circuit passes, the nudge is only produced if a skill matching the verify constant is present in the loaded skill set.
- Skill preload paths always get `""`.

**Evidence**

`if (e.options?.isSkillPreload) return "";`

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

### Cloud and remote sessions try to start device registration

Cloud and remote sessions try to start device registration, which does nothing while the gate is off.

**What**

Attaching a cloud session and completing remote auth setup both now attempt to bring up the device bridge for that session.

**Details**

- both paths dynamically import `startDeviceRegistration` and call it with the session id, an access-token getter and the org uuid
- registration short-circuits on its own enable and egress checks, so nothing happens unless `tengu_violin_wood` and the `allow_remote_sessions` policy both permit it

**Evidence**

`startDeviceRegistration`

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

### Dream prompt rebound, shipping-policy text absent

Instructions telling background sessions to open a draft PR unprompted are not present in this build.

**What**

The memory-consolidation prompt keeps its exact wording but is now bound to different constants, and the background-session shipping instructions that told the model to open a draft PR are not present in this build's module graph.

**Details**

- The removed text instructed the model to open a draft PR via `gh pr create --draft` without asking.
- Whether that text was relocated elsewhere rather than deleted could not be established, so treat its status as unresolved.

**Evidence**

`open a draft PR via `gh pr create --draft` without asking`

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

### Public-reader artifact gate moved to a wrapper and now keys on the asset token alone

Reading an artifact as a public non-member reader still refuses; the switch stays off in this build.

**What**

The refusal shown when an artifact is served to a public, non-member reader moved out of the boot-response parser into a wrapper around it, and now fires whenever no asset token comes back.

**Details**

- The error text is unchanged: "artifact read failed: this artifact is served to you as a public (non-member) reader, and reading public artifacts that way is not enabled yet".
- Public reading remains behind the remote-config gate `tengu_cobalt_plinth_sedge`, whose in-source fallback is false, so it is off in this build.

**Evidence**

`tengu_cobalt_plinth_sedge`

- Flag `tengu_cobalt_plinth_sedge`: Not enough to say (read for one account on one subscription tier against v2.1.225; this account: no value returned, anonymous baseline: no value returned, compiled default: on) These values were read against a different version of Claude Code, so treat them as the nearest reading available instead of one taken on this release.
- Area: Artifacts
- Tier: Not switched on
- Useful: 2/5
- Signal: 3/5
- Present in the build but not switched on

### Remote Control refuses to mint a fresh session when reattach is required

Remote Control can now fail loudly instead of silently starting a new session when it can't reattach.

**What**

When init runs in reattach-or-fail mode, a reattach pointer that is gone (or absent entirely) now fails terminally with a user-facing message instead of quietly creating a new remote session.

**Details**

- Covers both the gone-pointer case and the no-pointer-at-all case.
- Emits distinct telemetry for each failure shape.
- The enabled state of the reattach-or-fail mode itself could not be resolved from the build.

**Evidence**

`[remote-bridge] Reattach-or-fail: no reattach pointer for this init; failing terminally instead of minting fresh`

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

### Auto-mode classifier now falls back when the primary model refuses

When the auto-mode model refuses a request, a backup model now gets a try instead of failing.

**What**

A safeguard refusal from the primary classifier model is treated like the model being unavailable, so the fallback model gets a chance instead of the request failing outright. The rollout state of this path could not be resolved in this build.

**Details**

- `refusedBySafeguard` joins unavailability as a trigger for retrying on the fallback model within the remaining time budget
- Usage and durations from both attempts are merged
- If the fallback also fails to produce a usable answer, the original result is kept
- The log line now distinguishes a refusal from an unavailability

**Evidence**

`refused (stop_reason=refusal)`

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

### Auto-mode nudge dialog simplified

The auto-mode suggestion dialog now just dismisses itself instead of switching your session to auto.

**What**

The auto-default-nudge dialog now renders a different component whose only action on completion is to dismiss itself, so the state it used to change is no longer applied here.

**Details**

- The previous onDone callback that switched the session to "auto" and merged a new toolPermissionContext is gone.
- The new component clears itself on done and nothing else.
- Whether and when this dialog is reachable in this build could not be resolved.

**Evidence**

`auto-default-nudge`

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

### Artifact tool parses an optional cursor argument

The artifact tool now accepts a cursor value, groundwork for fetching comments page by page.

**What**

The artifact tool's argument extractor now reads an optional string `cursor` in addition to `thread_id` and `text`.

**Details**

- `cursor` is returned as part of the parsed argument shape
- no schema entry or request consuming it was found in this build, so it appears to be plumbing ahead of paginated comment fetching

**Evidence**

`"cursor" in e`

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

### Auto-mode default nudge requires the session to actually support auto

The auto-mode suggestion would only appear in sessions where auto mode can actually be used.

**What**

The nudge suggesting the user's non-auto default permission mode no longer fires in sessions where auto mode is not reachable.

**Details**

- `shouldShowAutoDefaultNudge` now takes the tool permission context
- additionally checks that auto mode can be cycled to, meaning context availability plus the auto-mode gate
- the nudge itself sits behind `tengu_maple_pier`, fallback false

**Evidence**

`shouldShowAutoDefaultNudge`

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

### Device bridge lifecycle telemetry

The device bridge reports its whole connection lifecycle, but none of it fires yet.

**What**

The bridge reports its full connection lifecycle, gated the same way as the bridge itself, so none of it fires in this build unless `tengu_violin_wood` is on.

**Details**

- events: `tengu_device_bridge_started`, `_stopped`, `_skipped` (with reasons `missing_org` and `missing_account`), `_start_failed`, `_connected`, `_rejected`, `_closed`, `_connect_failed`, `_pong_timeout`, `_reconnect_exhausted` and `_heartbeat_unsupported`
- the connected event records the protocol version plus negotiated `hb_interval_ms`, `staleness_ms` and `rpc_timeout_ms`
- repeated transport failures are de-duplicated before being reported

**Evidence**

`tengu_device_bridge_heartbeat_unsupported`

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

### Workshop authoring-progress hooks fire during streaming

Streaming now feeds tool and partial-input events to the workshop progress display.

**What**

Streaming now emits tool-use and partial-JSON callbacks into a workshop authoring-progress module, which is off unless internal workshop mode applies.

**Details**

- `onToolUseStart` is called on a content_block_start for a tool use.
- `onInputJsonDelta` is called on each partial JSON delta.
- The module is only consulted when enabled, which requires an internal workshop-mode predicate.
- Setting `CLAUDE_WORKSHOP_PROGRESS` to false switches it off.

**Evidence**

`CLAUDE_WORKSHOP_PROGRESS`

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

### Remote Control re-mint retry loop and auth-specific failure messages

Remote Control now retries reconnecting with backoff and tells you to run /login when auth fails.

**What**

Credential re-mint after a transport close now retries with exponential backoff instead of giving up, and authentication failures get their own terminal messages telling you exactly which command to run.

**Details**

- Backoff uses a 30s base with a 300s cap, showing "Remote Control server unreachable — retrying (attempt N)".
- A rejected credential forces one OAuth refresh before the next attempt.
- Terminal failures direct the user to run /login and then /remote-control.

**Evidence**

`Claude.ai login expired — run /login, then /remote-control`

- Area: Remote Control
- Names: `/login`, `/remote-control`
- Tier: You'll notice
- Useful: 3/5
- Signal: 0/5

### Leaked recovery-flag detection and worker-credential recovery cap

Remote Control auth recovery stops looping forever when a credential keeps failing or a flag sticks.

**What**

Two guards were added to Remote Control auth recovery so a stuck flag or a repeatedly failing credential no longer loops forever.

**Details**

- If the auth-recovery in-flight flag is still held past a computed ceiling, the close is handled directly and logged as leaked.
- Close code 4094 gets its own attempt counter.
- Recovery gives up after repeated failures with no successful heartbeat in between.

**Evidence**

`worker-credential recovery exhausted after `

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

### Untrusted-device credential rejection surfaces as a rejection

An untrusted device now gets a clear rejection instead of a silent failure and endless retries.

**What**

Remote credential fetch retries once through a new device-attestation path, and an untrusted_device result without the relevant capability now returns a terminal rejection instead of a bare null.

**Details**

- The rejection carries `reason: "request_rejected"` with status 403 and is marked terminal, so callers stop retrying.
- OAuth-rejected results are now passed through to the caller rather than swallowed.
- The enabled state of the surrounding path could not be resolved from the build.

**Evidence**

`{ terminal: !0, reason: "request_rejected", status: 403 }`

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

### Keybindings can be pre-warmed from the storage backend

Your custom keybindings can load in the background at startup so they're ready sooner.

**What**

Startup, after prefetch, can run an async warm read that populates keybindings from the storage backend ahead of first use, tracked by a `warmedFromBackend` flag.

**Details**

- the warm read suppresses feature events; the next synchronous read re-validates and discards the warmed value if customization is disabled, so a backend-loaded set does not survive the feature being turned off
- warming is skipped in non-interactive mode and when the feature is off
- a backend read failure during warming logs the telemetry reason `warm_backend_read_failed_fell_back`
- hot-reload and delete paths clear the flag
- gated on `tengu_keybinding_customization_release`, which falls back to true

**Evidence**

`warm_backend_read_failed_fell_back`, `warmedFromBackend`

- Flag `tengu_keybinding_customization_release`: Off by default, switched on for this account (read for one account on one subscription tier against v2.1.225; this account: on, anonymous baseline: on, compiled default: off) These values were read against a different version of Claude Code, so treat them as the nearest reading available instead of one taken on this release.
- Area: Terminal UI
- Names: `tengu_keybinding_customization_release`
- Tier: You'll notice
- Useful: 2/5
- Signal: 0/5

### Daemon auth cooldown and status can go through the v5 storage layer

Background daemon login cooldown and status can live in a storage service instead of local files.

**What**

The headless daemon's browser-login cooldown and auth-status records can now be read and written through the v5 storage client instead of files in the config dir.

**Details**

- both records gained state keys for the v5 layer
- active only when the supervisor is constructed with a storageV5 client
- dedicated failure reporting on that path, for example `daemon-auth v5 cooldown read failed`
- falls back to the `daemon-auth-cooldown` and `daemon-auth-status.json` files under the config dir when no client is passed

**Evidence**

`daemon-auth-cooldown`

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

### Transcript directory scans ask the storage backend to skip scope stats

Listing your past transcripts now skips some extra bookkeeping, which may make it a bit lighter.

**What**

Both transcript scope-listing loops now pass a flag to `listEntries` requesting that scope statistics be skipped.

**Details**

- the calls pass `{ skipScopeStats: !0, ... }` instead of only a cursor
- the flag appears at exactly these two call sites in the bundle, so the consumer is not visible and what the backend does with it could not be confirmed

**Evidence**

`skipScopeStats`

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

## Internal Changes

### Fewer shell-outs for environment probing

Docker detection and git identity lookups no longer spawn subprocesses, so startup is slightly lighter.

**What**

Docker detection and git identity lookups no longer spawn subprocesses, doing the same work in-process.

**Details**

- The Docker check no longer runs `test -f /.dockerenv` as a subprocess.
- Git identity no longer shells out to `git config --get user.email` or `user.name`.
- Both results stay memoised as before.

**Evidence**

`/.dockerenv`

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

### SDK spawn errors carry a telemetry class and redacted paths

Launch failure reports now scrub secrets out of file paths before sending them.

**What**

Executable launch failures and subprocess init timeouts now report a structured error class, and any path in the message is scrubbed for secrets before it is interpolated.

**Details**

- The helper producing "executable not found / failed to launch" strings now returns an object with `telemetryMessage` and `errorClass`, one of "executable_launch_failed" or "executable_not_found".
- Paths go through a new redaction helper applying the shared token patterns: Bearer, sk-ant-, AKIA, gh*_, JWT and others.
- Subprocess init timeouts become a TelemetrySafeError with errorClass "initialize_timeout"; the user-visible message is unchanged.

**Evidence**

`errorClass: "initialize_timeout"`

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

### Telemetry event schema drops repl_code and git_remote_url, adds MCP and auth fields

Usage reporting stops sending your repo remote URL and adds MCP server and tool names.

**What**

The protobuf-shaped event metadata changes which fields it carries, removing two code and repo identifiers and adding MCP identity plus reported account details.

**Details**

- Removed: `repl_code` and `git_remote_url`.
- Added: `mcp_server_name`, `mcp_tool_name`, an optional `event_metadata_vars` string, and a `client_reported_auth` submessage carrying `account_uuid` and `organization_uuid`.
- The experiment-exposure message separately gains `variation_key`, and `user_attributes` / `experiment_metadata` become optional, serializing as undefined rather than an empty string.

**Evidence**

`client_reported_auth`

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

### OTel export drops repl_code and git_remote_url, promotes MCP server and tool names

OpenTelemetry export no longer reports your git remote URL and now reports MCP server and tool names.

**What**

The OpenTelemetry-to-internal-event transform stops reporting two attributes and starts reporting MCP identity in their place.

**Details**

- No longer emits repl_code or git_remote_url.
- Emits `mcp_server_name` and `mcp_tool_name`, sourced from `_PROTO_mcp_server_name` and `_PROTO_mcp_tool_name`.
- `_PROTO_code` and `_PROTO_git_remote_url` are still stripped from additional_metadata, they are just no longer reported anywhere.

**Evidence**

`_PROTO_mcp_tool_name`

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

### Credential-store cache gained an unused failure slot

The login credential cache now has a slot to remember read failures, but nothing uses it yet.

**What**

The credentials cache carries a new field for recording a read failure, with nothing reading it yet.

**Details**

- `lastReadFailure: null` added to the cache object literal and to its reset routine
- sits alongside the existing `cache`, `generation` and `readInFlight` fields
- nothing else in the bundle reads or writes it, so there is no observable effect in this build

**Evidence**

`lastReadFailure`

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

### Session listing can enumerate from the storage backend

Resuming sessions can list them from a storage service instead of scanning folders on disk.

**What**

Resume-style session and transcript listing gained a backend path that pages through stored entries instead of scanning directories.

**Details**

- pages `listEntries` for the "transcript" namespace to enumerate project keys and session files
- falls back to the previous readdir-based scan when no backend is passed
- the old standalone directory-scanning helpers were removed

**Evidence**

`{ namespace: "transcript" }`

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

### SDK subprocess errors carry an error class and sanitized message

SDK subprocess failures are now grouped by cause with paths and stderr scrubbed from reports.

**What**

Errors from the SDK's subprocess transport are wrapped with a redacted `telemetryMessage` and an `errorClass` so failures can be grouped.

**Details**

- classes include `spawn_failed`, `process_exited_nonzero`, `process_killed_by_signal`, `executable_not_found` and `control_request_failed`
- covers spawn failures, non-zero exits, signal kills, aborts, control-request failures and error results
- stderr tails and filesystem paths are redacted before being attached

**Evidence**

`process_killed_by_signal`

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

### Org-memory discovery can be re-decided after an account change

Organization memory can be rediscovered after you switch accounts, and it refuses in untrusted folders.

**What**

New serialized entry points re-run org-memory discovery and report how the decision moved.

**Details**

- refuses to run in an untrusted workspace or when the account boundary changed, returning a refusal with a reason
- on success it clears the discovery cache, re-runs discovery and reports both the previous and current state

**Evidence**

`{ kind: "refused", reason: "untrusted_workspace" }`

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

### Task-notification wake routing runs on a timer

Background task notifications get checked on a repeating timer and delivered per agent without duplicates.

**What**

A wake router scans pending notifications on each query-loop tick and delivers them as meta prompts.

**Details**

- delivers task-notification prompts per agent, de-duplicated by `agentId`
- re-arms a 60s timer while agent notifications remain pending
- drops events for agents whose session has already ended

**Evidence**

`[wakeRouter] dropping `

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

### Tool description cache key gained a segment

Tool descriptions are cached separately depending on which tools are loaded, avoiding stale reuse.

**What**

Prepared tool descriptions are cached under a key that now varies on whether a particular tool is in the tool set.

**Details**

- an "S:" prefix segment is added when that tool is present, empty otherwise
- sessions with and without it no longer share cached descriptions

**Evidence**

`? "S:" : ""`

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

### Held peer messages record their hold cause on re-evaluation

Queued messages from other sessions now report why they are still being held.

**What**

Re-running the inbound policy over queued cross-session messages now reports why each still-held message is held.

**Details**

- the hold cause, which may have changed since the message arrived, is passed to a listener along with the current queue depth
- refused messages additionally fire a drop callback

**Evidence**

`[cross-session-inbound] dropped `

- Area: Cross-Session Messaging
- Tier: Under the hood
- Useful: 2/5
- Signal: 0/5

### Session env files can go through the storage abstraction

Session environment files can be read and written through a storage layer instead of directly on disk.

**What**

The hook-generated session environment loader and the cwd-env clearing routine can now read and write through a storage handle instead of the filesystem directly.

**Details**

- Both take an optional storage handle; when supplied, entries under `session-env` in `userConfigDir` are listed, read and written through that layer, paged with a cursor
- When no handle is passed, the existing direct `fs/promises` path is used
- `CLAUDE_ENV_FILE` handling is unchanged

**Evidence**

`Failed to clear cwd env files`

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

### SDK query context construction was consolidated

The non-interactive query path builds its context in one shared place, meant to behave identically.

**What**

The non-interactive query path no longer builds the same large context object inline in two places, which is intended to be behaviour-preserving.

**Details**

- Context is built through a shared factory called with `phase: "input"` and `phase: "query"`
- Model and thinking configuration come from a single resolver
- `systemPrompt` arrives pre-rendered from the prompt builder, including appendSystemPrompt and mcpClients
- Compact-boundary handling is delegated to `applyCompactBoundary` and `flushBeforeCompactBoundary`

**Evidence**

`applyCompactBoundary`

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

### Version bumped to 2.1.225

The version reported by /status and --version is now 2.1.225.

**What**

The embedded build constants advance everywhere they appear.

**Details**

- VERSION 2.1.224 to 2.1.225, BUILD_TIME 2026-08-07T19:37:58Z, and a new GIT_SHA.
- These are what /status, `claude --version`, the updater messages and the `claude-cli/…` user agent report.

**Evidence**

`VERSION: "2.1.225"`

- Area: Elsewhere
- Names: `/status`, `claude --version`
- Tier: You'll notice
- Useful: 2/5
- Signal: 0/5

### Secret-redaction patterns factored into shared lists

Secret-scrubbing rules live in one shared list so more paths can strip credentials consistently.

**What**

The credential-scrubbing regex table was split into two reusable arrays so more than one code path can apply the same rules.

**Details**

- One array covers Bearer, Basic, sk-ant, sk-, AKIA and ASIA forms.
- The other covers gh*_ tokens, xox*- tokens and JWTs.
- The existing transcript redactor splices both lists in.
- A new standalone helper applies all of them to any string, and is what the SDK error paths use for file paths.

**Evidence**

`/\bey[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g`

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

### Tool renderers can now supply a progress renderer

Artifact tools can now show live progress lines in the transcript while they work.

**What**

The tool UI override map for the artifact tool passes `renderToolUseProgressMessage` alongside `renderToolResultMessage`.

**Details**

- This is the path by which the publish-retry progress line reaches the transcript.

**Evidence**

`renderToolUseProgressMessage`

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

### Error telemetry message extraction hardened

A weird thrown error can no longer break error reporting on its way out.

**What**

Reading and augmenting error metadata for telemetry no longer risks throwing from a malformed thrown value.

**Details**

- `telemetryMessage` is read through a guarded helper that type-checks the property and swallows any throw.
- A companion helper copies missing fields onto an error only when it is a non-null extensible object, also wrapped in try/catch.
- Fields already present on the error are never overwritten.

**Evidence**

`telemetryMessage`

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

### Aborted transport operations report a consistent 'aborted' error class

Cancelling a connection is now recorded as an abort rather than a generic failure.

**What**

Aborts in the direct-connect transport are now classified as aborts in telemetry instead of appearing as generic failures.

**Details**

- the transport rejects with a helper that builds the abort error together with telemetry metadata, rather than constructing a bare error
- the resulting event carries `errorClass: "aborted"`

**Evidence**

`telemetryMessage: "Operation aborted"`

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

### Heuristic classifier summaries are emitted without a flag

Lightweight conversation summaries are now always generated instead of needing a flag.

**What**

Heuristic classifier summaries are now the unconditional fallback rather than needing their own flag turned on.

**Details**

- the selector previously returned "llm" under one flag, "heuristic" under a second flag, and null otherwise
- it now returns "heuristic" whenever the llm flag is off
- `tengu_classifier_summary_llm_emit` still selects "llm", fallback false
- `tengu_classifier_summary_heuristic_emit` no longer appears anywhere in the bundle

**Evidence**

`tengu_classifier_summary_llm_emit`

- Flag `tengu_classifier_summary_llm_emit`: On for this account, and not off by default (read for one account on one subscription tier against v2.1.225; this account: on, anonymous baseline: on, compiled default: on) These values were read against a different version of Claude Code, so treat them as the nearest reading available instead of one taken on this release.
- Flag `tengu_classifier_summary_heuristic_emit`: On for this account, and not off by default (read for one account on one subscription tier against v2.1.225; this account: on, anonymous baseline: on, compiled default: not a boolean we can read) These values were read against a different version of Claude Code, so treat them as the nearest reading available instead of one taken on this release.
- Area: Telemetry
- Tier: Under the hood
- Useful: 2/5
- Signal: 0/5

### Classifier refusals are tagged separately in blocking telemetry

Safety-classifier refusals are now logged distinctly from ordinary parse failures.

**What**

The two-stage prompt-injection and safeguard classifier now records when the model stopped with a refusal, which previously looked identical to an ordinary parse failure.

**Details**

- The parse-failure event now carries `stopReason` plus `failureKind: "policy_refusal"`.
- The block result itself carries `refusedBySafeguard`.

**Evidence**

`refusedBySafeguard`

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

### Daemon auth cooldown and status write through the v5 backend

Daemon login cooldown and status writes go through the storage backend and now surface errors instead of hiding them.

**What**

The browser-login cooldown timestamp and the `auth_required` status file go through the storage backend when one is available instead of direct file IO.

**Details**

- writes use a umask-derived file mode
- backend failures raise explicit errors carrying telemetry codes rather than being swallowed

**Evidence**

`daemon-auth v5 cooldown write failed`

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

### Transcript recording and eviction moved into one recorder

Transcript saving and cleanup logic is consolidated in one place; nothing changes for you.

**What**

The query loop's inline transcript bookkeeping is now a single factory with named operations rather than scattered inline logic.

**Details**

- covers incremental persistence, tombstone eviction, compact-boundary handling and the unanchored preserved-tail flush
- the telemetry event for preserved-but-unanchored messages is unchanged

**Evidence**

`tengu_compact_preserved_unanchored`

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

### Bundled command registration split apart

Built-in command setup was split into separate build and register steps.

**What**

The bundled-command builder no longer constructs and registers a command object inline; registration moved to a dedicated function.

**Details**

- The old helper now just returns `{ skillRoot, getPromptForCommand }`.
- A reset helper clears the bundled command lists and the kill-switch-survivor set tracked by `survivesBundledKillSwitch`.
- A batched file-writing helper with `tolerateExisting` and verify-content semantics was added alongside.

**Evidence**

`survivesBundledKillSwitch`

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

### Experiment-exposure telemetry gains variation_key and stops padding empty strings

Experiment tracking records which variation you got and omits blank optional fields.

**What**

Experiment events now record which variation was seen, and absent optional fields are omitted rather than sent as empty strings.

**Details**

- Adds a `variation_key` field to the experiment event schema.
- `user_attributes` and `experiment_metadata` default to undefined instead of "".
- The same treatment applies to the generic event's `process` and `additional_metadata` fields.

**Evidence**

`variation_key`

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

### Bridge teardown distinguishes a skipped re-mint loop from a skipped teleport

Session teardown reporting now distinguishes two different kinds of skipped cleanup.

**What**

The bridge teardown telemetry event can now say which of two skips happened when a session is torn down.

**Details**

- `tengu_bridge_repl_teardown` gains `archive_status` value `skipped_remint_loop`, alongside the existing `skipped_teleport`.
- Reported when the teardown reason is something other than `remote_control_disabled`.
- The value is new in this build.

**Evidence**

`skipped_remint_loop`

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

### Account identity readable without a full config load

Your account details can now be read straight from the config file when credentials refresh mid-session.

**What**

A new async helper pulls the account identifiers directly off the config file instead of going through the synchronous cached config.

**Details**

- Reads `oauthAccount` for `accountUuid` and `organizationUuid`.
- Returns undefined on any parse or type failure rather than throwing.
- Used by the OAuth refresh path and by the bridge hook when refreshing credentials mid-session.

**Evidence**

`"oauthAccount" in t`

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

### Protobuf and CEL runtime restructured into CommonJS module exports

Bundled protobuf and expression-evaluator code was repackaged, with no change you can see.

**What**

The bundled protobuf descriptor and CEL evaluator code moved from module-scope variables to explicit exported members.

**Details**

- Edition and SymbolVisibility enums are now published with matching `*Schema` descriptors.
- CEL conversions go through exported `toCel`, `celError` and `isCelUint` helpers.
- No behaviour change is visible from the diff; it reads as a bundling or module-format change.

**Evidence**

`SymbolVisibilitySchema`

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

### Bundle module-initialization graph reshuffled around the coordinator user-context module

Startup module loading was reshuffled internally, with nothing visible changing for you.

**What**

A large module-init block was emptied down to a single import of `getCoordinatorUserContext`, and a new init block elsewhere now pulls in the roughly 45 modules, plus `require("crypto")`, that used to live there.

**Details**

- Bundling and lazy-init layout only; no behaviour visible to a user changes.

**Evidence**

`getCoordinatorUserContext`

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

### Replay harness reports exhausted cached calls

Internal replay testing now says which tool ran out of cached calls before failing.

**What**

A replayed tool call that runs past the end of the cached call list now records the mismatch before throwing.

**Details**

- The message is assembled from the position, tool name and count, ending in "... invoked but only N calls cached (exhausted)".

**Evidence**

` calls cached (exhausted)`

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

### Docker detection consolidated behind one helper

Detecting whether you're inside Docker now happens through one shared helper.

**What**

The runtime-environment classifier that returns "docker" now goes through a shared helper rather than inlining its own caching.

**Details**

- Replaces the inline `cached ?? probe()` pattern with a helper wrapping it.
- The same detection is now shared with other callers.

**Evidence**

`kubernetes`

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

### Bash command patterns and transcript markers moved modules

Test and typecheck command patterns moved between modules without changing what they match.

**What**

Several regex sets and constants were relocated between lazy-init modules with no change to their behaviour.

**Details**

- Test runner patterns (bun/npm/yarn/pnpm/deno test, go/cargo/make/... test, pytest/jest/vitest/rspec/phpunit/ctest) moved.
- Typecheck patterns (tsc, mypy) moved.
- The transcript marker strings and the opening-tag regex moved.
- The patterns themselves are unchanged.

**Evidence**

`(?:bun|npm|yarn|pnpm)\s+run\s+typecheck\b`

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

### Spinner control moved behind an event emitter

The spinner's text and colours are now updated through a shared event channel.

**What**

Spinner text and colour updates go through a shared event emitter instead of calling into the render layer directly.

**Details**

- a new module exposes `setSpinnerMessage` and `setSpinnerColors`
- these emit `main-message` and `main-colors` events on a shared emitter
- a handler switch on the receiving side applies them

**Evidence**

`kind: "main-colors"`

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

### Classifier failure telemetry gained a stopReason field

Failure reports for the classifier now include why generation stopped.

**What**

Classifier failure events now report why generation stopped.

**Details**

- `stopReason` is added to the payload alongside `classifierModel`, `classifierType` and `failureKind`
- the optional `errorKind` and `fallbackFrom` fields are unchanged

**Evidence**

`stopReason`

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