# Unity CLI v1.0.0-beta.9

### Changed

- `unity install` and `unity install-modules` now document the child-modules pair as `--child-modules` / `--no-child-modules`, matching `unity editors module add` and the rest of the option surface. The old camelCase spellings (`--childModules` / `--no-childModules`, inherited from the Hub CLI) and the `--cm` / `--no-cm` shorts keep working.

- `unity install --list-components` is now `unity install --list-modules`, matching the `-m/--module` terminology. The old spelling keeps working as a hidden alias.

- `unity modules list` and `unity install --list-modules` now label the last column `Aliases` (it lists the alternate module names `-m/--module` accepts), and the `--format json` field is renamed from `downloaderName` to `aliases`.

- `unity skill show` prints the CLI's agent skill to stdout, so it can be read without being installed. Until now `unity skill` offered only `install` and `refresh`, and both write into an AI client's config directory — so an AI agent that found the command was offered a filesystem change it may not be permitted to make, and went back to reading `--help` instead. `unity skill show` writes `SKILL.md` verbatim and changes nothing: no prompts, no files, no network. `--path <file>` prints one of the skill's reference files, `--list` lists every file it contains, and `--format json` returns the file's content and that index together, so nothing has to be guessed from a filename. The content is the copy already inside the binary, so it works offline and reads the same skill an install would write.

- `unity --help` now says the agent skill exists. Its last line points at `unity skill show` for a task-oriented guide to the CLI, and at `unity skill install <client>` to put that guide into an AI client. Root help is where anyone — person or AI agent — starts guessing at an unfamiliar CLI, and until now the whole help surface mentioned the skill exactly once, as the `skill` command's own one-line description, which read as a setup chore rather than as something to read. The line is on the root surface only: a reader deep enough to be running a subcommand's help has already passed this one, so repeating it everywhere would only add noise to output people read constantly. The `skill` command's description changed with it, from "Install the Unity CLI agent skill into an AI client's skills directory" to "Read the Unity CLI agent skill, or install it into an AI client" — the group now has a read-only `show` verb as well as `install` and `refresh`, so describing only the install was both misleading and incomplete. Both strings ship in all ten languages.

- `unity plugin upgrade` now answers "is a newer version available, and what is it" with a real version comparison instead of a checksum guess. Until now, staleness was judged from the checksum and source sentinels written at install time, and when neither could date an install the command reported `indeterminate`, offering `--force` as the only remedy — which was reachable far more often than a genuinely undatable install, since most installs simply predated the sentinel that would have dated them. The CLI now reads the acquired version from a new `<moduleId>.version` install-ledger sentinel (written alongside the existing checksum and source sentinels) and compares it against the version the plugin registry publishes. Unity Hub does not write this new sentinel yet, so a plugin the Hub installed carries no ledger; `plugin upgrade` falls back to the existing checksum/source-sentinel comparison for exactly that case, so a Hub-installed plugin keeps getting the same staleness answer it always has. `indeterminate` is reachable only when NEITHER mechanism can date the install — nothing on disk at all, or an install predating both this ledger and the source sentinel. An installed version with higher precedence than anything the registry currently admits — a manual downgrade or a sideloaded build — is reported as its own `ahead-of-registry` outcome rather than folded into `indeterminate`, and is left untouched. When that higher-precedence installed version is specifically found `yanked` in the registry, `upgrade` instead downgrades to the highest non-yanked version and says so plainly, naming both the yanked version (and why) and the version it downgraded to — a silent downgrade would be worse than a loud one. `--force` keeps re-acquiring a genuinely undatable install, exactly as before. `--format json`/`tsv`/`ndjson` gain `fromVersion` and `toVersion` fields alongside the existing per-member ones, plus a `yanked` flag so a script can tell a downgrade-to-recover apart from an ordinary upgrade.

### Added

