unity-cli

June 11, 2026 · View on GitHub

English | Korean

Control Unity Editor from the command line. Built for AI agents, works with anything.

License: MIT

No server to run. No config to write. No process to manage. Just type a command.

Why this exists

I wanted to control Unity from the terminal. The existing MCP-based integrations required Python runtimes, WebSocket relays, JSON-RPC protocol layers, config files, server processes that need to be started and stopped, tool registration ceremonies, and tens of thousands of lines of over-engineered code. All just to send a simple command to Unity.

On top of that, every AI agent that wanted to use it needed its own MCP config and integration setup. The CLI doesn't care — any agent that can run a shell command can use it immediately.

That felt wrong. If I can curl a URL, why do I need all that?

So I built the opposite: a single binary that talks directly to Unity via HTTP. No server to run — the Unity package listens automatically. No config to write — it discovers Unity instances on its own. No tool registration — just call by name. No caching, no protocol layers, no ceremony.

The entire CLI is ~800 lines of Go (plus ~300 lines of help text). The Unity-side connector is ~2,300 lines of C#. It's just a thin layer that lets you control Unity from the shell — nothing more. You install the binary, add the Unity package, and it works.

Install

Linux / macOS

curl -fsSL https://raw.githubusercontent.com/youngwoocho02/unity-cli/master/install.sh | sh

Windows (PowerShell)

irm https://raw.githubusercontent.com/youngwoocho02/unity-cli/master/install.ps1 | iex

Other options

# Go install (any platform with Go)
go install github.com/youngwoocho02/unity-cli@latest

# Manual download (pick your platform)
# Linux amd64 / Linux arm64 / macOS amd64 / macOS arm64 / Windows amd64
curl -fsSL https://github.com/youngwoocho02/unity-cli/releases/latest/download/unity-cli-linux-amd64 -o unity-cli
chmod +x unity-cli && sudo mv unity-cli /usr/local/bin/

Supported platforms: Linux (amd64, arm64), macOS (Intel, Apple Silicon), Windows (amd64).

Update

# Update to the latest version
unity-cli update

# Check for updates without installing
unity-cli update --check

Unity Setup

Add the Unity Connector package via Package Manager → Add package from git URL:

https://github.com/youngwoocho02/unity-cli.git?path=unity-connector

Or add directly to Packages/manifest.json:

"com.youngwoocho02.unity-cli-connector": "https://github.com/youngwoocho02/unity-cli.git?path=unity-connector"

