unityctl - Unity Editor Remote Control
Control a running Unity Editor from the command line without batch mode.
Setup (Required First)
Run unityctl status first to check what's already running. If Unity is already connected, skip straight to commands.
unityctl status # Check current state — may already be connected
unityctl bridge start # Start bridge daemon (idempotent, skips if running)
unityctl editor run # Launch Unity Editor (or open project manually)
unityctl wait # Block until Unity is connected (up to 120s)
Commands
# Status & Bridge
unityctl status # Check Unity, bridge, and connection status
unityctl bridge start/stop # Manage bridge daemon
# Editor
unityctl editor run/stop # Launch or stop Unity Editor
# Compile (run after modifying C# scripts or importing packages)
unityctl asset refresh # Compile scripts, returns errors on failure
# Play Mode
unityctl play enter/exit # Enter or exit play mode
unityctl play pause # Toggle pause (works in edit mode too — arms pause-on-play)
unityctl play step # Advance one frame (only in play mode)
# Logs
unityctl logs # Show logs since last clear (auto-clears on play/compile)
unityctl logs -n 50 # Limit entries
unityctl logs --stack # Include stack traces
unityctl logs --full # Ignore clear boundary
# Scenes
unityctl scene list # List scenes
unityctl scene load <path> # Load scene (e.g., Assets/Scenes/Main.unity)
unityctl scene load <path> --additive # Load additively (without unloading current)
# Testing
unityctl test run # Run edit mode tests
unityctl test run --mode playmode
# Screenshots
unityctl screenshot capture # Capture game view
# Video Recording (requires com.unity.recorder package)
# Note: record start auto-enters play mode if not already playing
unityctl record start # Start recording (manual stop)
unityctl record start --duration 10 # Record 10 seconds, blocks until done
unityctl record start --frames 300 # Record exactly 300 frames, blocks until done
unityctl record stop # Stop recording, returns file path + duration
# Scene Snapshot (structured observation with instance IDs)
unityctl snapshot # Scene hierarchy tree (default depth 2)
unityctl snapshot --depth 4 # Deeper traversal
unityctl snapshot --id 14200 --components # Drill into one object with all properties
unityctl snapshot --id 14200 --filter "type:Rigidbody" # Filter drill-down result
unityctl snapshot --screen # Add screen-space bounds and visibility for UI elements
unityctl snapshot --filter "type:Rigidbody" # Filter by type:T, name:N*, tag:T
unityctl snapshot --scene Assets/Scenes/Other.unity # Snapshot another scene (read-only)
unityctl snapshot --prefab Assets/Prefabs/Player.prefab # Snapshot a prefab asset
unityctl snapshot query 400 300 # What UI element is at screen pixel (400, 300)?
# UI Interaction (play mode only)
unityctl ui click --id 14200 # Click UI element by instance ID
unityctl ui click 400 300 # Click at screen coordinates
# Prefab Editing
unityctl prefab open Assets/Prefabs/Player.prefab # Open prefab in isolation mode
unityctl prefab open Assets/Prefabs/Player.prefab --context 14200 # In-context editing
unityctl prefab close # Close prefab stage, return to scene
unityctl prefab close --save # Save changes then close
unityctl prefab close --discard # Discard changes then close
# Dialog Detection (native OS popups blocking Unity)
unityctl dialog list # List detected popup dialogs
unityctl dialog dismiss # Dismiss first dialog (clicks first button)
unityctl dialog dismiss --button "OK" # Click specific button
Scene Observation & Manipulation Workflow
Use snapshot to observe, ui click to interact, eval --id for custom actions, then snapshot to verify.
UI elements auto-show text content, interactable state, and RectTransform layout.
interactable means the element has pointer event handlers (Button, Toggle, Slider, or custom IPointerClickHandler/IPointerDownHandler). For standard Selectables it reflects Selectable.interactable; custom pointer handlers always report as interactable.
Use --screen to add screen-space bounds and visibility for UI elements. Hittability (blocked-by detection) is only available in play mode.
Use snapshot query <x> <y> to identify what UI element is at a screen coordinate. Response includes a mode field: play (accurate) or edit-approximate (hit ordering may be imprecise).
Use ui click --id <instanceId> to click a UI element (play mode). Reports if the element is blocked by another.
unityctl snapshot --screen # See the scene with UI screen bounds
unityctl ui click --id 14200 # Click a button by instance ID
unityctl snapshot # Verify the result
unityctl snapshot query 400 300 # What UI element is at pixel (400, 300)?
unityctl ui click 400 300 # Click at those coordinates
Multiple targets with --id (uses targets[] array):
unityctl script eval --id 14200,14210 'targets[0].transform.SetParent(targets[1].transform); return "done";'
Script Execution
Evaluate C# expressions directly (common usings like UnityEngine, UnityEditor, System auto-included):
unityctl script eval 'Application.version'
unityctl script eval 'GameObject.FindObjectsOfType<Camera>().Length'
unityctl script eval --id -1290 'target.transform.position'
unityctl script eval -u UnityEngine.SceneManagement 'SceneManager.GetActiveScene().name'
unityctl script eval -u UnityEngine.UI,UnityEngine.SceneManagement 'SceneManager.GetActiveScene().name' # comma-separated
Pass arguments to the script with --:
unityctl script eval 'args[0]' -- hello
Full Script Execution
For complex scripts with custom classes, multiple methods, or logic beyond a single expression. Use the Write tool to create a .cs file, then execute it. The script must define a class with a static Main() method returning object (JSON-serialized):
// /tmp/MyScript.cs
using UnityEngine;
public class Script
{
public static object Main()
{
var player = GameObject.Find("Player");
return player?.transform.position.ToString() ?? "not found";
}
}
unityctl script execute /tmp/MyScript.cs
unityctl script execute /tmp/SpawnObjects.cs -- Cube 5 'My Object'
Use Main(string[] args) to accept arguments passed after --.
Important: Always use the Write tool to create the .cs file rather than shell heredocs (cat << 'EOF'), which break on single quotes in C# code.
Use -t <seconds> on script eval/script execute for long-running operations (default 30s):
unityctl script eval -t 600 -u UnityEditor 'return BuildPipeline.BuildPlayer(opts).summary.result.ToString();'
Typical Workflow
# After editing C# files...
unityctl asset refresh # Compile (fix errors if any)
unityctl snapshot # Observe scene state
unityctl play enter
unityctl snapshot # Check runtime state with instance IDs
unityctl logs # Check runtime logs
unityctl play exit
Troubleshooting
Run unityctl status first to diagnose issues.
| Problem | Solution |
|---------|----------|
| Bridge not responding | unityctl bridge stop && unityctl bridge start |
| Editor not connected | Normal - exponential backoff, up to 15 seconds |
| Connection lost after compile | Normal - domain reload, auto-reconnects |
| "Project not found" | unityctl setup or unityctl config set project-path <path> |
| Can't tell when Unity is ready | unityctl wait --timeout 300 |
| Command timed out | A native dialog may be blocking Unity: unityctl dialog list |
| Progress bar stuck | Check with unityctl dialog list, wait or dismiss |
| Editor not found | Use --unity-path to specify Unity executable |
Plugin Commands
Plugin: sample-script
Sample script plugin demonstrating the plugin system
unityctl sample-script hello [name] [--loud]Say hello from Unity
Plugin: sample-exec (executable)
A sample executable plugin that prints connection info. Demonstrates the unityctl-{name} naming convention.
unityctl sample-exec [args...]Prints greeting and bridge connection details.