unity-technologies/skills

unity-package-management

Use when adding, removing, upgrading, or discovering Unity (UPM) packages programmatically from outside the Editor — headless or CI package installs via the C# UnityEditor.PackageManager.Client API, verifying package ids/versions against the Unity registry, or choosing which packages a game needs by genre, platform, and monetization. The Unity CLI does not manage UPM packages, so this skill covers that gap. Triggers on "install a Unity package", "add com.unity.*", "set up packages headless/CI",…

All-time #3964 Trending #1143 Hot #6040 First seen Jul 9, 2026
8-week activity · all time api

Installation

$ npx skills add unity-technologies/skills --skill unity-package-management

Summary

  • Use when adding, removing, upgrading, or discovering Unity (UPM) packages programmatically from outside the Editor — headless or CI package installs via the C# UnityEditor.PackageManager.Client API, verifying package ids/versions against the Unity registry, or choosing which packages a game needs by genre, platform, and monetization.
  • The Unity CLI does not manage UPM packages, so this skill covers that gap.
  • Triggers on "install a Unity package", "add com.unity.*", "set up packages headless/CI", "which packages for a <genre> game".

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from unity-technologies/skills · top by installs.

npx skills add unity-technologies/skills

Browse all from unity-technologies/skills

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Repository health

Stars 747
License LICENSE.md
Default branch main
Status Active

Skill metadata

Parsed from SKILL.md frontmatter.

Allowed toolsBash, Read, Write, Edit

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 13,026 B
  • docs SUMMARY.md 570 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 3,347 installs

SKILL.md

