# Unity CLI v1.0.0-beta.8

### Added

- `unity skill install <client> --local` now also installs the agent skill the project's `com.unity.pipeline` package ships. The package publishes a `unity-pipeline` skill inside itself, but a resolved package lives under `Library/PackageCache`, which no AI client's skill discovery reads — so until now nothing could load it. A project-local install mirrors it beside the `unity-cli` skill (for example into `.claude/skills/unity-pipeline/`), an embedded copy under `Packages/` wins over the package cache, and a project without the package behaves exactly as before. The copy is tracked like any other install: `unity skill refresh` re-reads it from the project's package wherever refresh runs from, reports rather than deletes when the package is gone, and a directory this CLI did not write is never overwritten without `--force`. The skill is never installed user-globally, because it versions with the project's own package.

- Bitbucket is now a provider the CLI can create repositories on and report readiness for, through the third-party Bitbucket CLI (`bkt`). `unity vcs providers` reports whether it is installed, at what version, which Bitbucket hosts it holds a session for and as whom; `unity projects create --vcs <your-bitbucket-host>` and `link vcs` create the remote through it; and a clone or push that fails to authenticate against a Bitbucket host now names `bkt auth login`, the exact token scopes the operation needs, and the page that mints one. Both Bitbucket Cloud and Bitbucket Data Center are covered, including their different repository-creation options — Cloud takes a workspace and sets the default branch on the first push, Data Center takes a project key and can pin the branch at creation. Installing it is offered through Homebrew and winget where the tool is packaged; no Linux distribution packages it yet, so those platforms get the command to run rather than a silent failure.

  **`bkt` is not Atlassian's own tool, unlike `gh`, `glab` and `tea`, and two consequences of that are handled rather than assumed.** Its binary name collides with an unrelated subprocess-caching utility that is in Homebrew's main repository and installs a binary under the same name, so finding `bkt` on your PATH is not evidence the Bitbucket CLI is installed: the CLI confirms identity from the tool's own output before running anything with your repository arguments, and reports a look-alike as not installed rather than driving it. For the same reason the Homebrew install names the tap explicitly instead of the bare package. Bitbucket Cloud and Data Center also reject each other's creation options rather than ignoring them, so the CLI reads which product a host is before building the command instead of guessing.

- Repository creation now works on Azure DevOps, through Microsoft's own `az` CLI, alongside the `gh`, `glab`, `tea`, and `bkt` integrations. It is offered wherever those already are: when a host has no built-in REST support and a token is not the route, the CLI looks for a signed-in provider CLI and uses it, and `unity vcs providers` reports `az`'s install and sign-in state alongside the rest. Azure DevOps differs from the other three in ways that are surfaced rather than papered over. `az repos` ships in a separate `azure-devops` extension, so an `az` that runs perfectly but lacks it is reported as its own state — installed, with a version, but unable to act — and the remedy named is `az extension add --name azure-devops` rather than a sign-in, which without the extension would not run either. Its own auto-install is not used, because with input closed it attempts a pip install and fails with an error that names neither the extension nor a fix. A repository lives under an organization *and* a project, so `--vcs-namespace` accepts `organization/project`, or a bare `project` to use the organization from your own `az devops configure`. Naming a non-default host without an organization is refused instead of being resolved against whichever organization az has configured, which would create the repository on the wrong server. Azure DevOps has no per-repository visibility at all — a repository is exactly as visible as its project — so `--visibility public` and `internal` are refused rather than silently ignored, and a description is refused for the same reason: `az repos create` has no option for one. Organization auto-detection is turned off on every call, so the target always comes from what you asked for rather than from the git remote of the directory you happen to be standing in. `az` is only ever offered for a host that is actually Azure DevOps — Azure DevOps Services, or an Azure DevOps Server matching the organization URL in your own `az devops configure` — so a machine with an Azure login never has `az` proposed for an unrelated self-hosted GitLab or Gitea. Both of az's credential routes count: an `az login` Entra session and a PAT stored by `az devops login`. Because Azure DevOps has no per-repository visibility — a repository is exactly as visible as its project — the project's visibility is checked before anything is created, so a private repository is never quietly created into a public project; when it cannot be read, nothing is created. An Azure DevOps Server deployed under a virtual directory (`https://server/tfs/Collection`) keeps its path. And if `az` reports success but its response cannot be read, the failure says a repository may have been created and names it where it can, rather than reporting that nothing was. Sign-in guidance for an unrecognized host now names Azure DevOps too, the "nothing here can create a repository" message lists `az` and `bkt` alongside `gh`, `glab`, and `tea`, and the CLI can point at the right token page and the one scope it needs, `vso.code_manage`.
- `unity vcs uvcs review` reads the code review comments a person left in the Unity Version Control GUI, and answers them. Until now those comments were reachable only from the GUI, which broke the whole point of reviewing an agent's work: a reviewer would leave notes on specific files and lines, and then have to restate every one of them by hand because nothing the agent could run was able to see them. `review list` finds a review, narrowing by `--changeset`, `--branch` or `--status` server-side; `review comments` reads it, reporting per comment the file, the line, the text, the author, the timestamp, the type, the threading parent, and whether it has been resolved; `review reply` answers a comment and `review resolve` records which changeset addressed it. There is no `cm` route to any of this — `cm codereview` manages review objects but has no comment verbs at all — so the commands talk to Unity Cloud directly, on the same sign-in the rest of the CLI already uses. **Three things about the data are worth knowing, because none of them is guessable.** The line number is one-based to match what the dashboard shows, and is absent rather than zero for a comment that is not anchored to a line, so a general remark on a file cannot be mistaken for a remark on its first line. The comments a review carries are an activity feed of fourteen kinds, only four of which are things a person wrote against a file; those four are what you get by default, and `--all-activity` adds the status changes, reviewer requests and timeline entries for when you want the whole history. And resolving a comment *is* naming the changeset it was applied in — there is no separate resolved flag — so `--changeset` is required rather than inferred from whatever the workspace happens to be sitting on, and resolving an already-resolved comment is refused instead of quietly overwriting the record of which change fixed it. **Unity Cloud only:** a self-hosted Unity Version Control server has no reviews API, so those workspaces get a clear refusal naming the GUI rather than an unexplained failure. Everything is available as `human`, `json`, `ndjson`, and `tsv`. Comment text comes from other people and is sanitized before it reaches your terminal; a reply carrying control characters is refused rather than silently altered, so what lands in the review is exactly what was written.


