# Unity CLI v1.0.0-beta.10

### Security

- `unity mcp configure` no longer follows a symlinked path component when writing a client's config file. A `--local` config write (and vscode/vscode-insiders, whose config is always project-local) is now confined to the project directory: a repo-supplied symlink at any path component — the final config file or an intermediate directory such as a symlinked `.kiro/settings` — is refused outright rather than silently followed, closing an arbitrary-file-overwrite gap a malicious cloned repo could otherwise trigger. Every client config write (local and global) is also now atomic — written to a same-directory temp file created exclusively, then renamed into place — so a crash, a kill, a full disk, or two concurrent invocations can no longer leave a config file the CLI only partly owns truncated or torn.

- On macOS, the CLI no longer ad-hoc re-signs a downloaded plugin or component whose code signature fails to verify. Only Unity Version Control's `cm` client, which its publisher ships unsigned, keeps that fallback, and the CLI now logs when it fires. Any other component that fails verification now fails its acquisition instead of running under an anonymous signature.

- On Linux, a `.deb`/`.rpm` install of the Linux hardened auth-broker tier now actually enforces it. The dedicated `unity-broker` system account, its isolated `/var/lib/unity-auth-broker` store, and polkit consent were installable and doctor-reportable since the previous release but not yet load-bearing: the resident broker did not open that store or check consent, and no CLI process resolved the hardened tier's system socket, so the packaging had no effect on a real invocation. Both are now wired up — every `auth.getToken` grant against the hardened tier is gated on a real polkit consent check (cached per consumer, TTL-bound) before the isolated store is ever touched, and the CLI now connects to the hardened tier's socket automatically once it is provisioned. The tarball/per-user tier is unaffected.

- `unity mcp configure <client> --dry-run` no longer prints your whole MCP config file. It used to print the entire existing document merged with the change it was about to make — and an MCP config file is exactly where per-server credentials live, in each server's `env` block (API tokens, keys). `--dry-run` now previews only the entry it is adding or changing, the same way it always has for Codex's TOML config. This changes the preview: you no longer see the full resulting file (comment placement, unrelated keys and all) before confirming — only what this command itself will add or change. The file it actually writes is unaffected.

- `unity mcp configure codex` no longer risks corrupting an existing `config.toml` when it updates the Unity MCP server entry. Its line-based match for the `[mcp_servers.unity]` table could run past a multi-line string's closing `"""` and delete unrelated settings after it, stop early inside a multi-line array and leave a dangling `[`, or fail to recognize a quoted table name (`[mcp_servers."unity"]`) and append a duplicate table that breaks the whole file. The command now refuses the update and reports the file needs a manual edit instead of touching it whenever it cannot be sure the match is safe, and a quoted table name is now recognized and updated in place rather than duplicated.

- `unity mcp configure codex` no longer risks corrupting the sandbox and network-proxy settings it relaxes so Codex can reach the Unity Editor's Pipeline server. The same line-based matching this command uses for the Unity MCP server table also located `[sandbox_workspace_write]`, `[features.network_proxy]`, and `[features.network_proxy.domains]`, and could be hijacked the same two ways: a table-header-shaped line inside a multi-line string, or a multi-line array whose continuation line starts with `[`. The command now refuses the whole update and reports the file needs a manual edit instead of silently mangling it whenever it cannot be sure the match is safe.

- The resident auth broker now signs every refresh-token request with a DPoP (RFC 9449) proof bound to a key it generates and holds only in memory, and correctly handles the server's nonce challenge and retry. This is the client half of a protocol that makes a copied token store worthless on another machine; the identity platform does not validate these proofs yet, so this does not change what a copied token store can do today. Nothing about sign-in, token requests, or existing sessions changes observably.

- The resident auth broker's DPoP signing key is now hardware-backed where the machine supports it: a non-exportable key inside the TPM via the CNG Platform Crypto Provider on Windows, a Secure Enclave-generated key on macOS, and a TPM2-backed key via `tpm2-tools` on Linux. Where none of those is available, the broker falls back to the same in-memory key as before. The active tier is recorded in telemetry. This key was already never written to disk in plaintext; hardware-backing additionally means the private key material never exists outside the TPM/Enclave at all, so a full process memory dump on a supported machine yields nothing usable. Nothing about sign-in, token requests, or existing sessions changes observably, and the identity platform still does not validate DPoP proofs yet, so this does not change what a copied token store can do today.

- `unity projects link vcs` no longer lets a project directory choose which program answers its credential lookup. Linking a repository, and the cleanup `--delete-remote` runs on a partially-finished one, used to run `git credential fill` with the project directory as that command's own working directory, which put a repo-local `credential.helper` (and, interactively, `core.askPass`) in scope — a hostile or attacker-planted `.git/config` in an extracted archive or downloaded starter project could substitute its own credential for the one you actually meant to authenticate with, the same class of finding HUB-7103 measured and fixed in Unity Hub. The project's recorded identity (`credential.username`, and its URL-scoped form) is now read as inert configuration data instead, and reapplied to a lookup that always runs from a neutral directory, so only your trusted organization- or global-scoped credential helper chain ever answers it. A project's own recorded identity still resolves the same login it always has; only a repo-local `credential.helper` *program* stops being honored. `git init`/`commit`/`push` and any hooks they run are unaffected.

- The resident auth broker's token store is now sealed to hardware where the machine offers it — a non-exportable TPM key via Windows' CNG Platform Crypto Provider, or Linux TPM2 via `systemd-creds` — falling back through DPAPI, the Linux Secret Service, the macOS Keychain, and finally a local key file, in that order. CLI-809 built this hardware-sealed store and its fallback chain some time ago, fully tested, but the live broker kept opening the plain OS keyring directly; this closes that gap so a real sign-in actually seals through it. The practical effect: the token store's at-rest blob becomes worthless off the machine that wrote it — copying the whole store directory to another machine, a backup, or a support bundle no longer yields a usable credential, because the machine binding is authenticated cryptographically (an AES-GCM tag) rather than merely compared, so there is no string an attacker could edit around. This does not stop malware already running as you on this machine, which can ask the broker for a live token over the same local connection any other process on your account already could — see `apps/cli/docs/cli-809-hardware-sealed-store.md` for the full threat model. One behavior change worth knowing: every signed-in identity's tokens now live in one sealed file rather than one independently-encrypted OS keyring entry each, so an event that makes the file unreadable (a cleared TPM, a motherboard swap) signs every identity out together rather than one at a time — each simply signs in again. `auth.hello`'s `sealTier`/`sealHardwareBacked` fields, previously always null, now report the real tier once a token operation has actually opened the store; they are informational only today and gate nothing.

- The resident auth broker now makes itself a poorer target for a memory dump the moment it starts, before binding its socket or touching any credential: on Linux it refuses a same-uid `ptrace` attach and `/proc/<pid>/mem` read and disables core dumps outright (`prctl(PR_SET_DUMPABLE)`/`RLIMIT_CORE`); on macOS it disables core dumps; on Windows it suppresses the crash-dialog (not Windows Error Reporting itself, which can still write a dump where `LocalDumps` is configured). It also now holds the refresh token it reads from the credential store — and, underneath that, the whole encrypted credential map the CLI-809 hardware-sealed store reads and writes — in pinned, locked, zeroize-on-release memory for as short a span as it can, rather than as an ordinary string for its whole idle lifetime. CLI-810 built and tested these primitives months ago, but they had no caller anywhere in the live broker; this closes that gap. None of this changes any command's behavior, output, or exit code — see `apps/cli/docs/cli-810-secret-memory-hygiene.md` for the full threat model and, just as importantly, what a managed runtime honestly cannot close.

### Changed

- `unity auth consumers` now reflects real activity: every token grant and refusal the resident auth broker's token-vending path reaches is recorded to the audit trail this command reads. Both `auth consumers` and `auth revoke` have existed since `1.0.0-beta.9`, but until now nothing populated the trail `auth consumers` reads from, so it always reported an empty list regardless of how much the broker had actually served. One thing does NOT change here and is worth stating plainly: `auth revoke` still only writes to the revocation list `auth consumers` displays — no live broker grant path yet consults it, so revoking an application does not (yet) prevent that application from being granted a token again. Nothing about redaction or the five anomaly signals changes; they were designed and tested in `1.0.0-beta.9` and are only now actually seeing broker traffic.