- `unity doctor` now reports the CLI's bundled third-party components. A new section lists each open-source runtime dependency with its version and SPDX license identifier, alongside the .NET runtime, and points at `https://spdx.org/licenses/` for the full license texts (the texts themselves are not embedded, to keep the binary small). The list is generated from the CLI's own direct package references so their versions can never drift from what the binary ships, and a test guards against a transitive runtime dependency slipping in unattributed. The section renders in every output format — `human`, `github`, `json`, `tsv`, and `ndjson` — with the machine formats carrying the component, version, and license as distinct fields.

- `unity plugin changelog <id>` shows a managed tool's own release notes: fetched from its publisher, rendered from markdown, and paged like `unity changelog` already is. `--version <version>` shows notes for a specific version instead of the one Unity would install. `plugin upgrade` now also prints each tool's one-line summary from the plugin registry before acting, and `plugin upgrade --changelog` fetches and shows the full notes first. The vendor's notes are shown exactly as published, in whatever language that is; the chrome around them is translated into all ten languages. A tool with no published notes for the requested version, or no publisher-hosted changelog at all, says so — it never blocks or fails `plugin upgrade`. `--format json` returns the notes and summary as plain strings, never as colored terminal output.

- A `--color <auto|always|never>` global flag, with `--no-color` as shorthand for `--color never`, gives you direct control over colored output on every command. Until now color was governed only by the `NO_COLOR` and `FORCE_COLOR` environment variables and whether the terminal is interactive; the flag takes precedence over both, so `--color always` keeps color when piping into a tool that preserves it and `--color never` turns it off for one run without exporting anything. It affects only human-readable output — the `json`, `tsv`, and `ndjson` formats never emit color.

- `unity version` is a new command that prints the CLI's version details in any output format, and the `--version` flag now honors `--format`. `unity version --format json` (also `tsv` and `ndjson`) reports the version, release channel (stable, beta, …), commit, platform, architecture, and runtime as one canonical object — the same version fact `env` and `doctor` already carry, now first-class and parseable. A bare `unity --version` still prints just the version string, so scripts that read it are unaffected; an explicit machine format set before the flag — `unity --json --version`, `unity --format json --version`, or `UNITY_FORMAT=json unity --version` — switches it to the structured object instead. (The flag terminates parsing, so the format must precede it; for order-independent machine output use `unity version --format json`.) Machine formats stay chrome-free, with no banner, consent prompt, or update notice.

- The auth broker's token store is now sealed to the machine it was written on, so the file on disk is worthless anywhere else. Windows seals to the TPM through the CNG Platform Crypto Provider and falls back to DPAPI where there is no TPM; Linux seals to TPM2 through `systemd-creds` and falls back to the Secret Service keyring; macOS uses the Keychain. Every platform falls back to a local key file so a machine with no TPM and no keyring still works. Which protection is in force is reported by `unity doctor` and in telemetry. This mitigates backup, stolen-disk and copied-file theft; it does not stop software already running as you on your own machine, which can ask for the store to be decrypted exactly as the broker does. Replacing a motherboard, clearing the TPM or resetting the OS asks you to sign in again rather than failing.

- The CLI now warns when a self-installed `unity` and a Homebrew-managed one are both on your `PATH`. In that state `which unity` keeps resolving to the self-installed copy while `brew upgrade` updates the other, so the running binary and the one Homebrew keeps current drift apart. The background update check spots the pair and points you at `unity self-uninstall -y` to remove the self-installed copy so Homebrew manages updates.

- Auth broker client libraries, for products that want Unity sign-in without holding the user's refresh token. A .NET library for external consumers and a TypeScript one for the Hub, both generated from the broker's wire contract, both discovering the broker by address alone (`UNITY_AUTH_SOCKET`, else a per-user socket or named pipe) and never by a shared secret. Each verifies the broker's publisher before it sends anything, and refuses by default when it cannot. When the installed CLI is too old to serve a consumer, the error names the upgrade path for how that CLI was actually installed — `unity upgrade` for a self-installed copy, or the owning package manager's command for a Homebrew, npm, apt or rpm install, which cannot replace itself. Integration guide: `apps/cli/docs/auth-broker-client.md`. The broker itself is not shipped yet; these are the client half.