To pin a specific version, append a tag to the URL (e.g. #v0.2.21).

Once added, the Connector starts automatically when Unity opens. No configuration needed.

By default, Unity throttles editor updates when the window is unfocused. This can delay CLI commands because Unity API work is dispatched on the Editor main thread.

To fix this, go to Edit → Preferences → General → Interaction Mode and set it to No Throttling.

The connector also requests a PlayerLoop update whenever a CLI request arrives. No Throttling is still recommended for the most responsive background behavior.

Quick Start

# Check Unity connection
unity-cli status

# Enter play mode and wait
unity-cli editor play --wait

# Run C# code inside Unity
unity-cli exec "return Application.dataPath;"

# Read console logs
unity-cli console --type error,warning,log

How It Works

Terminal                              Unity Editor
────────                              ────────────
$ unity-cli editor play --wait

    ├─ scans ~/.unity-cli/instances/*.json
    │  → selects the Unity instance for this project

    ├─ sends command to the selected Unity listener
    │  { "command": "manage_editor",
    │    "params": { "action": "play",
    │                "wait_for_completion": true }}
    │                                      │
    │                                  HttpServer receives
    │                                      │
    │                                  CommandRouter dispatches
    │                                      │
    │                                  ManageEditor.HandleCommand()
    │                                  → EditorApplication.isPlaying = true
    │                                  → waits for PlayModeStateChange
    │                                      │
    ├─ receives JSON response  ←───────────┘
    │  { "success": true,
    │    "message": "Entered play mode (confirmed)." }

    └─ prints: Entered play mode (confirmed).

The Unity Connector:

  1. Opens a local HTTP listener when the Editor starts
  2. Writes a per-project instance file to ~/.unity-cli/instances/ so the CLI knows where to connect
  3. Updates the instance file every 0.5s with the current state (heartbeat)
  4. Discovers all [UnityCliTool] classes via reflection on each request
  5. Routes incoming commands to the matching handler on the main thread
  6. Survives domain reloads (script recompilation)

Before compiling or reloading, the Connector records the state (compiling, reloading) to the instance file. When the main thread freezes, the timestamp stops updating. The CLI detects this and waits for a fresh timestamp before sending commands.

Built-in Commands

CommandDescription
editorPlay/stop/pause/refresh the Unity Editor
consoleRead, filter, and clear console logs
execRun arbitrary C# code inside Unity
testRun EditMode/PlayMode tests
menuExecute any Unity menu item by path
reserializeRe-serialize assets through Unity's serializer
screenshotCapture scene/game view as PNG
profilerRead profiler hierarchy, control recording
listShow all available tools with parameter schemas
statusShow Unity Editor connection state
updateSelf-update the CLI binary

Editor Control

# Enter play mode
unity-cli editor play

# Enter play mode and wait until fully loaded
unity-cli editor play --wait

# Stop play mode
unity-cli editor stop

# Toggle pause (only works during play mode)
unity-cli editor pause

# Refresh assets (blocked in play mode unless --force is set)
unity-cli editor refresh

# Refresh and recompile scripts (also blocked in play mode unless --force is set)
unity-cli editor refresh --compile

# Force refresh while in play mode
unity-cli editor refresh --force

Console Logs

# Read error and warning logs (default)
unity-cli console

# Read last 20 log entries of all types
unity-cli console --lines 20 --filter error,warning,log

# Read only errors
unity-cli console --type error

# Include stack traces (user: user code only, full: raw)
unity-cli console --stacktrace user

# Clear console
unity-cli console --clear

Execute C# Code

Run arbitrary C# code inside the Unity Editor at runtime. This is the most powerful command — it gives you full access to UnityEngine, UnityEditor, ECS, and every loaded assembly. No need to write a custom tool for one-off queries or mutations.

Use return to get output. Common namespaces are included by default. Add --usings only for project-specific types (e.g. Unity.Entities). --usings accepts comma-separated namespaces and can be repeated. The csc compiler and dotnet runtime are auto-detected; if detection fails, specify manually with --csc <path> or --dotnet <path>.

unity-cli exec "return Application.dataPath;"
unity-cli exec "return EditorSceneManager.GetActiveScene().name;"
unity-cli exec "return World.All.Count;" --usings Unity.Entities
unity-cli exec "return World.All.Count;" --usings Unity.Entities --usings Unity.Mathematics

# Pipe via stdin to avoid shell escaping issues
echo 'Debug.Log("hello"); return null;' | unity-cli exec
echo 'var go = new GameObject("Marker"); go.tag = "EditorOnly"; return go.name;' | unity-cli exec

exec blocks async, coroutine, and deferred Unity callback keywords by default because the command returns before those paths complete. Use --allow-async only when that delayed behavior is intentional.

Because exec compiles and runs real C#, it can do anything a custom tool can — inspect ECS entities, modify assets, call internal APIs, run editor utilities. For AI agents, this means zero-friction access to Unity's entire runtime without writing a single line of tool code. Piping via stdin avoids shell escaping headaches with complex code.

# Execute any Unity menu item by path
unity-cli menu "File/Save Project"
unity-cli menu "Assets/Refresh"
unity-cli menu "Window/General/Console"

Note: File/Quit is blocked for safety.

Asset Reserialize

AI agents (and humans) can edit Unity asset files — .prefab, .unity, .asset, .mat — as plain text YAML. But Unity's YAML serializer is strict: a missing field, wrong indent, or stale fileID will corrupt the asset silently.

reserialize fixes this. After a text edit, it tells Unity to load the asset into memory and write it back out through its own serializer. The result is a clean, valid YAML file — as if you had edited it through the Inspector.

# Reserialize the entire project (no arguments)
unity-cli reserialize

# After editing a prefab's transform values in a text editor
unity-cli reserialize Assets/Prefabs/Player.prefab

# After batch-editing multiple scenes
unity-cli reserialize Assets/Scenes/Main.unity Assets/Scenes/Lobby.unity

# After modifying material properties
unity-cli reserialize Assets/Materials/Character.mat

This is what makes text-based asset editing safe. Without it, a single misplaced YAML field can break a prefab with no visible error until runtime. With it, AI agents can confidently modify any Unity asset through plain text — add components to prefabs, adjust scene hierarchies, change material properties — and know the result will load correctly.

Profiler

# Read profiler hierarchy (last frame, top-level)
unity-cli profiler hierarchy

# Recursive drill-down
unity-cli profiler hierarchy --depth 3

# Set root by name (substring match) — focus on a specific system
unity-cli profiler hierarchy --root SimulationSystem --depth 3

# Drill into a specific item by ID
unity-cli profiler hierarchy --parent 4 --depth 2

# Average over last 30 frames
unity-cli profiler hierarchy --frames 30 --min 0.5

# Average over a specific frame range
unity-cli profiler hierarchy --from 100 --to 200

# Filter and sort
unity-cli profiler hierarchy --min 0.5 --sort self --max 10

# Enable/disable profiler recording
unity-cli profiler enable
unity-cli profiler disable

# Show profiler state
unity-cli profiler status

# Clear captured frames
unity-cli profiler clear

Run Tests

Run EditMode and PlayMode tests via the Unity Test Framework.

# Run EditMode tests (default)
unity-cli test

# Run PlayMode tests
unity-cli test --mode PlayMode

# Filter by test name (substring match)
unity-cli test --filter MyTestClass

Requires the Unity Test Framework package. PlayMode tests trigger a domain reload; the CLI polls for results automatically.

List Tools

# Show all available tools (built-in + project custom) with parameter schemas
unity-cli list

Custom Tools

# Call a custom tool directly by name
unity-cli my_custom_tool

# Call with parameters
unity-cli my_custom_tool --params '{"key": "value"}'

Status

# Show Unity Editor state
unity-cli status
# Output: Unity: ready
#   Project: /path/to/project
#   Version: 6000.1.0f1
#   PID:     12345

The CLI also checks Unity's state automatically before sending any command. If Unity is busy (compiling, reloading), it waits for Unity to become responsive.

Global Options

FlagDescriptionDefault
--project <path>Select Unity instance by project pathauto
--timeout <ms>HTTP request timeout120000
--ignore-version-mismatchSkip CLI/connector version checkfalse
# Select by project path when multiple Unity instances are open
unity-cli --project MyGame editor stop

# Run even when the CLI and connector versions differ
unity-cli --ignore-version-mismatch status

Use --help on any command for detailed usage:

unity-cli editor --help
unity-cli exec --help
unity-cli profiler --help

Writing Custom Tools

Create a static class with [UnityCliTool] attribute in any Editor assembly. The Connector discovers it automatically on domain reload.

using UnityCliConnector;
using Newtonsoft.Json.Linq;
using UnityEngine;

[UnityCliTool(Name = "spawn", Description = "Spawn an enemy at a position", Group = "gameplay")]
public static class SpawnEnemy
{
    public class Parameters
    {
        [ToolParameter("X world position", Required = true)]
        public float X { get; set; }

        [ToolParameter("Y world position", Required = true)]
        public float Y { get; set; }

        [ToolParameter("Z world position", Required = true)]
        public float Z { get; set; }

        [ToolParameter("Prefab name in Resources folder", DefaultValue = "Enemy")]
        public string Prefab { get; set; }
    }

    public static object HandleCommand(JObject parameters)
    {
        var p = new ToolParams(parameters);
        float x = p.GetFloat("x", 0);
        float y = p.GetFloat("y", 0);
        float z = p.GetFloat("z", 0);
        string prefabName = p.Get("prefab", "Enemy");

        var prefab = Resources.Load<GameObject>(prefabName);
        var instance = Object.Instantiate(prefab, new Vector3(x, y, z), Quaternion.identity);

        return new SuccessResponse("Enemy spawned", new
        {
            name = instance.name,
            position = new { x, y, z }
        });
    }
}

Call it directly with flags or JSON:

unity-cli spawn --x 1 --y 0 --z 5 --prefab Goblin
unity-cli spawn --params '{"x":1,"y":0,"z":5,"prefab":"Goblin"}'

Key points:

  • Name: without Name, auto-derived from class name (SpawnEnemyspawn_enemy, UITreeui_tree). With Name = "spawn", the command becomes unity-cli spawn.
  • Parameters class: optional but recommended. unity-cli list uses it to expose parameter names, types, descriptions, and required flags — so AI assistants can discover your tool without reading the source.
  • ToolParams: use p.Get(), p.GetInt(), p.GetFloat(), p.GetBool(), p.GetRaw() for consistent param reading.
  • Discovery: unity-cli list shows built-in tools first (group: "built-in"), then custom tools (group: "custom") detected from the connected Unity project.

Attribute reference:

AttributePropertyDescription
[UnityCliTool]NameCommand name override (default: class name → snake_case)
DescriptionTool description shown in list
GroupGroup name for categorization
[ToolParameter]DescriptionParameter description (constructor arg)
RequiredWhether the parameter is required (default: false)
NameParameter name override
DefaultValueDefault value hint

Rules

  • Class must be static
  • Must have public static object HandleCommand(JObject parameters) or async Task<object> variant
  • Return SuccessResponse(message, data) or ErrorResponse(message)
  • Add a Parameters nested class with [ToolParameter] attributes for discoverability
  • Class name is auto-converted to snake_case for the command name
  • Override with [UnityCliTool(Name = "my_name")] if needed
  • Runs on Unity main thread, so all Unity APIs are safe to call
  • Discovered automatically on Editor start and after every script recompilation
  • Duplicate tool names are detected and logged as errors — only the first discovered handler is used

Multiple Unity Instances

When multiple Unity Editors are open, each registers its project path:

# See all running instances
unity-cli status

# Select by project path
unity-cli --project MyGame editor play

# Default: uses the current working directory's Unity project, or the only active instance
unity-cli editor play

Compared to MCP

MCPunity-cli
InstallPython + uv + FastMCP + config JSONSingle binary
DependenciesPython runtime, WebSocket relayNone
ProtocolJSON-RPC 2.0 over stdio + WebSocketDirect HTTP POST
SetupGenerate MCP config, restart AI toolAdd Unity package, done
ReconnectionComplex reconnect logic for domain reloadsStateless per request
Client supportMCP client setup onlyAnything with a shell
Custom toolsSame [Attribute] + HandleCommand patternSame

Author

Created by DevBookOfArray

YouTube GitHub

License

MIT