- `unity command`/`unity cmd` now renders `recompile`, `recompile_status`, `test_status` and `run_tests` results as short, readable plain text in the default (TSV) Result column, instead of a compact-JSON blob crammed into one cell. These four are the only async, polled commands in the whole pipeline catalog, so they are also the ones an edit-compile-test loop calls most. Every other command's Result cell is unchanged — this is a per-command, opt-in formatter, not a generic conversion; a generic line-flattening rewrite was measured 1.2-1.8x *larger* than the JSON it would replace on a deeply nested result, so it was deliberately not built. `--format json`/`ndjson`/`human` output, `unity eval`'s TSV output, and every command without a dedicated formatter, are byte-for-byte unchanged (`unity eval` shares the same lookup mechanism internally, but has no dedicated formatter registered, since an eval expression's return value has no fixed shape to format against).

- Every command's OAuth token read now goes through the resident auth broker instead of reading the OS credential store directly, and — new behavior worth calling out on its own — a command that needs a token starts the broker on demand if one is not already running, exactly like `unity open` has always started the Editor identity daemon on demand. Commands that read session state (`config list`, `cloud status`, `license status`, and others) can now leave a `unity --internal-auth-broker-serve` process running in the background for up to two minutes of inactivity before it exits on its own; it holds no more than the same OAuth credentials this process already read directly before this change, and it exits on its own with no user action needed. `UNITY_NO_AUTH_BROKER` disables this and restores the previous direct-keyring behavior for anyone who needs to opt out. Whether every read-only/session-checking command SHOULD be the thing that starts a resident daemon this early in the broker rollout, versus gating the spawn on a narrower set of commands, is an open design question for the epic owner — see `apps/cli/docs/auth-broker-fallback-policy.md`'s note on it — and may change in a follow-up before this is more widely relied upon.

- `unity command <name>` (foreground and `--detach`) and `unity run --command <name>` no longer hard-refuse a bare, argument-free invocation against an Editor whose `com.unity.pipeline` predates argv/commandLine binding. Previously any invocation against such a peer failed outright with "This Editor's Unity Pipeline package is too old to parse command lines," even for a command that takes no parameters and would have run fine. A bare command name has nothing that needs binding, so it now retries once against the pre-argv request shape those peers still understand and runs normally. An invocation that does carry arguments still refuses with the same message and remediation as before — binding those without the Editor's schema is exactly the class of silent-wrong-parameter bug the argv change was built to eliminate, so that case is intentionally left alone.

- `unity self-update` now downloads through the CLI's real download engine instead of a bespoke one-shot fetch, and shows a transfer rate alongside the percent and byte counts while it downloads. The practical change: an interrupted download — a dropped connection, a closed laptop lid, a `Ctrl+C` — resumes from where it left off on the next `unity self-update`, rather than starting over from zero. The download persists under the CLI's download-cache directory (honoring a configured custom download location) keyed by the target version and its expected checksum, so a resume can never pick up bytes for the wrong build; a corrupted or mismatched partial always falls back to a fresh download rather than a bad one. Retry count and backoff, sha256 verification, and proxy support are unchanged.

- `unity self-update` now prefers a Brotli-compressed download when the release publishes one — about two-thirds smaller than the raw binary, measured on the shipped darwin-arm64 build. The compressed bytes are verified against their own checksum as they arrive, then decompressed, and the resulting binary is verified again against the same checksum that has always guarded it — that second check is unchanged and stays the one thing the exec preflight and self-replace ultimately trust. Determinate progress now reports against the smaller, compressed transfer size. A release that has not yet published a compressed artifact for your platform, or an older manifest, falls back to the raw download exactly as before.

- A mistyped command or flag now says the agent skill exists. `unity frobnicate` and `unity --frobnicate` already tried to guess what you meant; when there is no close match to offer, they now add a line pointing at `unity skill show` for a task-oriented guide to the CLI's commands and flags. The same line follows a mistyped SUBcommand anywhere in the CLI — `unity auth frobnicate`, `unity vcs frobnicate`, `unity config frobnicate` and the rest — not just at the top level, since a wrong guess one level down leaves you exactly as stuck. That error is where a person — or an AI agent — has just guessed wrong and has nothing left but reading `--help`, which is 248 separate screens, so it is the one place the pointer saves the most work. It appears only when no "Did you mean" suggestion fired: a plain typo like `unity editrs` already has its answer and gets no extra line. It goes to standard error alongside the message it explains, and never under `--format json` or `--format ndjson`, so machine-readable output is unchanged. Exit codes are unchanged. The line ships in all ten languages.

- `unity command <name> --project-path <path>` pays less fixed CLI-internal overhead before it reaches the connected Editor. Two costs ran on every invocation regardless of whether they were needed: the OS display-language detector (which loads CoreFoundation via `NativeLibrary.Load` on every macOS call) ran even when a valid language preference was already persisted — the common case for any returning user — and the full ~140-command help tree was built even when the invocation could never dispatch to help. Both are now skipped unless actually needed. `--project-path`'s own resolution was already a direct descriptor read with no auto-discovery candidate enumeration; this closes out the remaining fixed per-invocation cost around it.

- The resident daemon that answers the Unity Editor's sign-in requests, and the auth broker, now verify a connecting peer's code signature before serving it — on macOS and Windows, by default, they refuse to serve an unsigned or non-Unity-signed local process, where previously any local process on the machine could connect. Until now this check existed but only ever logged what it would have refused; it now actually closes the connection. In the overwhelmingly common case (a real, Hub-installed Unity Editor or another Unity binary) this changes nothing observable. It can affect an unsigned or locally-built Editor (for example one built from Unity's own source, as some engine developers and advanced users do): that Editor no longer becomes signed in automatically through this channel. If that happens, `UNITY_PEER_AUTH_MODE=identify-only` restores the previous logging-only behavior for the affected machine while a broader fix is worked out. Linux is unaffected by this change: it has no code-signing mechanism to check against, so the daemon there continues to log only by default; an operator who wants to opt a Linux machine into enforcement anyway can set `UNITY_PEER_AUTH_MODE=enforce` together with `UNITY_PEER_AUTH_LINUX_ALLOWED_HASHES` (a comma-separated allowlist of trusted executable SHA-256 hashes).

### Added

- The resident auth broker gains `auth.adoptCredential`, an internal method letting an already-verified Hub process (the Electron Hub or the native `hub-gpui` Hub — never the CLI itself, and never an unverified caller) hand the broker a credential it already owns. A Hub-created keyring item and a CLI-created one for the same account now converge into one broker-owned credential automatically, with no extra sign-in prompt. The broker refuses the call with the same error an entirely unverified peer gets unless the caller verifies as the Hub product, and a credential the broker already holds for that account is left untouched rather than overwritten. This has no command-line surface of its own; both Hubs call it automatically right after a direct sign-in completes.

- `unity skill install` now also installs the agent skills that installed plugins ship. A plugin registry entry can declare, in its compiled table, where inside its payload an agent skill lives (`skills/unity-ai/` for the Unity AI Generators plugin, from its `0.1.0-alpha.3`); `unity skill install <client>` mirrors every such skill of an installed plugin beside the `unity-cli` skill, in the same scope as that install, exactly as it already mirrors the skill `com.unity.pipeline` ships in its package. `unity skill refresh` re-renders them from the plugin's current managed copy, so a `unity plugin upgrade` reaches the mirrored skill on the next refresh, and reports a skill whose plugin is no longer installed rather than deleting it. The skills go through the same ownership rules as the CLI's own skill: a directory no ledger row claims is refused without `--force`. `unity plugin install <id>` and `unity plugin upgrade [<id>]` end with a one-line pointer at `unity skill install` when the copy they just installed ships a skill (upgrade matters as much as install: a plugin can gain its skill in a later version, and there is no mirrored copy for `skill refresh` to find until it is installed once); neither writes into an AI client's directory itself. The declared path is compiled and never read from the remote registry document, for the same reason `pathNames` is not: it decides where the CLI writes.

- `unity context` — named bundles of the five defaults you otherwise set one command at a time. The CLI keeps an active account, a default organization, a default cloud project, a default editor and an install path, each set by its own command (`auth switch`, `cloud org set-default`, `cloud project set-default`, `editors default`, `install-path`). Moving between two setups — a work organization and a personal one, two clients — meant running five commands in the right order, and nothing showed which combination was currently in effect; getting one wrong was silent until a later `unity build` or `unity open` ran against the wrong organization or editor. `unity context save <name>` records all five, `unity context use <name>` applies them, and `unity context list` / `unity context current` show what exists and which one is active. Applying is all-or-nothing: the account, editor and install path are checked before anything is written, the organization and project are checked against Unity Cloud under the account the context selects, and any failure names the missing item and leaves every setting as it was — a context pinning an editor you have since uninstalled fails with that editor's version rather than half-switching. A context also **clears** the settings it does not pin, so switching to one that names no organization does not leave the previous one's behind; the single exception is the account, which has no "signed in as nobody" state and is therefore left alone when a context names none. When Unity Cloud cannot be reached — offline, or not signed in — the stored organization and project are applied as-is with a warning saying they were not verified, rather than refusing to switch at all. All five output formats are supported, and the `setting` values are stable machine tokens (`account`, `organization`, `project`, `editor`, `install-path`) in every format, human included, so a script can key on them; only `unity context list`'s column headers are translated. Unrelated to `unity build --profile`, which names a Unity Build Profile asset; inside `unity shell`, bare `context` still shows that session's own ephemeral selection and `context <subcommand>` reaches this group.

- `unity build --create-profile <target>` creates a Unity 6+ Build Profile asset for a target platform (e.g. `WebGL`, `Android`) and exits — no build happens. Previously the only way to build a fresh project for a platform with no Build Profile yet was `--target`/`--execute-method` mode, which works but never creates a profile asset, so opening the project in the Editor afterward showed no Build Profiles at all despite having built for those platforms. Build with the new profile in a separate `unity build --profile <target>` invocation. Requires Unity 6000.0 or newer, same as `--profile`. See `apps/cli/docs/build-profile-create.md`.

- `unity commands` — a machine-readable manifest of the CLI's own command tree. `unity commands --format json` emits every command, subcommand, positional argument, and option (recursively, with aliases, descriptions, value placeholders, and defaults) in the standard success envelope, so agents and tooling can discover what the binary can do without walking `--help` level by level and parsing prose; `unity commands` (no flag) prints a compact grouped listing. Hidden and dev-only surfaces are excluded, the same visibility rule `--help` uses. See `apps/cli/docs/json-output.md`.

- Unity Accelerator support. Unity's asset-import cache server can now be configured once instead of retyped into every invocation: `unity config accelerator <host:port>` persists an endpoint (a bare host defaults to port 10080), and `unity run`, `unity test` and `unity build` inject `-EnableCacheServer -cacheServerEndpoint` into the Editor automatically. On those three commands, `--accelerator <host:port>` and `--no-accelerator` override or suppress it for one invocation, and `UNITY_ACCELERATOR` sits between the two — the precedence is flag, then env var, then the persisted setting. When nothing is configured the Editor command line is unchanged, so this costs projects that do not use an Accelerator nothing. The Editor flags themselves always worked when forwarded by hand; what was missing was somewhere to keep the endpoint and any way to tell a working cache server from a broken one, since the two differ only in wall-clock time. So `unity diagnose accelerator` reports the resolved endpoint and where it came from, the project's own cache-server settings, and whether the endpoint answers — it does not claim a project's cache-server mode can make that endpoint moot, since no mode does (the same CLI-1045 measurement below); `unity doctor` gains an Accelerator section; and `unity doctor --ci` probes reachability as a non-blocking preflight row, which can never fail a pipeline for a project that does not use one. Only the two flags the CLI manages are reserved; the tuning family (`-cacheServerEnableImportResultCaching`, `-cacheServerNamespacePrefix`, the upload/download and shader and texture cache switches) stays forwardable after `--`. Note that import result caching is disabled by default from Unity 6.5 in new projects. A project's own `m_CacheServerMode` (including Disabled) never voids these injected flags either — a live-Editor measurement (CLI-1045) found all three modes connect and take real cache hits — so neither `run`/`test`/`build`'s injection nor `diagnose accelerator`'s report claims otherwise. See `apps/cli/docs/accelerator.md`.

- `unity vcs blame <scene|prefab>` reports who last changed each GameObject and component in a Unity asset, with the commit, author and date. `git blame` on a serialized scene answers "who last touched line 4,812" — the file is ordered by serialization rather than by structure, everything in it is a numeric id, and a reserialization rewrites lines nobody meaningfully edited — so "who changed the Player's Rigidbody" has until now needed archaeology. `--object <name>` narrows to one object by name or hierarchy path, and to the components hanging off it; `--field <name>` narrows to the last commit that changed one serialized field; `--at <ref>` blames as of another revision; `--max-revisions <n>` bounds how far back the walk looks. Identity is matched with the same three-tier matcher `unity vcs diff` uses, and every row reports the weakest match it rests on — an attribution reached across a reserialization says it is uncertain rather than naming an author confidently. A walk stopped by its bound reports the affected objects as unknown rather than blaming the oldest commit it happened to see, and a binary-serialized or non-Unity asset is reported as such rather than as an empty result.

- `unity eval --validate` lands the CLI-side surface for a dry-compile mode: compile an expression and return diagnostics without executing it, so a code-generating agent could catch a compile error in the same round trip instead of discovering it only after other side effects have already happened. No published Unity Pipeline package can currently tell the CLI "compiled, but never executed" versus "executed and happened to succeed" — an older package would just silently ignore the request and run the code for real — so `--validate` refuses unconditionally today, before contacting the Editor at all, rather than risk running code it was asked not to. It starts working once a Unity Pipeline release adds the acknowledgement this needs. `--validate` cannot be combined with `--detach`.

- `unity open --wait` (also `unity projects open --wait` and `unity projects upgrade --wait`) blocks until the Editor it launched actually exits, and reports its real outcome, instead of returning as soon as the hand-off completes. Until now the CLI only watched the Editor briefly after launching it — long enough to catch an instant failure, not a real crash or a licensing timeout, which routinely take longer than that to surface — so most genuine failures still reported success. `--wait` removes that limit: the command blocks for as long as the Editor runs and exits 6 (`OPEN_EDITOR_EXITED`, or the existing licensing diagnosis for a 198) if it fails, or 0 if it exits cleanly. The Editor runs in its own process group while `--wait` is active, so pressing Ctrl-C stops the CLI from watching but never touches the Editor — there is no partial-cancel: every Ctrl-C is absorbed and the wait always runs to completion, because a deterministic exit code is the entire point of the flag. `unity projects create --open` and `unity projects new --open` do not gain `--wait`: those two already report a failed open as a warning rather than fail the command, and mixing that with a flag whose job is a deterministic verdict would not make sense. macOS and Linux only for now — `--wait` is refused on Windows (exit 2) rather than silently falling back to the old bounded watch, since giving a Windows CI script false confidence in a guarantee it does not have would be worse than an honest refusal; Windows support is tracked separately. A clean exit still reports success; a crash (a signal death, such as `SIGSEGV`) is now reported as a distinct, named failure too — see the entry below — so this closes the "the CLI can only report an Editor that fails while it is still watching" limit noted above for late *failures* as completely as the observation window allows.

- `unity open`, `unity projects open`, and `unity projects upgrade` now say when the Editor they launched was killed by a signal, instead of silently reporting success — both in the ordinary bounded watch and under `--wait`. A segfaulting Editor used to report nothing at all and the command exited 0, indistinguishable from a clean exit; it now fails with "The Editor started and then stopped with signal SIGSEGV, so the project was not opened," exit 6, machine code `OPEN_EDITOR_EXITED_SIGNAL`. This is the exact treatment `unity build`/`unity test`/`unity run` already give a signal-killed Editor (see the `1.0.0-beta.6` entry further below); `open` now matches it. An Editor that exits normally with a non-zero code is unaffected and still reports that code under `OPEN_EDITOR_EXITED`.

- `unity command <name>`/`unity eval` gain `--result-only`, a slim json mode for agent/CI consumers: it prints the com.unity.pipeline server's own `result` value alone, parsed to a real JSON value, with none of the CLI's added `command`/`parameters`/`target`/`success`/`errors`/`warnings` envelope around it. Measured on `recompile_status`, a 47-byte raw result, the existing `--format json` envelope adds 372 bytes of pure client-side overhead around it — 95% of the bytes. `--result-only` is additive: plain `--format json` is unchanged, and the new flag implies json output on its own (same precedence tier as `--json`), so it needs no separate `--format json`. It cannot be combined with `--detach`, which has no synchronous result to print. See `apps/cli/docs/output-formats.md`.

- `unity mcp`'s `capture_game_view` / `capture_scene_view` tools now fall back to an OS-level screenshot of the whole desktop when the Editor's main thread doesn't answer in time — the scenario this exists for is a blocking modal dialog, which is exactly the moment a screenshot is most useful for diagnosis, and exactly the moment the Editor-mediated capture can't produce one (the same main thread the capture needs to render on is the one the dialog is blocking). The fallback captures independently of the Editor process — CoreGraphics on macOS, GDI on Windows, Xlib on Linux (X11 sessions only; a Wayland-only session reports that plainly rather than attempting a portal round trip) — so it still returns an image when the Editor can't be reached at all. It is a deliberately partial answer: it captures the whole screen, not the specific camera or Scene View the tool call asked for, and says so in an accompanying text note (naming the blocking dialog when the Editor's own `/api/dialog` probe or busy-gate found one). The precise fix — the Editor rendering its own capture off a background thread, the way `com.unity.pipeline`'s `DialogStateTracker` already answers `/api/dialog` off the main thread — needs a change in that package, a different repository; see CLI-1118.

- `unity assets inspect <file.unitypackage>` lists what a `.unitypackage` contains without importing it — offline, with no Editor installed and no open project. For each entry it reports the asset path, its GUID, the payload size, and whether a preview image is bundled, and it prints a summary of the entry count and total size. The archive is streamed rather than read into memory, so a multi-gigabyte package is inspected in constant memory. `--format json`/`tsv`/`ndjson` carry the raw byte size and a boolean preview flag for scripts, while the default human table shows readable sizes. Asset paths come from the archive and are sanitized before they reach the terminal. A missing, corrupt, truncated, or non-package file fails with a structured error and a nonzero exit. See `apps/cli/docs/assets.md`.

- `unity build --list-targets` lists every valid `--target` value and exits, so finding the right string for a platform — WebGL, Android, iOS — no longer means triggering the invalid-target error on purpose or hunting through `--help`'s handful of examples. Each target is flagged zero-code (no `--execute-method` needed, using the legacy desktop player flags — `--output-path` is still required) or as needing `--execute-method` (or a Unity 6+ build profile via `--profile` instead) — the distinction that confused the original cross-platform reports this closes out. Honors `--format` (`human`/`tsv`/`json`/`ndjson`) for scripts and CI, and is sourced from the same target catalog `--target` itself validates against, so there is no second list to drift.

- `unity build --list-profiles` lists a project's Build Profile assets (name and path) and exits — the discovery command for platforms that build only through `--profile`, most notably partner and package-provided platforms (Meta Quest, for example) that have no entry in the `--target` catalog at all. A customer question in Slack showed this path was supported but not discoverable: `unity build --help` and the docs never said Build Profiles are how you reach those platforms, and the invalid-target error for one of them read as "unsupported" rather than "use the other flag." Both are fixed here too — `--help`'s examples and its closing note now spell out the Build Profile path explicitly, and `BUILD_INVALID_TARGET` now redirects to `--profile` for a target outside the classic catalog. `--list-profiles` is sourced from the same `Assets/Settings/Build Profiles` folder `--profile` itself resolves against, so there is no second list to drift, and honors `--format` (`human`/`tsv`/`json`/`ndjson`) like `--list-targets`.

- `unity license status` now reports the two things you need to diagnose a floating-license seat that looks stuck: `floatingServer`, the licensing server this machine is configured against, and `machineId`, the identity the licensing client reports for it — the value to match against a lease record on the server, whose entries carry the holder's hostname and user. Both appear in `--json` and `--ndjson`, appended after the existing keys so nothing already there moves, and in human and `tsv` output only when a floating server is configured, so a personal or serial license gains no new lines. When the licensing client does not respond, `license status` fails as it always has and its error envelope carries no payload, so neither field appears there — but a configured floating server is named on standard error beside the failure, because that value comes from local configuration rather than from the client that just failed to answer. That state is itself a finding, since a machine that cannot reach its own licensing client is not renewing any lease. The background this closes out: a floating seat is a server-side lease that the licensing client renews and the server reclaims once renewals stop, so a CI agent destroyed mid-job frees its seat on the order of the server's `floatingLeaseRenewIntervalInMinutes` (15 minutes by default) rather than holding it until an administrator intervenes — but nothing in the CLI let you confirm which machine was holding what. `docs/cli/headless-licensing.md` gains a section on floating seats for ephemeral and containerized agents, covering the lease model, the two server timers that are easy to confuse, the teardown step (which `unity ci init` already generates for both providers), and how to tell an exhausted seat pool apart from one machine holding a seat it should have returned.

- `unity build run` launches the project's most recent successful build without rebuilding — iterating on a build (checking a screen, timing a level, handing a WebGL build to a tester) no longer means re-running the whole `unity build` just to relaunch what was already produced. Every successful `unity build` now records its output path, target, architecture, and editor version inside the project's own `Library/UnityCli/last-build.json` (gitignored, machine-local, mirroring how `com.unity.pipeline` already colocates its own port descriptor under `Library/`); `unity build run` reads that record and launches accordingly, with `--path <path>` to launch a different build output instead. A desktop standalone player (`.exe` on Windows, `.app` on macOS, an ELF binary on Linux) launches natively, detached, the same way the Editor's own "Build And Run" does; a WebGL build is served from a built-in local static HTTP server, bound to loopback only, with correct `Content-Type` for the asset extensions Unity's WebGL output produces and correct `Content-Encoding` for `.br`/`.gz` pre-compressed output, and the default browser opens to it automatically. `unity build run` refuses cleanly and actionably when the recorded build's platform cannot run on the current OS, when the recorded artifact no longer exists on disk, or when no build has ever been recorded for the project — each a distinct, translated message rather than a generic failure.

- `unity watch test` re-runs `unity test --affected` every time a project file changes, instead of you re-running it by hand after each edit. Changes are debounced, so one save or one branch switch triggers exactly one run, and a burst of file-system events that arrives while a run is already going still coalesces into a single follow-up run rather than a queue of them. `Library/`, `Temp/`, `Logs/`, `Build/` and `obj/` are ignored by default (case-insensitively, wherever they appear in the project), and no `FileSystemWatcher` is even attached to a top-level directory that is ignored — so the heavy churn Unity's own import writes into `Library/` never reaches the CLI to begin with. `--ignore <pattern>` adds more file or directory patterns to that default set (repeatable). A run in flight always finishes; only the run after it can reflect a change that arrived during it, so two Editor invocations against the same project can never overlap. `Ctrl-C` while idle (waiting for the next change) exits cleanly with no Editor running at all; `Ctrl-C` while a run is in flight terminates the CLI and the spawned Editor together, the same way it already does for a plain `unity test`. Refuses to start under `--non-interactive` or in a detected CI environment, since a loop with no exit condition of its own would otherwise hang a job indefinitely. `unity watch build` is a documented follow-up, not included here.

- A repo can now commit `ProjectSettings/UnityCliConfig.json` to declare `unity build`/`unity test` defaults once, instead of repeating them on every invocation, in every CI workflow, and by every teammate's clone. The CLI discovers it by walking up from the working directory (or an explicit project path) to the project root — the same `ProjectSettings/ProjectVersion.txt` anchor `unity projects require` uses — and reads nine keys: `build.target`, `build.outputPath`, `build.profile`, `build.timeout`, `test.mode`, `test.reportFormat`, `test.coverage`, `test.coverageOptions`, `test.timeout`. Precedence is command-line flag, then environment variable (only `build.timeout`/`test.timeout` have one), then this file, then a new global config layer (`unity config set build.target …`, alongside the existing `config get`/`list`/`unset`), then the CLI's built-in default; `unity config resolve <key> [project]` prints the resolved value and names which of those layers supplied it. The file is validated, not sanitized, since it is untrusted committed input read out of someone else's clone: an unknown key or a malformed value fails with a message naming the file and the key rather than silently ignoring it, and it can never affect which editor binary the CLI runs or where from — that stays `ProjectVersion.txt`'s job alone. A repo with no such file behaves exactly as it did before this change. See `apps/cli/docs/config.md` and the schema at `apps/cli/schema/project-config.schema.json`.

### Fixed

- On Windows on Arm, `unity` no longer misreports a working git installation as missing or broken. The CLI's git-availability check pointed git's global and system config at the reserved device name `NUL` so it could ask `git --version` without reading the machine's own config; some Windows-on-Arm builds of git reject `NUL` outright as an invalid config path (`fatal: unable to access 'NUL'`) instead of treating it as no config at all, and the CLI's probe read that failure as "no git." The probe now points at a private empty file instead, which every `git` build already treats the same way a missing config file is: no config. `unity doctor` and `unity vcs providers`, which run the same git-readiness probe, are fixed by the same change.

- A signed-in user is no longer reported as signed out purely because the resident auth broker rejected their connection under the peer-authentication enforcement introduced above. The CLI's credential read already fell back to your locally-stored session whenever the broker could not be reached at all; it now falls back the same way when the broker answers but refuses this peer's identity (the common case: a locally-built or unsigned `unity` binary talking to its own broker), instead of treating a rejected connection as an authoritative "no account." Your stored session is untouched either way — this only changes whether a rejected connection is read as "try the keyring instead" or as "you are signed out."

- The CLI no longer reports that no Unity Editor is reachable when it runs inside WSL and the Editor runs on the Windows host. Before dialing the Editor's Pipeline server, the CLI checked that the process id recorded in the project's lockfile was still alive — but it asked its own operating system, and WSL and the Windows host do not share a process table. A process id that is genuinely running on the host can never be found from inside WSL, so that check always failed and the CLI treated a healthy Editor as one that had crashed: it reported nothing reachable, and it deleted the running Editor's lockfile as stale. The deletion is why updating the CLI never cleared it — the state a working Editor depends on had already been removed. A failed liveness check now counts as proof only where it can actually see the process, so under WSL the CLI dials the port and reports what it finds. `unity projects close` follows: for an Editor outside its own environment it asks over the same channel and confirms the exit by watching the port, and it declines `--force` instead of signalling a process id that could belong to something else on the local machine. Reaching the Editor at all still depends on the distro's networking mode — WSL shares the Windows host's localhost only in mirrored mode — so where the port does not answer, `unity status` now says that the Editor is on the host and names the setting to change, rather than reporting an unreachable Editor with no explanation.

- A running Editor whose project layout hides the `com.unity.pipeline` package is no longer reported as unreachable or missing. `unity status` and `unity pipeline list` decided whether to even look for an Editor's connection descriptor by first parsing the project's `Packages/manifest.json` — an inference about whether a Pipeline server *could* be running, standing in front of the descriptor the running server itself writes, which is direct evidence that one *is*. Wherever that manifest does not resolve — a Multiplayer Play Mode virtual player's partial clone (the reported case), a `Packages` folder reached through a symlink the CLI cannot follow, or a permissions refusal on the manifest — a perfectly healthy Editor answering on its own recorded port was listed as `unreachable` by `unity pipeline list` and omitted from `unity status` altogether. Addressing that same Editor with `--project-path` always worked, because that path reads the descriptor directly and never had the check; that mismatch is what made the behavior look arbitrary. Both commands now read the descriptor for every Unity project they discover and let it settle the question, so an MPPM clone shows up and reports reachable like any other instance. This can only ever add instances that are genuinely running: a project with no descriptor and no resolvable package reports exactly as before. `unity pipeline list` also stops asking the package registry for the latest published version when no discovered instance has an installed version to compare it against — that answer could not change any row, and against an unreachable registry it cost roughly 30 seconds of timeout and retries on what is otherwise a purely local command, which is precisely the offline case this fix newly surfaces.

- `unity plugin install` and `unity plugin upgrade` no longer download a plugin build that the plugin registry publishes on a Unity-staff-only channel (`alpha`, `nightly`) for an account that is not Unity staff. The registry already declined those versions for such an account, but when nothing else was admitted the commands fell back to the build compiled into the CLI, which for a plugin whose only builds are staff-only is the same artifact, so the decline only hid the plugin from `unity plugin list` while `install` fetched it anyway. Both commands now stop before any download with a message naming the plugin and pointing at `unity auth login`, and exit `3` (an authorization refusal, distinct from the `1` a failed download earns). Nothing changes for Unity staff, for plugins on the `stable` or `beta` channels, or when the registry has no entry for the plugin.

- Concurrent writes to the shared `user-settings.json` no longer drop each other's changes. Every setting `set`/`delete` rewrites the whole file, so two writers changing DIFFERENT keys at the same instant — a `unity` invocation and the Unity Hub, or two `unity` invocations — could each read the file before the other's write landed and then write back a version missing the other's key, silently losing it (for example a saved language override vanishing when the Hub happened to persist an unrelated preference at the same moment). Settings writes now take a short cross-process lock on the file — the same `<file>.lock` protocol the CLI already uses for its other shared state, and keyed on the same path in both binaries — so writers serialize and every key survives. A single writer is unaffected (an uncontended lock adds only a directory create + remove), and a lock that genuinely cannot be taken falls back to writing anyway rather than wedging the command.

- The CLI no longer mistakes an unrelated process for a running Unity Editor or Player when a stale connection descriptor names a recycled process ID. Editor and runtime discovery read the Pipeline connection descriptor (`Library/Pipeline/.unity-pipeline-port` for an Editor, `.unity-pipeline-runtime-port` for a player build), which records the process ID, and then checked only whether *some* process still holds that ID. Process IDs get reused, so after the recorded process exited and the operating system handed its ID to a different program, that leftover descriptor read as a live instance — and commands like `unity pipeline list`, `unity command`, and `unity command --runtime-path`/`--runtime` would try to connect to it. Discovery now also compares the process's start time against when the descriptor was last known current, so a process that started after the descriptor was written is recognized as a different, later process and the stale descriptor is cleaned up. A genuinely running Editor or Player is never affected, and when a start time can't be read the previous behavior stands.

- Commands that talk to a running Unity Editor over its Pipeline connection (`unity command`, `unity job`, `unity list`, and others) now name a second possible cause of a connection refusal: an AI coding agent's sandbox (OpenAI Codex's macOS Seatbelt profile is the reported case) blocking the outbound loopback connection, rather than the Editor actually being down. Previously every connection refusal reported only the generic "Cannot connect to Unity Editor Pipeline server... Make sure Unity Editor is running" message, which can be actively misleading when the CLI already found a valid Editor descriptor for that host and port — the Editor may well be running, and the sandbox may be what's blocking the socket. When a known agent-sandbox environment marker is detected, the message now also suggests checking the sandbox's own network permissions, alongside the original remedy rather than in place of it — an agent tool being present is not by itself proof that it is sandboxing this particular process. The message is unchanged when no such marker is present.