- `unity auth consumers` lists the applications that have used your Unity sign-in on this machine — when each was first and last seen, which scopes it asked for, and how many requests were granted or refused — and `unity auth revoke <application>` stops one from using your sign-in. `unity auth revoke <application> --restore` undoes that. Revoking is deliberately coarse: it covers every scope for that application, so nothing is left still answering. The list is built from the auth broker's own audit trail, so on a machine that has not used the broker it is empty and says so.

- Telemetry ping. The CLI now sends one `cli telemetry` event per run, so total CLI usage can be counted — including the runs that never see a consent prompt, such as CI and agent runs. It is not a measure of who opted in, and no consent state is reconstructable from it. It takes one pathway for everyone, and the event reports only which Unity tool sent it, the CLI version, whether this looks like CI, and whether a consent prompt was possible — no user, machine, or session identifier (a fresh random UUID per invocation, never persisted), no stored IP, and no consent state. To turn it off, set `UNITY_NO_CLI_INVOKED_TELEMETRY=1`. The consent prompt, `unity analytics status`, `opt-in`, and `opt-out` all disclose it.

- `unity test --affected --since <ref>` runs only the tests a change can reach, taken from `unity vcs affected`'s guid impact graph rather than a hand-written filter. A Unity suite is often the slowest thing in a pipeline, and until now nothing consumed the impact analysis. Selection is opt-in and never the default, it writes to `test-results.affected.xml` so a selective run cannot overwrite the full-suite report it reads next time, and `--format json` names the selected and skipped assemblies, the test counts, the measured seconds, and the reason selection was or was not applied.

  **Selection is an approximation, and the command never lets you forget it.** The impact graph is a lower bound by construction — Addressables groups, resource-folder lookups, assets loaded by path or name, and references inside binary assets are real dependencies no static reader finds — so every run prints the sentence naming those classes, in human output, on stderr in tsv, and as `lowerBound` in json. **Anything the analysis cannot prove runs the whole suite**, with a stable untranslated token saying which case it was: a change touching any non-code file (the graph maps changed *code* to assemblies and has no edge from a prefab to the test that loads it, so a change carrying both scripts and assets refuses rather than selecting on its code half), a moved assembly boundary, a project that uses Addressables (read from `Packages/manifest.json` and `Packages/packages-lock.json`, so a project that pulls Addressables in through another package is caught too, and an unreadable manifest or settings folder counts as using them rather than as not), files changed outside the project, an empty diff, an unusable revision, no repository, unreadable project input, a missing or unattributable inventory, an inventory older than the change, a change that edits a test source at all (an inventory is keyed on assembly names, so a new case added inside an existing test assembly is invisible to it, and selecting anyway would skip the very test the change adds while still exiting 0), a set too large for one test filter, and — deliberately — a selection that came out empty, because "no test is affected" is the largest claim a lower bound can fail to support. A refused run behaves exactly as an invocation without `--affected` would, so a `--filter` you also passed is still honoured.

  **`--affected-compare` is how you find out whether to trust it on your project.** It runs every test, then reports what selection would have skipped and, from that run's own results, how many of those tests *failed* — the measured false-negative rate — beside the seconds selection would have saved. Both survive onto a failing run's output, which is the run most likely to have one. Selection needs an NUnit report from an earlier full run to resolve assemblies into test names, because the editor's command line filters by test full name and a full name carries no assembly; `--affected`, `--affected-compare`, `--shard` and `--rerun-failed` are mutually exclusive for the same reason, since the editor accepts one test filter.