### Changed

- The command that updates the CLI itself is now `unity self-update`. `unity upgrade` keeps working — it is a registered alias, so it dispatches, takes every option, and completes exactly as before, and nothing you have scripted or documented breaks. The old name was the one thing in the CLI called "upgrade" that did not upgrade a project or an editor: `unity editors upgrade`, `unity projects upgrade` and `unity plugin upgrade` all act on something you own, while this one replaces the binary you are running. Naming it alongside `unity self-install` and `unity self-uninstall` says which of those it is before you run it, and follows the convention Unity documentation uses — update means installing a new version of a tool, upgrade means transforming your data so it works with one. The wording moved with the name, so the command now speaks of updating rather than upgrading in every language it ships in. One machine-visible detail moves with it: `--format json` and `ndjson` now report `"command": "self-update"` where they reported `"command": "upgrade"`, and the same name is what the CLI records for this command when analytics are on. A script that matches that field on the old value needs the new one; nothing else about the envelope, the exit codes or the options changed.

- Spanish and Chinese no longer double a word when the CLI reports that it updated itself. The sentence is assembled from a template plus a verb, and the preposition or particle joining them belongs to the template — but the Spanish and Chinese verbs carried it too, so a completed update printed "Actualizado a a 1.2.3." and "已已降级至 1.2.3。" instead of "Actualizado a 1.2.3." and "已降级至 1.2.3。". It has read that way in those languages for as long as the message has existed, in all three of its forms — update, downgrade and channel switch. The other seven languages were already correct and are untouched.

- `unity close <path-or-name>` closes the Unity editor that has a project open, and is available for the first time in a released build: its graceful path asks the editor to quit over the `com.unity.pipeline` channel, and that package has only now shipped publicly; `--force` never depended on it. `close` is now a top-level verb as well as `unity projects close`, the same pairing `open` and `unity projects open` already had. **Neither path saves your work.** The default asks the Editor to exit, which it does immediately and without prompting; `--force` signals the process instead (SIGTERM, then SIGKILL after a five-second grace) — once `--timeout` expires if the Editor could be asked to quit at all, or straight away if it offers no graceful channel to ask over — and is never used unless you ask for it. Closing a project no editor has open is a clean success with a notice rather than an error. When the editor is running but the project does not include `com.unity.pipeline`, the failure now names that package and the `unity pipeline install` that adds it, instead of reporting an unreachable editor — machine formats report this as a distinct `pipeline_missing` reason.

  One behavior is removed: a `unity-close` executable on `PATH` no longer runs as `unity close`. A first-party command has to win that lookup, or a binary dropped on `PATH` could shadow a shipped one — but if you have such a plugin, rename it, because it will otherwise stop being found with no error.