Unity Package Management (headless, via the C# Client API)

Add, remove, upgrade, and discover UPM (Unity Package Manager) packages programmatically with UnityEditor.PackageManager.Client, driven headless from the terminal or CI. Do not hand-edit Packages/manifest.json — the Client API resolves dependencies and compatible versions correctly, whereas manual edits routinely break resolution.

This complements the unity-cli skill (editor install, project creation, build/test): the CLI has no package-management command, so all package work goes through the Editor's C# API.

When to use

  • Add / remove / upgrade one or more packages in an existing or freshly-created project.
  • Set up a project's packages non-interactively in CI.
  • Verify a package id exists, or find its available versions, before depending on it.
  • Decide which packages a game actually needs — see

[references/select-packages.md](references/select-packages.md).

Choosing what to install

Install what the project actually needs, not everything; prefer packages the chosen template already provides (URP templates already include the render pipeline, Input System, etc.). The genre / look / platform / monetization → package mapping, plus how to search the registry, is in [references/select-packages.md](references/select-packages.md). Produce a deduplicated list of package ids and read it back to the user before installing.

The -quit problem — why NOT unity run for installs

Client.Add / Client.AddAndRemove are asynchronous: they return a Request that only completes on later EditorApplication.update ticks (the UPM child process marshals its result back on the Editor's main-loop pump, so a blocking while (!req.IsCompleted) busy-wait deadlocks it). The Editor must stay alive after -executeMethod returns, until the request finishes.

unity run cannot be used for the installer: its default path injects -quit (see the reserved flags in the unity-cli skill). With -quit, the Editor quits the instant the method returns — before UPM resolves — so packages never install and the callback never runs.

Solution: launch the Editor binary directly in -batchmode without -quit. The Editor stays alive, EditorApplication.update keeps ticking, the poll callback runs, and it calls EditorApplication.Exit(code) itself when done — which both quits and sets the process exit code.

The installer script

Write this to Assets/Editor/ProjectBootstrap/PackageInstaller.cs. It must live under an Editor/ folder (or an Editor-only assembly) because it uses UnityEditor.

using System.Linq;
using UnityEditor;
using UnityEditor.PackageManager;
using UnityEditor.PackageManager.Requests;
using UnityEngine;

namespace ProjectBootstrap
{
    // Installs (and optionally removes) a fixed set of packages via the PackageManager
    // Client API, headless-safe.
    public static class PackageInstaller
    {
        // EDIT this list to match the package selection (see references/select-packages.md).
        static readonly string[] PackagesToAdd =
        {
            "com.unity.inputsystem",
            "com.unity.cinemachine",
            "com.unity.render-pipelines.universal",
            // "[email protected]"  // pin a version with @ when a minimum is required
        };

        // Optionally drop packages in the same resolution pass (e.g. a template default you don't want).
        static readonly string[] PackagesToRemove = { };

        const double TimeoutSeconds = 600; // UPM resolution + downloads can be slow

        static AddAndRemoveRequest _request;
        static double _deadline;

        // Invoke with: -executeMethod ProjectBootstrap.PackageInstaller.Install  (NO -quit)
        public static void Install()
        {
            if (PackagesToAdd.Length == 0 && PackagesToRemove.Length == 0)
            {
                Debug.Log("[PackageInstaller] Nothing to do.");
                EditorApplication.Exit(0);
                return;
            }

            Debug.Log($"[PackageInstaller] Adding: {string.Join(", ", PackagesToAdd)}");
            _request = Client.AddAndRemove(packagesToAdd: PackagesToAdd, packagesToRemove: PackagesToRemove);
            _deadline = EditorApplication.timeSinceStartup + TimeoutSeconds;
            EditorApplication.update += Poll;
        }

        static void Poll()
        {
            if (_request == null) return;

            if (!_request.IsCompleted)
            {
                if (EditorApplication.timeSinceStartup > _deadline)
                {
                    EditorApplication.update -= Poll;
                    Debug.LogError("[PackageInstaller] Timed out waiting for UPM.");
                    EditorApplication.Exit(2);
                }
                return;
            }

            EditorApplication.update -= Poll;

            if (_request.Status == StatusCode.Success)
            {
                var names = _request.Result.Select(p => $"{p.name}@{p.version}");
                Debug.Log($"[PackageInstaller] Resolved: {string.Join(", ", names)}");
                EditorApplication.Exit(0);
            }
            else
            {
                Debug.LogError($"[PackageInstaller] Failed: {_request.Error?.message}");
                EditorApplication.Exit(1);
            }
        }
    }
}

AddAndRemove installs the whole set in a single UPM resolution pass — faster and less error-prone than one Client.Add per package.

Add / remove / upgrade with one script:

  • Add: list the id in PackagesToAdd.
  • Remove: list the id in PackagesToRemove.
  • Upgrade / pin: add the id with @<version> (e.g. [email protected]). Without

a version, resolution picks the latest compatible release.

Discovering / verifying packages

To confirm an id exists or list its versions before adding it, search the registry. The in-Editor Client.SearchAll() / Client.Search("<id>") calls are also async, so they use the same poll-and-Exit pattern and the same headless run as the installer. Write Assets/Editor/ProjectBootstrap/PackageSearch.cs:

using System.Linq;
using UnityEditor;
using UnityEditor.PackageManager;
using UnityEditor.PackageManager.Requests;
using UnityEngine;

namespace ProjectBootstrap
{
    public static class PackageSearch
    {
        const double TimeoutSeconds = 120;
        static SearchRequest _request;
        static double _deadline;

        // Invoke with: -executeMethod ProjectBootstrap.PackageSearch.SearchAll  (NO -quit)
        public static void SearchAll()
        {
            _request = Client.SearchAll();                 // or Client.Search("com.unity.cinemachine")
            _deadline = EditorApplication.timeSinceStartup + TimeoutSeconds;
            EditorApplication.update += Poll;
        }

        static void Poll()
        {
            if (_request == null) return;
            if (!_request.IsCompleted)
            {
                if (EditorApplication.timeSinceStartup > _deadline)
                {
                    EditorApplication.update -= Poll;
                    Debug.LogError("[PackageSearch] Timed out.");
                    EditorApplication.Exit(2);
                }
                return;
            }
            EditorApplication.update -= Poll;

            if (_request.Status == StatusCode.Success)
            {
                foreach (var p in _request.Result.OrderBy(p => p.name))
                    Debug.Log($"[PackageSearch] {p.name}@{p.versions.latestCompatible}  {p.displayName}");
                Debug.Log($"[PackageSearch] {_request.Result.Length} packages found.");
                EditorApplication.Exit(0);
            }
            else
            {
                Debug.LogError($"[PackageSearch] Failed: {_request.Error?.message}");
                EditorApplication.Exit(1);
            }
        }
    }
}

_request.Result is a PackageInfo[]; each entry exposes name, displayName, description, and versions (.latest, .latestCompatible, .all). For a terminal-only check without the Editor (a known id, not free-text search), query the registry directly — see [references/select-packages.md](references/select-packages.md#discovering-and-verifying-packages).

Run it headless (direct Editor invocation, no -quit)

Resolve the Editor binary from the version, then run it in batch mode. The script owns quitting via EditorApplication.Exit, so do not pass -quit:

VERSION="<version>"          # e.g. 6000.0.47f1 (or an installed version)
PROJECT="<project-path>"
METHOD="ProjectBootstrap.PackageInstaller.Install"   # or ...PackageSearch.SearchAll

# Install directory of that editor (Hub layout), via the unity CLI
ED=$(unity editors path "$VERSION" --format json | python3 -c "import sys,json;print(json.load(sys.stdin)['data']['path'])")

# Resolve the executable per-OS (handles both "dir containing Unity.app" and the ".app" itself)
case "$(uname)" in
  Darwin) if [ -d "$ED/Unity.app" ]; then UNITY_BIN="$ED/Unity.app/Contents/MacOS/Unity";
          elif [[ "$ED" == *.app ]]; then UNITY_BIN="$ED/Contents/MacOS/Unity";
          else UNITY_BIN="$ED/Unity"; fi ;;
  Linux)  UNITY_BIN="$ED/Editor/Unity" ;;
  *)      UNITY_BIN="$ED/Editor/Unity.exe" ;;   # Windows (Git Bash / MSYS); use Editor\Unity.exe in PowerShell