- A generic `unity config get|set|list|unset <key>` interface now sits over the CLI's existing settings, so persistent configuration is readable and writable by key instead of only through a command per setting. `unity config list` prints every configuration key with its resolved value, honoring `--format human|json|tsv`; `get` reads one key, `set` validates and stores one, and `unset` clears one back to its default. The keys map onto the same stores the purpose-built subcommands already use — `proxy` and `proxy.bypass` (the proxy URL and its bypass list), `accelerator` (the Unity Accelerator endpoint), and `update-check` (the background update check, `on` or `off`) — so `unity config set proxy …` and `unity config proxy …` write the same place, and the existing subcommands keep working unchanged. Secret-shaped values are redacted on read: a proxy URL carrying credentials shows as `http://***:***@host` in `get` and `list` output while the real value is still stored for the proxy to use. An unknown key, a read-only key, or an invalid value is rejected with exit 2 and a message pointing at `unity config list`; the machine formats suppress banner and update chrome and emit a clean envelope, frame, or tab row.

### Fixed

- `unity cache key` no longer crashes on Windows when the project path points at a file rather than a directory — an installer in the Hub downloads directory, for instance. It read `ProjectSettings/ProjectVersion.txt` beneath the path without first checking the path was a directory, and beneath a file Windows raises an I/O error the command didn't handle. It now tests that the path is a directory first, so a non-directory path reports that it isn't a Unity project cleanly on every platform, while a genuine I/O fault inside a real project directory still surfaces.

- On Windows, `unity pipeline list` and `unity status` no longer report that no Unity Editor is running when one is. Both commands find Editors by asking Windows for every process and its command line, and that answer came back encoded in the console's code page. Any character that code page maps to a control byte — a bullet, a pilcrow, a section sign, an arrow — then corrupted the reply, and the whole query failed. The catch is that the character does not have to be anywhere near Unity: the query reports the command line of *every* process on the machine, so one unrelated background program with a bullet in its arguments was enough to hide every running Editor for as long as that program lived. It looked intermittent rather than broken, and it cleared on its own when the other program exited. The query now speaks UTF-8 on both ends, so no code page can corrupt it.

- On Windows, one unrelated program can no longer stop the CLI from seeing which Unity Editors are running. This is the other half of the problem above, and the fix for that one did not reach it. Windows reports the command line of every process, and any program may put a control character in its own arguments — a terminal bell is the one seen in the wild. Those characters have to be written in escaped form for the reply to be valid JSON, and Windows PowerShell writes them literally, so reading the reply failed at the first one. The program at fault need not be Unity or have anything to do with it. And because commands that delete files refuse to act on a check they could not complete, this did more than hide a running Editor: it also stopped `unity editors prune` and `unity projects clean` from running at all. The reply is now repaired before it is read, so every process is reported and the unusual command line is preserved exactly as it was.

- `unity pipeline list` and `unity status` now say when they could not check which Editors are running, instead of reporting that none are. The two answers had been rendered identically — an empty list, `success: true`, exit 0 — so a failed check was indistinguishable from an idle machine, which is what made the problem above so hard to recognize. When the check cannot run, both commands now warn, and `--format json` carries the same text in `warnings`. Anything they did find is still listed, and exit codes are unchanged. `unity editors prune` and `unity projects clean` already reported this.

- On Windows, `unity skill install` no longer refuses to update its own earlier install. The install ledger records where each skill copy was written, and that record is what marks a copy as this CLI’s own. The lookup compared the recorded path to the computed one character by character, so a record spelling the same folder differently — forward slashes where Windows uses backslashes, or different casing — read as “not ours”, and the CLI declined to overwrite what it took for another tool’s copy, asking for `--force`. Records now match by location rather than by spelling, so an update proceeds normally. Nothing about foreign-copy protection is relaxed: a copy no record claims is still refused without `--force`.

- `unity doctor` no longer hangs on macOS when the keychain wants to ask you something. The credential-store check verifies that a sign-in can persist by writing a probe value to the OS keychain, and a keychain operation the system cannot settle on its own (a locked keychain, or a binary whose signature the keychain cannot pin an owner to, which is what every locally built CLI is) parks the command on a security dialog that a piped, scripted, or SSH session can never answer. A diagnostic must answer rather than ask, so the probe now runs with keychain prompts disabled: an operation that would have needed one fails immediately, and the report says the store cannot hold a value unattended and names the service-account environment variables as the way around it, instead of the command sitting there until it is killed.