### Fixed

- Failures caused by the state of your machine no longer ask you to file a bug against the CLI. The error block a failed command prints ends with `Run unity bug to report this issue to Unity.`, and it appeared for every failure — including 23 conditions that are neither a defect in the CLI nor a mistake in what you typed: `tar` not being installed, the operating system refusing to launch an Editor or a configured AI client, an Editor executable no longer at its registered path, a cache or skill path that is a symlink, a `TEMP` directory the Windows update helper cannot use, another `unity` already owning your `PATH`, or no published binary for your platform. The same command succeeds unchanged once the machine changes, so there is nothing to report. Those failures now print the rest of the block — the error, its code, where the logs are, and how to re-run with `--verbose` — without the bug-report line, and they no longer count against the CLI's crash reporting. Every other failure, including anything that may be a defect in the CLI, still asks for the report.

- Commands no longer stall, or hang, on an OS keyring read after their work is done. Since the release that started sending the signed-in user's id with usage analytics, every command on a profile that is both signed in and opted in resolved that id from the OS keyring synchronously, on the main thread, on the telemetry path that runs after dispatch. The command did its work, printed its result, and only then blocked: 40 seconds measured in a non-interactive run, and unbounded in principle, so on a headless, CI, or SSH machine it could wait indefinitely rather than merely slowly. The read now happens at most once per process, on a background thread, at flush time, and only when there is something to send; it is given at most half of the 2-second telemetry budget the exit path already advertised, and an answer that does not arrive in time is abandoned, which sends the batch anonymously on its device id exactly as a signed-out run does. Nothing about what reaches Amplitude changes when the read succeeds, and a run that has opted out still constructs no analytics client and performs no keyring read at all. One visible consequence of the original change remains, and is expected: commands that never had reason to touch your keyring before, such as installing an editor or listing plugins, do read it once on an opted-in, signed-in profile, so macOS may ask for access the first time.

- Commands no longer resolve the default editor install path when the configured one could not be read. The install path is stored in `secondaryInstallPath.json`, which the Hub and the CLI share behind one advisory lock. When a read of it failed — the lock unavailable, or the file itself — the CLI treated that as "no custom install path is configured" and answered with the platform default, at exit 0, with nothing on screen to say otherwise. On a machine with a custom root that meant `unity install-path` printed `C:\Program Files\Unity\Hub\Editor` while the editors lived elsewhere, and an install started in that window landed there, splitting a user's editors across two roots for no visible reason. Those two states are now distinct. A read that loses the lock is retried once — the common contention shape recovers on the next attempt — and if it still fails, `unity install-path` and `unity editors install-path` print the path with a warning naming the file that could not be read, while `unity install` and `unity install-modules` refuse before anything is downloaded or written, saying nothing was installed and why. Installing into the platform default because that genuinely is the setting is unaffected.