- Sign-in failures where the OS keyring accepts a write but does not keep it are now diagnosable. The CLI already writes each credential and reads it back to confirm it persisted, but only a write that failed outright was recorded in the log — a write that reported success and then read back nothing, a different value, or an error produced no log line at all, which is exactly why a real Windows sign-in failure could not be diagnosed from the report. That read-back failure is now logged too, with a message distinct from a write failure so the two can be told apart. The entry never contains your token or any secret.

- The CLI's Editor identity daemon no longer deletes a Hub's socket file when it stands down. On macOS and Linux a starting Hub rebinds the machine-wide Editor pipe underneath a running CLI daemon (binding unlinks the path first, so it always succeeds). The displaced daemon then deleted that path — which by then pointed at the Hub's socket — leaving a live Hub listening on a socket no Editor could reach for the rest of its session. The deletion happened even when the daemon just exited without releasing the pipe, because a .NET-bound Unix socket unlinks its path in the runtime's process-exit cleanup regardless. The daemon now binds its listener without handing that path to the runtime's cleanup, so it deletes the socket file only when it is genuinely still the owner and never a path a Hub has taken over.

- Commands that only ever talk to a running Editor over loopback — `unity command`, `unity status`, `unity job` — no longer pay for a proxy decision they never use. Every command used to resolve the OS's system proxy configuration up front, before dispatch, whether or not that command would ever make a proxied request; on a Windows machine with an explicit corporate proxy auto-config URL, that resolution alone measured 168–1065ms per invocation, dwarfing the sub-15ms round trip to a local Editor. The resolution now only runs the first time something actually needs it, so an invocation that never leaves loopback never pays for it. Every command that does make an external request (`unity self-update`, `unity auth login`, analytics, and so on) still resolves the same proxy, in the same way, with the same result — this only changes when the work happens, never what it produces.