- `unity doctor` also no longer hangs (or raises a surprise keychain dialog) reading your sign-in status on macOS. Reading the saved sign-in decrypts a keychain entry, and when another app owns that entry (Unity Hub signed in first, or an earlier CLI release wrote it), macOS wants to ask you before handing it over. In a terminal session with no one to ask, that read blocked forever. Doctor now reads with keychain prompts disabled, and when the keychain declines, the report says what is actually true instead of hanging, prompting, or claiming you're not signed in: a saved sign-in exists that this binary can't read without a prompt, with an `auth-keychain` warning naming `unity auth login` as the fix. Commands that use the sign-in, like `unity auth login` and the cloud commands, keep their prompt so allowing access still works there.

- `unity templates create` and `unity templates pack` now tell you the project is open in an Editor, instead of failing with the opaque `UNABLE_CREATE_TPL`. A template carries the project's asset cache so projects created from it import quickly, and since Editor 6000.6.0a4 part of that cache is a content store the Editor keeps memory-mapped and locked for its whole session. Copying it while the Editor runs cannot work, and there is no supported way to ask a running Editor to flush it, so the command now checks before it starts and stops with the project path and the process id of the Editor holding it — the same guard `unity run`, `unity test` and `unity build` already had. If the Editor opens the project after that check, the copy itself now reports which file is held and that closing the Editor is the fix, rather than the bare code. Quitting the Editor and running the command again is all that is needed. On Linux this was worse than an opaque failure: the lock the Editor takes there is invisible to the mechanism the copy used, so the copy SUCCEEDED and wrote a template whose asset cache was captured mid-write — nothing failed until a project created from that template tried to import its assets. Those packs are now refused as well, so a template that packs is a template that works; if you packed one from an open project on Linux, pack it again with the Editor closed. Packing a project no Editor has open is unaffected, and a project whose Editor exited long ago is still packable — the leftover marker Unity writes on load and never removes is checked for liveness, not just presence.

- `unity install 6000.5` works again now that the 6000.5 line has reached end of life. Installing by a partial version — a line like `6000.5`, or a bare major like `6000` — only ever resolved against the current release catalog, which carries a handful of releases per stream. The day 6000.6 took over the supported slot, `6000.5` stopped matching the catalog and fell through to the archive lookup, which answers for one complete version: for a partial one it returned the oldest release in the line, and the install then failed with "No editor version matched". A partial version now resolves to the newest release in its line wherever that line lives — the current catalog or the archive — preferring official releases over prereleases. So `unity install 6000.5` installs the last 6000.5 release ever published, `unity install 2021.3` reaches the newest 2021.3 patch, and `unity install 6000` resolves to the newest official 6000 release instead of failing because more than one release matched. Complete versions and the `latest` / `lts` aliases behave exactly as before.

- `unity open` now reports failure when the Editor it started exits with an error, instead of always reporting success. Opening a project hands it to an Editor that outlives the CLI, so `unity open` watches the Editor only briefly and then returns. When the Editor quit with an error inside that window, the CLI printed “Editor exited with code N” and still exited 0 — and under `--format json` it printed nothing at all and still reported success, so a script could not tell a project that opened from one that never did. That contradicted the documented meaning of exit code 6, “the Editor exited with an error”. It now exits 6 and reports `OPEN_EDITOR_EXITED` in the machine-readable envelope, with the Editor’s own exit code in the message. An Editor that fails to start because of licensing still gets the same diagnosis it did before. `unity projects open` and `unity projects upgrade` change with it, for the same reason — an upgrade whose Editor died did not upgrade. `unity projects create --open` and `unity projects new --open` deliberately do not: the project is created and registered before the Editor is launched, so a failure to open it is reported as a warning — printed on screen, and carried in the machine-readable envelope alongside `opened: false` — and creating the project still succeeds. Note the limit of this: the CLI can only report an Editor that fails while it is still watching. An Editor that starts normally and quits some time later is not observed, and `unity open` still exits 0 for it.