- `unity run --command` now honors `--timeout` while it waits for the Editor it just started, instead of overrunning it by up to five and a half seconds. The wait that polls for the Editor's Pipeline server probed that server on a flat five-second timeout and then slept a flat half-second between polls, and neither was measured against the startup budget the run was given — so an Editor whose server is listening while its main thread is still busy (importing assets, reloading a domain, or holding a startup dialog) accepts the probe, never answers it, and carries the wait past the deadline before the deadline is even consulted. Both are now clamped to whatever is left of the budget, so `--timeout 10` reports its timeout at ten seconds rather than at fifteen and a half. An Editor that comes up right on the budget is still found, and an Editor that exits is still reported with its own exit code rather than as a timeout.
- When a connected Editor's `com.unity.pipeline` package is too old to run a command or a detached job, the error now names the package version that carries the missing behavior, not just the command to run. Previously it said only to run `unity pipeline upgrade`, which resolves whatever the registry currently serves as its latest version. When that is still older than the CLI needs, the upgrade reports success and the command fails exactly as before, with nothing on screen to say why. That is what happened to `unity command` and `unity run --command` in 1.0.0-beta.7, which is why it was withdrawn: the package version carrying the argument binding those commands need was published five hours after the release reached users, so the remedy the CLI named could not reach it. Naming the version makes that state visible at the moment you hit it.
- Windows installs no longer fail with `Access to the path '…' is denied` on machines where the lock directory's parent refuses new names. Since 1.0.0-beta.6 the CLI reclaimed a stale lock by renaming it aside before deleting it, and Windows authorizes those two operations differently: the rename must create a new name in the parent directory, which hardened ACLs and endpoint-protection rules can deny, while deleting the artifact stays permitted. Every affected command — `unity install`, `unity install-modules`, `unity install-path` — failed with a raw permissions error and spent several seconds retrying first. When the rename is impossible, the CLI now disposes of the stale lock through a handle it holds on that exact artifact, so its staleness is judged and its removal performed on the same object — a fresh lock another process puts at the same name in the meantime is out of reach by construction and is never deleted. A leftover install lock that genuinely cannot be removed — something on the machine is holding it open, or something foreign is inside it — is now reported as a plain failure naming the lock's path and the action to take, instead of a raw `System.IO.IOException` that also filed a crash report for a state of the machine.
- `unity skill refresh` no longer follows a symlinked directory component when re-rendering a tracked install on macOS and Linux. The install ledger is a plaintext file, and refresh rewrites every row at the path the row records, so a symlink planted at one of that path's directories — after the install, or by editing the ledger — silently sent the whole re-render outside the tree the row appears to name. `unity skill install` and the legacy-row migration already refused exactly this; refresh now applies the same refusal to ordinary tracked rows, bounded the same way (the row's own project for a `--local` install, your user profile for a global one). A refused row is reported with the same message the install path prints, left alone, and counted in the refresh conflict summary. A tracked path that is not a location this CLI installs to at all — a hand-edited entry, which gives that walk no honest bound — is likewise never rewritten, on any platform, though it is still pruned once nothing sits there. The symlink walk itself stays macOS/Linux-only, as with the install-side guard: a Windows profile legitimately contains junctions, and that platform relies on ACLs and per-user profile isolation.
- A successful `unity install --format json` now prints a result envelope instead of nothing at all. Every other outcome already reported itself — a failure renders the error envelope, and `--resume` with nothing to do reports why — but a successful install printed no stdout and no stderr and exited 0, so a script or an agent could not tell a finished 8 GB install from a crashed one. The envelope names the command, carries every item with its kind and final status, and repeats any warnings, and it is the same payload `--format ndjson` already reported, so the two formats describe a run identically. `--format tsv` is unchanged and still prints nothing, since a completed install has no table to draw.

- `unity install` no longer reports a successful install as failed on Windows when you are not an administrator. Installing to a location that needs elevation — the default `C:\Program Files\...`, or any root you set with `unity install-path --set` — ran the Editor installer in the elevated helper, which also creates the version folder, so that folder ends up writable only by administrators. Registering the Editor afterwards writes `modules.json` and `metadata.hub.json` into it, and that write was still being made by the unelevated command, so it was denied and the whole item was reported as `✗ <version>-<arch> (failed)` with the run exiting `INSTALL_FAILED` — after the Editor had installed correctly and had already been verified on disk. Those two writes now travel over the same elevated connection that ran the installer, so the single permission prompt at the start of the command covers them too and no second prompt appears. The per-module `selected` flag, written to the same file after each module installs, is routed the same way, so `unity install <version> --module <id>` is fixed as well. The two files are also written in the order that makes an interrupted registration recoverable, and each is written atomically, so a reader never sees half a document. Where a denial is still genuinely possible — a read-only volume, a policy that denies administrators, or `--no-elevate` — it now reports `INSUFFICIENT_PERMISSIONS` and names the remedy instead of failing with no error code at all.
- `unity editor module refresh <version>` now repairs an Editor that an earlier `unity install` left unregistered, which is the way back for anyone affected by the Windows permission bug above before it was fixed. Such an Editor has no `modules.json`, so it lists as manually installed with no modules and `install-modules` declines it, and reinstalling does not help — the installer sees an Editor already in the version folder and adds a second copy alongside it instead of registering the first. Refresh used to report success while writing nothing at all for these, because it only wrote for Editors you had added yourself with `unity editors add`. It now also writes for an Editor found in your install root, as long as there is no module list there already to overwrite, and if the folder needs administrator rights it asks for them once and completes the write. Note that an Editor you installed by hand outside of Unity Hub looks the same from the outside, so running refresh on one will likewise bring it under Unity Hub management — which is what makes `install-modules` work on it, and also means `unity uninstall` will then treat its folder as one it owns. Nothing does this on your behalf: it happens only when you name a version explicitly.
- `UNITY_NO_ELEVATE` is now honored by the permission prompt that `unity editor add` and `unity editor module refresh` can raise when the Editor folder needs administrator rights. Neither command has a `--no-elevate` flag, so that environment variable is the only way to say "never ask", and an unattended run that set it could still be left waiting on a dialog nobody was there to answer. Both now report the permission problem and stop instead. Both also announce why the prompt is about to appear before raising it, so a declined prompt leaves a record of what asked.
- A lock the operating system refuses to create is now reported as a plain, actionable failure, and releasing a lock no longer stalls on machines that refuse to rename one. Two leftovers from the Windows lock fix above. On a machine whose folder permissions block creating new names at all, reclaiming the stale lock now succeeds and creating the replacement then cannot, which surfaced as a raw `CreateDirectory failed for "..."` naming no cause and no remedy, and filed a crash report for what is a state of the machine rather than a defect. That case now names the lock file and tells you to check your write permission for the folder, in every language the CLI ships. Only a permissions refusal is reworded: a full disk or a read-only volume keeps the operating system's own more specific message, which says more than any wording of ours would. Separately, releasing a lock on such a machine spent about a second and a half retrying a rename that could never succeed before falling back to the removal it was always going to perform. That fallback is now taken immediately, so a release costs what it did before 1.0.0-beta.6. A genuinely temporary condition, such as another program holding the folder open for a moment, is still waited out.