- `unity status` no longer reports "no instances" when a Pipeline discovery file exists but this process cannot read it. Previously a permission-denied read of `Library/Pipeline/.unity-pipeline-port` was treated identically to a missing file, so a genuinely running Editor whose descriptor was written under a different OS user or account — the common case for a sandboxed AI agent tool, whose sandbox commonly runs the CLI as a separate, restricted account — produced the same misleading "Open Unity and install the Pipeline package" message as an Editor that was never running at all. `unity status` now names the actual cause, and calls out "sandboxed agent" specifically when an agent-sandbox environment marker (e.g. `CODEX_SANDBOX`) is detected.

- `unity templates pack` and `unity templates create` now ask a more precise question before copying a project's `Library` cache on Windows: whether another process holds a Windows byte-range lock on it, the mechanism Unity's `Library/DataStore` content store uses there instead of an incompatible share mode. This closes a possible silent-success gap analogous to one already fixed on Linux (a running Editor's cache copying without error into a template, rather than the command refusing) — reasoned from how that store locks on Windows rather than confirmed against a running Editor there, so treat this as hardening on top of the existing refusal rather than a report of a bug you were hitting.

- `unity self-update` and `unity self-install` on Windows now also cooperatively ask a resident `unity open`-spawned identity-server daemon to release the binary before attempting the replace, the same way they already do for a resident auth broker (CLI-818). Previously only the auth broker was covered, so a self-update could still fail with a Windows "file in use" error whenever an identity-server daemon was resident — the daemon is a second, independent long-lived holder of the same executable image, with its own idle timeout. `unity close` / `unity projects close` also now ask a resident identity-server daemon to check whether it is idle right after closing an editor, instead of always waiting out its full idle window; the daemon still only exits when it is genuinely not serving another editor. The daemon's existence and its post-close lingering window are now documented in `unity close --help` / `unity projects close --help`, alongside the existing `UNITY_NO_EDITOR_IDENTITY_SERVER=1` opt-out.

- `unity mcp` no longer strands an agent session with zero Unity tools for its entire lifetime when the MCP client connects before any Unity Editor is running. The server already re-attempted live tool discovery on every `tools/list` call, but most agent clients (Claude Code included) call `tools/list` exactly once, at connect time, and never ask again — so a session that started before the Editor came up saw an empty catalog and stayed that way until the user manually reconnected. The server now declares the standard `capabilities.tools.listChanged: true`, and a lightweight background watcher (a poll every 5 seconds, reusing the same live-discovery path `tools/list` already takes) sends `notifications/tools/list_changed` when the tool catalog transitions — an Editor connecting or disconnecting, or its Pipeline package adding or removing a command — so a compliant client re-fetches automatically, with no server restart or manual reconnect.

- `unity doctor`'s `credential-store` check now actually exercises the Windows credential-manager chunking path a real sign-in's tokens take, instead of only proving a small value round-trips. The check's synthetic probe value was 64 hex characters — well under the single-entry size cap Windows Credential Manager enforces — so on Windows it only ever tested the plain, non-chunked write path; a real sign-in's token payload is virtually guaranteed to exceed that cap and take the chunked-write/reassemble path instead, which the check never touched. The probe value is now sized to force that path, so a keyring that accepts a chunked write but cannot later reassemble it is caught rather than reported as a clean pass. The check's outcomes, exit-code behavior, and message keys are unchanged; only the size of the value it writes changed.

- A chunked credential write that the OS credential store accepts but does not actually persist no longer strands entries behind, and no longer takes your previous sign-in with it. On Windows a token payload over the single-entry size cap is split across numbered chunk entries plus a manifest entry naming them, and that manifest was trusted the moment the store reported the write had succeeded. On a store that accepts a write it cannot honor, two things then went wrong at once: the chunk entries stayed behind unreachable forever, because the id naming them lived only in the manifest that never landed, and the previous, still-valid generation was scrubbed anyway — so a failed sign-in could also cost you the session you already had. The write now reads the manifest back and confirms it before reporting success; when it cannot, it rolls back the chunks it just wrote and leaves the previous generation intact. A single glitched read is retried rather than taken as a lost write, and an entry that is present but is not a manifest is never mistaken for an empty slot, so a short plain token already in the slot survives. Sign-in still fails on a store that cannot hold a credential — what changes is that the failure no longer accumulates unreachable entries or destroys a working session. (Which error you see depends on the tier: a direct keyring write reports `KEYRING_WRITE_FAILED`, while through the auth broker, the default, the failure currently surfaces as not being signed in, tracked as CLI-1201.) **Entries already stranded on Windows by an earlier version are now cleaned up too**, rather than sitting in Credential Manager forever: signing out removes every chunk entry belonging to that account, and a sign-in that finds the slot in the state a failed write leaves behind collects the leftovers once the new token is safely stored. A sign-in only goes looking when the leftover manifest itself shows the slot is broken, so an ordinary token refresh — and a first sign-in on a machine that has no credential stored yet — is unchanged; leftovers with no manifest naming them at all are collected at sign-out instead, because at sign-in they cannot be told apart from a write another process is in the middle of. Stranded entries under an account name no longer known to anyone, and the macOS and Linux equivalents left by much older builds, are not covered.

- `unity install` no longer fails with an `INSTALL_FAILED` SQLite error about a missing `writer_kind` column when the shared install-state database was first created by an older Hub or CLI. That column was added additively — nullable, with older builds neither reading nor writing it — but only the Hub applied the additive migration on open, so a database carried forward from a pre-column build was left one column short in the CLI's opener and the next install failed as soon as the CLI tried to stamp its own writer identity onto its row. The CLI now runs the same additive migration the Hub already does, so a database from an earlier build is brought up to shape on open and the install proceeds normally.

- A declined or blocked Windows elevation prompt no longer files a crash report, and neither does a wrong installer/uninstaller format, an unsafe install path, an editor installed somewhere that cannot be safely uninstalled, or an install location you do not have permission to write to. None of these is a defect in the CLI: the first two are your own decision or a state of the machine (security software refusing elevation), and the rest are usage mistakes or properties of the machine the CLI is running on. A blocked elevation, an unsupported uninstall location, and an unwritable install location also stop asking you to run `unity bug` — the same treatment routine machine-state failures already got. The error message, its `Code:` line, and the exit code are all unchanged; only anonymous crash reporting and, for those three, the bug-report hint are affected. Codes that can still indicate a real defect in the install logic — a failed extraction, a generic installer failure, an incomplete uninstall, a missing destination — are unaffected and keep reporting.

- `unity status` no longer reports a Unity Editor as `ready` while it is still starting up. Readiness used to be decided entirely by the Pipeline server's listener answering, which happens as soon as it binds its port — well before the Editor's main thread can actually run a command, while it is still importing assets or compiling scripts after launch. A command dispatched into that window failed with a busy error `unity status` gave no warning of. It now also probes the main thread directly (the same bounded check the CLI already uses while waiting for an Editor it just launched), so an instance that is up but not yet able to run anything now reports `starting` instead of `ready`, both in the human output and in the `port`/`state`/`project`/`version`/`pid` rows of `--format json`/`ndjson`/`tsv`; the exit code changes to match, no longer reporting success for an Editor that cannot yet accept a command.

- `unity test` no longer reports "Tests ran, but the results ... could not be converted to JUnit" when the editor exits cleanly without writing a fresh results file — a self-contradicting message that gave no indication of what actually happened or what to do next, and that could also be triggered by an unrelated older report left over at the same `--output` path from a previous run. The run now tells the two real conditions apart: when `--filter`, `--shard`, `--rerun-failed`, or `--affected` narrowed the run and the editor matched nothing, that is reported as its own outcome telling you to broaden the selection; any other case names the problem plainly and points you at the editor log. Either way the command now exits with a failure code rather than risking a false success, since a run that produced no fresh results — whether the file is absent or just untouched — is not one a CI pipeline should treat as passing.

- `unity bug --share-project` no longer bundles an entire Unity project into the report. The copy walked the whole project — many thousands of assets, scripts, and GUID-named `.meta` files — and the report's intake pipeline attaches each file to the resulting ticket individually, so a shared project could produce a ticket with 1000+ separate attachments that was impossible to triage. The shared copy is now capped at 1000 files, with a warning when it is truncated; the archive is still delivered as a single `bugfiles.zip`.

- Crash reports no longer carry the invisible marker characters the CLI uses to style command hints. When an error message includes a hint like “Run unity self-update to install a version first”, the command name is wrapped in control characters that the terminal renderer strips at print time — but the raw message the crash reporter captures kept them, so a reported error reached Sentry with stray control bytes in its title and grouping key. Crash reports now render those hints to plain text and drop any leftover control character before an event is sent, so the message reads exactly as it does on screen. This affects only anonymous crash reporting; nothing you see in the terminal changes.

- `unity vcs uvcs review list`, `comments`, `reply` and `resolve` now work when you are signed in. All four failed on every invocation with an error telling you to run `unity auth login` — including immediately after doing exactly that, with `unity auth status` confirming the session. Reading code reviews needs a short-lived Unity Cloud gateway token, obtained by exchanging the credential that signing in gives you, and these four commands never performed that exchange: the request went out carrying nothing, and the service's rejection was reported as "you are not signed in". They now obtain the token before their first request, the same way every other command that reaches Unity Cloud already does. The sign-in error is also now reserved for a session that has genuinely expired or been refused — a gateway that simply could not be reached is reported as unavailable rather than as an authentication problem, so it no longer sends you to re-run a sign-in that was never the issue.

- Commands that reach Unity Cloud no longer fail once, spuriously, the first time you run one after your sign-in has been idle for a while. Each of them renews your credentials before its first request, but the renewed credential was saved without being handed to the part of the command that was about to use it — so that command still sent the credential that had just expired, was refused, and told you to sign in again. Running the same command a second time worked, because by then the renewal from the failed attempt had been saved. The renewal is now visible immediately, within the same run. This affected every command that talks to Unity Cloud, including `unity cloud project list`, `unity projects create` and `unity projects link vcs`, not only the code-review commands above.

- `unity install`, `unity install-modules`, and `unity editor module add` no longer try to install a bundled Visual Studio module when a compatible version is already on your machine. The CLI had no way to detect an already-installed Visual Studio, so it read the module's default-recommended flag as its install state instead — treating it as already installed or not installed regardless of what was actually on disk. It now checks the real state on Windows (via `vswhere`) and macOS, and reports Visual Studio as not installed on Linux, where the Installer doesn't exist.

- `unity command --caller plugin` no longer misreports Unity's own third-party-agent plugin as `other`. `--caller` matched against the same closed list of AI-client names the MCP `initialize` handshake uses, and `plugin` was never in it, so it folded to `other` regardless of what was actually passed. `--caller` now recognizes its own small, dedicated set of callers, still normalizing anything else — a bare flag included — to `other` exactly as before.

- Concurrent installs are no longer let past the single-install lock while an earlier install is still finishing. The cross-process lock (`install-state.db`) records the process that *claimed* it, which is not always the process doing the work — on Windows the elevated installer is a separate process that can outlive the `unity` invocation that started it. The CLI reclaimed the lock as soon as that recorded process read dead, so a second install could take the lock while the first was still writing the editor directory. It now reclaims a stale lock only when the holder's heartbeat is past its TTL *and* its process is gone — the same two-signal rule the Hub already uses over the shared store — so a still-running install keeps the lock, and a genuinely abandoned one is still reclaimed. A liveness check now answers whether install work is actually still happening rather than only whether the claiming process is up. The read that decides whether a lock is reclaimable and the write that claims it also now run as a single transaction, matching the Hub's own store: two installs racing the same stale lock could previously both read it as reclaimable and both claim it, before this change, with only one of the two writes actually persisting. The lock's answer is also now actually enforced: `unity install` and `unity install-modules` used to check the mutex only to decide when to start its heartbeat, then start installing regardless of what it said, so a peer holding the lock never actually stopped a second install from writing the same editor directory. A conflicting claim now stops the item before it reaches the installer.

- `unity mcp configure codex` now also relaxes Codex's own sandbox network policy, not just the MCP server entry. Codex's sandbox blocks outbound network access by default, so even a correctly spawned `unity mcp` server couldn't reach the Unity Editor Pipeline server over localhost — every tool call failed as if the Editor weren't running, with nothing pointing at Codex's sandbox as the real cause. The command now also ensures `sandbox_workspace_write.network_access` and a loopback-scoped `features.network_proxy` are turned on in Codex's `config.toml`, additively: any table or domain the file already has is left exactly as it was, only what's missing gets added. Because that setting is session-wide rather than specific to the MCP server, this also fixes `unity command <command>` when Codex runs it directly through its own shell tool.

- `unity status`, `unity editors running`, and `unity command` no longer report a healthy Unity Editor as unreachable right after it recompiles. Each probes the Editor's Pipeline server using the `evalToken` from a lockfile on disk, and Unity rotates that token on every domain reload (a script recompile). A probe landing in the brief window after a reload used the now-stale token, got a 401, and reported the Editor unreachable even though it and its Pipeline server were completely healthy. Each now re-reads the lockfile once and retries before giving up, the same way the CLI's MCP tool-call path already recovers from the same race. The error shown when several Editors are running and none looks reachable is affected too, since it relies on the same reachability check.

- On Windows, `unity self-install` and `unity self-update` no longer fail (or silently defer, on `self-update`, to a background swap that could never land) when a resident background helper is holding the installed binary open. Windows cannot overwrite the image of a running executable, and the retry windows both commands already had were sized for a short-lived process exiting, not a long-lived one. Both now first ask a resident helper at the same install location to release cooperatively — authenticated the same way every other request to it already is — before retrying, and fall back to the existing behavior unchanged if none is present or none responds.

- Fixed a class of hang where a handful of internal subprocess calls (the Windows editor/module installers, the terminal raw-mode helper, the archive-readability probe, `os.release()` detection, the WSL path translator, external-tool auto-fixups, and the self-uninstall cleanup wrapper) could deadlock or wedge past their own timeout if the spawned tool wrote enough to both stdout and stderr at once. Each read its two output streams one after the other instead of at the same time, which blocks forever once the tool fills whichever pipe isn't being read yet — the same shape CLI-487 fixed in the CLI's git-credential-helper calls. All of them now drain both streams concurrently, matching the pattern already used elsewhere in the CLI; nothing about their output or behavior otherwise changes.

- `unity install <version> -a x86_64` no longer reports "No editor version matched" on an Apple Silicon Mac that still supports Rosetta 2. The install catalog's architecture filter excluded x86_64 for every Apple Silicon Mac unconditionally, so an explicit x86_64 request could never find a match unless you happened to know the release's changeset and passed `--changeset` (which builds its own query and was unaffected). The catalog now offers x86_64 on Apple Silicon through macOS 27, the last release to ship Rosetta 2, and continues to omit it from macOS 28 onward, matching the architecture rule `unity editors upgrade` and `unity editors list` already use.

- `unity plugin install`, `unity plugin upgrade`, and `unity plugin remove` no longer race each other, Unity Hub's own background module updates, or the native Hub's own licensing-client install, into deleting a build another process just finished installing. Installing and removing a managed component (Plastic SCM, the licensing client, and similar) now serializes on one cross-process lock shared by the CLI and both Hubs, so a remove landing moments after an install can no longer silently destroy the fresh copy — the two settle in a defined order instead. This only ever showed up when two such operations ran against the same component at nearly the same time, which is not a normal workflow; when it happens now, the side that loses the race reports the module is busy rather than corrupting the winner's install. A lock left behind by a crashed or killed process is reclaimed automatically rather than blocking every later `unity plugin` command.

- `unity projects link vcs` and `unity projects create --vcs …` now record a per-project git identity that actually holds, even once a per-organization identity pin exists (Unity Hub can write one). Whenever the credential helper resolves a login for the project, it is now saved into the project's own `.git/config` as both the plain form AND a URL-scoped form keyed to that repository. Git ranks credential config by how specifically its URL matches first and only falls back to repo-local-vs-global to break a tie, so the plain-only form used to lose to any organization-wide pin with no error or warning; the URL-scoped form outranks it. This affects only projects where the credential helper resolves a specific login — a session started with `--git-token`, `--git-token-stdin`, or a provider env var carries no identity of its own and records nothing, exactly as before.

- `unity status` no longer sends an anonymous crash report when every discovered Unity Editor instance is unreachable, and no longer suggests running `unity bug` for it. That message's own remedy — open Unity, check the Pipeline package is installed — was already entirely up to you: the ordinary causes (Unity not running yet, a stale port file, the Pipeline package missing) are never a defect in this binary. The exit code is unchanged: `unity status` still exits non-zero when no instance is reachable, and `--format json`/`ndjson` still report the same `STATUS_ALL_UNREACHABLE` error.

- `unity license activate` now names `unity doctor` when a sign-in or session-expired failure hits a macOS machine that has already signed in before. A macOS login keychain that is locked (a headless restart, or an idle timeout) reads exactly like no token was ever stored — that conflation is deliberate and unchanged — so the failure used to send you back to `unity auth login` with no hint that the real problem might be the keychain, not the session. The hint only appears when this machine's own account record shows a session was recorded here; a machine that has genuinely never signed in still gets the plain sign-in message.

- `unity command`, `unity eval`, `unity run --command` and every other command that auto-discovers its Editor (no `--project-path`) no longer report "No Unity Editor instances found with reachable Pipeline servers" about an Editor that is completely healthy, just running a Pipeline package too old to parse the command line. Auto-discovery used to require a lightweight reachability probe to succeed before it would even consider an instance, and that probe can come back inconclusive against an old-but-live server even though the command itself would go through fine — dropping the only running Editor from the candidate list and reporting none found. Passing `--project-path` explicitly always worked correctly for the same Editor, because that path never ran the probe at all and let the real command attempt settle it, surfacing the accurate "this Editor's Pipeline package is too old to parse command lines — run `unity pipeline upgrade`" message. Auto-discovery now falls back the same way when it has exactly one such instance (or the working directory picks one out of several), so it reaches that same accurate message instead of the misleading "no instances found". When a running Editor with the Pipeline package installed has no readable descriptor at all — not merely an inconclusive probe — auto-discovery now names that project and points at `unity pipeline list` instead of reporting a flat absence.

- A crash traced from Sentry as `System.InvalidOperationException: Decoder ran into invalid data.` (Windows only, both at startup and at exit) is fixed at its one remaining unguarded site: any HTTP response this CLI reads — the update-check manifest fetch, the anonymous-usage exit flush, and anything else built on the shared HTTP client — is transparently Brotli-decoded, and a response whose declared encoding doesn't match its actual bytes made that decode failure escape as a raw, unclassified exception instead of the CLI's own typed network-error vocabulary every other partial-response failure already gets. It's now folded into that same handling, matching the identical guard `unity self-update`'s compressed-download path (CLI-995) already carried for this exact .NET exception. The embedded translation bundle's own Brotli container — a second, structurally similar decode path, and a candidate for the startup-phase half of those crash reports — is separately hardened to degrade to its already-existing "no translation available" fallback instead of crashing, should it ever decode a corrupt or truncated container; the shipped container itself is unaffected and continues to be verified byte-for-byte against source on every build.

- `unity open`, `unity projects open`, and `unity projects upgrade` no longer launch the Editor inheriting this CLI's own working directory. Unity's Editor startup code crashes outright if its working directory is unreadable at the moment it starts — deleted out from under it, an unmounted volume, or, on macOS, a protected user folder like Documents the process isn't allowed into — and until now an unset working directory meant it silently inherited whatever directory `unity` itself happened to be invoked from, exactly the condition that triggers the crash. The Editor is now always launched from the OS temp directory instead, matching the same fix already shipped in Unity Hub and its native Hub rewrite. This applies on every platform (macOS, Windows, and Linux), for both a normal launch and `--wait`. If the OS temp directory itself turns out to be unusable at the moment of launch (a restrictive sandbox, a race), the CLI falls back to its own previous behavior — inheriting the caller's working directory — rather than failing the launch outright.

- `unity auth login --cloudEnvironment staging` (and `dev`) no longer reports "Signed in, but unable to fetch your profile" right after a successful sign-in. The post-login profile fetch resolved its `core` host from a locally cached `cloudConfig.json` unconditionally, ahead of the environment you actually asked for — so a production cache left over from an earlier sign-in (or written by Unity Hub, which shares the same cache file) sent the newly issued, environment-scoped access token to production's `core` host, which correctly rejected it as a token from a different signing realm. The cache is now only trusted when it was written for the same environment you are signing into, matching how this same login command already resolves its sign-in URLs, and how `unity cloud` / `unity vcs` commands already resolve theirs.

- `unity foundry` commands no longer tell you to run `unity auth login` when you are already signed in, just for a different Unity cloud environment than the one Foundry is configured to use. Production and staging are separate signing realms, so a production session's token is never valid against the staging Foundry gateway (and vice versa) — re-running sign-in against the same default environment could never have fixed that. This case is now reported on its own terms, pointing at `unity foundry config list` / `unity foundry config set env <value>` to align the two, or at signing in again for the matching environment.

- `unity foundry show`/`get`/`search` now say WHY Foundry rejected a bad request instead of just that it did. The CLI already parsed the service's own explanation out of the response body, but no error message ever included it — not the message, not `--verbose`, and not the log file the error's own hint block points at — so the one detail that would tell you what was actually wrong with the request (a malformed asset id, for instance) was silently dropped every time. It's now shown directly in the error, and falls back to the HTTP status when the service sent no explanation.

- `--debugMode` is now accepted on every command instead of failing every invocation that used it with `error: unknown option '--debugMode'`. The CLI's structured logger has read this flag from the raw command line since it was first written — opening a `debug-log.json` file, and echoing every log record live, whenever it is present — but nothing ever registered it as a recognized option, so no real invocation could reach that code path; it was reachable only from a handful of internal unit tests. Making it reachable also surfaced where that live echo went: stdout, which on this binary is `unity mcp`'s JSON-RPC transport and every command's own `--format json`/`ndjson`/`tsv` output. It now writes to stderr instead, so `--debugMode` can be combined with any of those without corrupting them. It stays an internal, undocumented diagnostic aid and does not appear in `--help`.

- `unity install --resume` can now recover an interrupted MODULE download, not just an interrupted editor installer. Previously only an editor installer left on disk with no recovery entry could be rediscovered and resumed; a module archive (an Android SDK component, `OpenJDK.zip`, and the like) with a lost or missing recovery row could only be deleted and re-downloaded from scratch, however large. The CLI now records which editor version, architecture, and module a download belongs to right next to the partial file as it starts, so `--resume` can find and continue it. A partial it cannot positively identify is reported so you know it's there, rather than guessed at or resumed against the wrong editor.

- `unity self-install` on Windows can now be pointed at a directory other than `%LOCALAPPDATA%\Unity`. Setting `UNITY_CLI_HOME` selects the install root the same way it already does on macOS and Linux — `<UNITY_CLI_HOME>\bin\unity.exe`, with the user-PATH registry edit following it there — instead of being silently ignored. This matters whenever the account actually running the install can't reach its own `%LOCALAPPDATA%`: a restricted service account, or an AI-agent sandbox (Codex's Windows sandbox runs the sandboxed process as a separate local Windows user account with no grant into the interactive user's profile) can point the override at a directory it can read and write, such as a path inside its own workspace. `install.ps1` honors the same variable, so the compiled-installer path and self-install agree.

- `unity open`'s long-lived Editor identity daemon no longer stops reporting a signed-in Editor forever once its resident auth broker restarts or exits after sitting idle. The daemon resolves its broker connection once and reads from that same connection for its whole (potentially hours-long) life; before this fix, once a broker restart or idle-exit made that connection stale, every later Editor probe on the same daemon silently read as "not signed in" until the daemon itself was restarted. The daemon now detects a stale broker connection and reconnects on the next read — recovering automatically once the broker is back, with no daemon restart needed — and falls back to the direct keyring path (the same fallback already used when no broker is reachable at all) only once a reconnect attempt confirms the broker really is gone for good, rather than retrying it forever.

- A signed-in user is no longer told to sign in again when the resident auth broker merely hiccups reading the access token — a broker restart, or a transient failure persisting a freshly refreshed pair — instead of genuinely reporting the session invalid. Every broker-side failure used to collapse to the same signal, so a transient one and an authoritative "not signed in" or "session expired" answer were indistinguishable, and a networked command's preamble reported the transient case as a sign-out. The broker's actual answer is now classified: an authoritative refusal still reports sign-in/session-expired messaging as before, while anything else fails only that one operation with a message telling you to try again, leaving your session untouched.

- A signed-in Mac user is no longer told to sign in again when the macOS keychain merely refuses to read a credential that actually exists — a foreign-owned item (the Hub signed in first, or an upgraded CLI no longer matches the identity of the one that wrote it) once the OS's own "Always Allow" prompt has already failed to produce a value, whether declined or impossible to show at all (a headless session). That denial used to collapse into the identical result a genuine sign-out produces, so `cloud`-family commands reported "you are not signed in" for a session that in fact exists and simply could not be decrypted this one time. It is now reported distinctly — the session shows as unresolved rather than signed out, and a networked command fails with a specific message pointing at `unity doctor` instead of the generic sign-in prompt.

- A future release can no longer silently cost every signed-in macOS user an unexplained keychain "Allow" prompt (or a one-time forced re-authentication) on their first `unity` invocation after upgrading. The CLI's macOS binary is signed, but its code-signing designated requirement's `identifier` — the one thing macOS actually keys a keychain item's access-control list on — was never pinned and instead defaulted to the artifact's filename at signing time, which had already changed silently across past releases and could change again on any future, unrelated packaging change. It is now pinned to the same value already shipping, so nothing changes today, and a future accidental rename can no longer flip it.

- `unity projects list` (and every other project-registry read) can no longer fail over to a stale or empty `projects-v1.json` shadow copy purely because `hub.db` was last closed cleanly by another process — the Hub quit, or a previous `unity` invocation exited — with no other connection still holding it open. The read path's SQLite open was bound to macOS's ancient system `libsqlite3.dylib` (kept for Native AOT single-file packaging reasons — see `apps/cli/src/TokenManager/AccountStore/SqliteProvider.cs`), and that build refuses a read-only open of a WAL-mode database whose `-shm` sidecar is absent, since creating one is exactly what a read-only open cannot do. The registry read now runs through a shared native core (`shared-crates/hub-project-registry`, also linked into the native Hub) built on a modern, bundled SQLite that reads the main file directly in that situation instead of refusing. Registry writes are unaffected by this change.

- The Linux `.deb`/`.rpm` now actually install the Linux hardened auth-broker tier's assets (CLI-825): the `unity-auth-broker` systemd socket and service units, the `unity-broker` system-account descriptor, and the polkit consent policy, which existed in the repo for months but were never referenced from the package. `unity-auth-broker.service`'s `ExecStart` also now invokes the broker's real entry point instead of a verb that never existed. This is a packaging-only fix: installing (or upgrading) `unity-cli` now provisions a dedicated `unity-broker` system account and enables an idle systemd socket, but nothing in the CLI resolves that socket's address today, so it has no effect on how any command signs in or holds a token — the resident broker still does not enforce the hardened tier's isolation or polkit consent, which remains open work. See `apps/cli/docs/cli-825-linux-hardened-tier.md`.

- `unity command <name> --format json` no longer prints two separate `success` fields — one on the response's inner `data` object, one on the outer envelope — both hardcoded to `true` independently of each other and of whether the command actually succeeded. A consumer reading only the top-level field, or diffing the JSON structurally, saw a duplicated key with no single source of truth. There is now exactly one `success` field, on the envelope, derived from the same eval-envelope check the CLI already uses to decide whether to fail the command in the first place — so a command whose exec genuinely failed (an unreachable Editor, a rejected argument, a failed `eval`/`eval_file` compile or run) still exits non-zero and reports through the existing, unchanged error envelope exactly as before; this field cannot itself render `false`, since anything that would make it `false` already stops the command before this envelope is written. What changes is code shape, not an outcome you can observe today: no more duplicate field, and the one that remains is a real, tested signal rather than an independent literal that happened to always agree with it. `unity command`'s `--format tsv` `success` column, and `unity eval --format json`'s single (already non-duplicated) `success` field, are driven by the same corrected signal. Exit codes were already correct and are unchanged.

- `unity vcs setup` now shows the equivalent invocation for a Windows path operand instead of reporting "no shell quoting reproduces this run" for almost every real run. Every value containing a backslash was rejected outright, and on Windows every absolute path contains one, so the line was suppressed for nearly all Windows users. An ordinary path with single, non-trailing backslashes (`C:\games\MyGame`) is safe inside double quotes in both bash and `cmd.exe` and now prints correctly; a value with two or more consecutive backslashes, or one ending in a backslash, still suppresses the line, since either is genuinely unsafe to quote.

- Git-owned files the CLI rewrites directly — `.git/config`, `.git/packed-refs`, and `.git/HEAD` (during `projects link vcs`/`unlink vcs` and related repository setup) — no longer race a concurrent `git fetch`/`git gc` or a second CLI invocation touching the same file. Each write now takes the file's own `<file>.lock` first and commits through an atomic rename — the identical exclusive-create-then-rename protocol git's own client uses for these same files — so a concurrent writer serializes cleanly or is refused outright, and a reader can never observe a torn or partially-written file. A lock that is already held is reported with an actionable error rather than the CLI blocking indefinitely or silently overwriting it; a lock left behind by a crashed process is surfaced the same way, for a person to remove by hand once they've confirmed nothing else is using the repository, rather than being auto-reclaimed.

- `unity install` now retries a failed editor or module download instead of failing the whole install on the first transient network error. `unity install-modules` has always retried a failed download with a bounded, backed-off policy (`UNITY_INSTALL_RETRIES`, two attempts by default); `unity install` — which downloads the editor and its modules together — had no retry at all, so a single dropped connection failed the entire operation. Measured in a nightly release run: an Android NDK download truncated about 500 MB short and the whole editor+modules install failed, where the identical failure under `install-modules` would have been retried twice. `unity install` now applies the same bounded retry policy per download item, not by restarting the whole operation — an editor or module that already installed successfully is never re-attempted, only the one that actually failed. A deterministic failure (a rejected installer, a stale release manifest) still fails immediately without spending the retry budget on something a retry can never fix.

- `unity mcp configure continue` no longer writes an entry to `~/.continue/config.json`. Continue deprecated `config.json` in favor of `config.yaml` for MCP server configuration some time ago, so the entry this command wrote was never actually picked up by the tool. Continue.dev was also acqui-hired by Cursor and the standalone product discontinued in mid-2026, which settled whether it was worth adding a YAML-writing dependency for one now-frozen target: it is not (full reasoning in `apps/cli/docs/nuget-evaluation.md`). `unity mcp configure continue` now prints setup instructions instead — covering both the global `~/.continue/config.yaml` `mcpServers` list and the per-project `.continue/mcpServers/*.yaml` block-file form — the same shape `unity mcp configure trae`/`openclaw` already use for clients this CLI cannot safely configure a file for directly. `unity mcp configure --list` reports `continue` as a no-file client accordingly.