- `unity build` now tells you when the project is already open in an Editor, instead of failing with what looks like a licensing problem. Building a project that a running Editor holds could never work: Unity refuses to open the same project twice, aborts with "It looks like another Unity instance is running with this project open", and exits 1. You almost certainly never saw that sentence. The CLI discards the Editor's console output, and the line is not written to the log file either, so what you were left with was a bare exit code and a path to a log whose most visible content is the services-config dump and the licensing handshake lines. That is why this was reported as the CLI being unable to contact licensing. It was not: nothing was wrong with your license. `unity build` now checks before it starts the Editor and stops with the project path and the process id of the Editor holding it, which is the same guard `unity run` and `unity test` already had. Note that this is scoped to the one project. Building while an Editor is open on a *different* project works and is unaffected, because the second Editor shares the licensing client the first one started.

- Release notes no longer lose their bold or italic styling when a line also contains a link. `unity changelog` and `unity self-update --changelog` render markdown to ANSI, and a bolded or italicized run appearing *before* a link on the same line was swallowed by that link: the styling vanished, a stray `1m` or `3m` was printed in its place, and the link text came out with a leading bracket. Only the colored rendering was affected, so `--format json`, `tsv`, and `ndjson` always showed the right text. The cause is that styling a run emits an escape sequence that itself contains a `[`, which the link matcher then mistook for the start of a link, and the fix is to match links first. Inline code spans were never affected, and text with no link on the line was never affected, which is why the notes shipped so far all read correctly.

- On Windows, `unity open <name>` and `unity projects open <name>` no longer print a mixed-separator path when the registered folder is not a valid project. The registry stores whatever spelling a project was registered under, including forward slashes, and the “Not a Unity project (ProjectVersion.txt not found at …)” message appended its own backslashed segments to that spelling, producing paths like `C:/Projects/Game\ProjectSettings\ProjectVersion.txt`. The printed path is now normalized to one spelling. In the same area, `unity projects unlink cloud` no longer re-spells the reported UVCS `workspaceRoot` with backslashes — it reports the workspace root exactly as the registry recorded it.

- A module whose published release data is out of date now fails once, and says so, instead of downloading the same file three times. When a release manifest declares a checksum and size that no longer match the file the download server serves — as it does for Windows Build Support, Windows Dedicated Server Build Support and Mac Dedicated Server Build Support on the 6000.5.0f1 through 6000.5.9f1 editors — verification correctly rejects the download. But the CLI reported the raw internal token `download:ERROR_CHECKSUM_MISMATCH`, which reads like a corrupt download and sends you to reinstall, and then spent its whole retry budget re-fetching a file that could never pass: over a gigabyte of transfer, on a 400 MB module, for a failure that was already settled. The CLI now compares the payload against what the download server said it was sending. When the two agree, the likeliest explanation is out-of-date release data rather than a damaged download, so the message says that, names the network and proxy as the alternative, and the CLI stops instead of re-fetching; the machine-readable `code` carries the checksum token. The message is careful not to overstate what it knows, because the download server supplies both numbers being compared. Nothing about verification is relaxed — the install still fails, a download that genuinely arrives corrupt is still retried exactly as before, and one module that stops early no longer consumes the retries its siblings in the same command were owed. The underlying release data is published by Unity, not by the CLI; 6000.5.10f1 and later are unaffected.

- Releasing a lock the CLI holds around its shared state files can no longer delete a different process's lock on a machine where the lock directory cannot be renamed (hardened Windows ACLs, some EDR configurations). A lock can go stale if the CLI is paused long enough — a suspended process, a system sleep — and another Unity process (the Hub, or a second CLI) may then reclaim it and take the lock for itself. The release path removed its lock by name, which on such a machine could destroy that other process's live lock and let both into the critical section at once. Release now removes the lock only while it still carries the identity the CLI last wrote, so a lock that was reclaimed while the CLI was paused is left untouched. This never surfaced as a visible error; it protects the shared settings, download, and install-state files from concurrent writes.