- `unity install --resume` no longer ignores `--force`. A forced resume now proceeds past an already-installed editor — printing the same “Reinstalling editor…” notice as a plain forced install — so a reinstall can reuse an already-downloaded payload instead of transferring it again. When the editor is installed and `--force` is not passed, the resume now reports why it stopped in every output format: `--format json` and `ndjson` carry a result envelope with `reason: "already_installed"` where they previously printed nothing at all and exited 0, and the human message names the version. A resume blocked by a stale registry entry — an editor still registered whose files are gone from disk — now cleans up that entry and installs fresh, the way a plain install already did.
- Installing a module into an editor that lives in a protected folder now works instead of being refused or failing halfway. Editors installed to the default `C:\Program Files\...` location end up in a folder only administrators can write to, and until now only Windows `.exe` module installers were run with those rights: language packs, and the archives that carry things like Android NDK and the platform support modules, were unpacked by the ordinary unelevated command and simply failed. The two ways you would have hit it are both fixed. `unity install <version> --module <id>` used to install the editor and then fail partway through its modules; it now completes. And `unity install-modules`, which involves no `.exe` at all, used to stop before doing anything with “needs administrator rights — run the command again from an elevated terminal”, even though the CLI could have asked for those rights itself; it now asks once, as its first action, and installs. The rename step some modules finish with is covered too. Nothing changed for an editor in a folder you already own — no prompt appears — and `UNITY_NO_ELEVATE` still declines to ask and reports the permission problem instead.

- Fixed being unable to attach a debugger to a Unity Editor opened with `unity open` on Linux. The Editor didn't appear in Rider's “Attach to Unity process” list, or in Unity's own VS Code debugger, so a project opened from the CLI couldn't be debugged at all. The CLI was launching the Editor through a link named after its version, created next to the binary — which is also why a source-built Editor looked as though it had been copied — so that each version's windows could carry their own taskbar icon. That renamed the Editor process, and the debuggers find an Editor by the name it was launched under. The CLI now launches the Editor binary directly and removes the links it created before, including next to editors it didn't install. Per-version taskbar icons and dock grouping are unaffected on Editors new enough to identify their own windows, which the CLI now reads instead of imposing; on earlier Editors every window groups under one Unity Editor icon. The CLI also now writes the shared “Unity Editor” entry and its themed icon itself, which until now only the Hub did, so a machine that has never run the Hub gets the icon too.

- Interrupting a download with Ctrl+C no longer leaves the Windows taskbar showing progress forever. The CLI mirrors download progress to the taskbar with a terminal escape sequence that is sticky by design: the terminal holds the last reported state until it is explicitly reset, or the tab closes. Every ordinary end of an install sent that reset, but an interrupted one never did, because `unity install` had no Ctrl+C handling at all — on Windows the process was killed outright, so none of its cleanup ran. That turns out to have cost more than the taskbar: the elevated helper running the editor's installer was orphaned rather than shut down, and the install lock was left behind, so a retry within ten seconds was refused as though another install were still running. Ctrl+C is now handled cooperatively — the download stops, the run unwinds through its own cleanup, and the command exits 130 — and a second Ctrl+C still force-quits for the phase that cannot be interrupted, killing the installer and releasing the lock on its way out. The reset is also wired to the paths that never unwind at all, including a crash, so a run that dies mid-progress leaves the taskbar clean. Spinners are covered by the same fix: interrupting any command that showed one, such as `unity license activate`, used to leave the taskbar animating indefinitely.