esac

"$UNITY_BIN" -batchmode -projectPath "$PROJECT" -executeMethod "$METHOD" -logFile -
echo "Exit code: $?"   # 0 = success, 1 = UPM error, 2 = timeout

-logFile - streams the Editor log (including the [PackageInstaller] / [PackageSearch] lines) to stdout so you can watch resolution progress and read any UPM error. If unity editors path output shape differs on your build, get the directory from unity editors --installed --format json instead.

Verify

# Every requested id should appear as a dependency
cat "<project-path>/Packages/manifest.json"

Confirm the run exited 0 and each package from the list is present in manifest.json. If a package fails to resolve, _request.Error.message is logged; read it and check the id/version against the registry. The Editor's own log (including the [PackageInstaller] lines) is the stdout you streamed with -logFile - above — read it there, not via unity logs (which shows the CLI's own log, not the Editor's).

Import & save headlessly (generate .meta files)

After a script or tool writes new .cs/asset files, Unity must import them so it generates the .meta file each asset needs — and every .cs/asset MUST be committed together with its .meta. Merely opening the project once (unity open "<project-path>") imports and generates them; use this method when you need it headless (in a script or CI).

Unlike the package installer, this is synchronous — it finishes before returning — so it's safe to run via unity run (its injected -quit is harmless; the method also calls EditorApplication.Exit for a clean exit code). Write Assets/Editor/ProjectBootstrap/ProjectSaver.cs:

using UnityEditor;
using UnityEngine;

namespace ProjectBootstrap
{
    public static class ProjectSaver
    {
        // Invoke with: -executeMethod ProjectBootstrap.ProjectSaver.SaveAll
        public static void SaveAll()
        {
            AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate);
            AssetDatabase.SaveAssets();
            Debug.Log("[ProjectSaver] Assets imported and saved.");
            EditorApplication.Exit(0);
        }
    }
}
unity run "<project-path>" --editor-version <version> \
  -- -executeMethod ProjectBootstrap.ProjectSaver.SaveAll

Notes

  • These editor scripts are a bootstrap convenience. Leave them in

Assets/Editor/ProjectBootstrap/ (they do nothing unless invoked) or delete them after setup — your call; mention it to the user.

  • All scripts live under Editor/ because they use UnityEditor; they never ship in a build.
  • Monetization / backend packages (com.unity.purchasing, com.unity.services.levelplay, the

UGS packages) install through this same mechanism, but do the actual integration via the dedicated skills: implement-in-app-purchases, levelplay-unity-integration, build-live-game.