2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ヘッドレスの Ubuntu 26.04 で、AI(Claude Code)に Unity を使わせる

2
Posted at

はじめに

※この記事はClaude Codeに書いてもらいました。人間には読みにくいかも知れませんが、同じことをAIにやって欲しい時に、この記事を渡すと上手く使ってくれて手戻り少なく作業してくれるのではと思います。

ヘッドレスの Linux サーバー上で Unity エディタを常駐させ、Claude Code から MCP 経由で操作する環境の構築手順です。
Ubuntu 26.04.1 LTS(x86_64)で構築・運用した実績があります(2026-10 時点)。

0. 全体像

サービスの構成と起動順

すべて systemd のユーザーサービスです(linger を有効にして、ログインしていなくても起動させます)。

エディタを操作するときの流れ

構築実績のある組み合わせは次のとおりです。

要素 バージョン / 場所
OS Ubuntu 26.04.1 LTS(x86_64)
実行ユーザー 一般ユーザー(本書では claude、グループ dev)
Unity CLI 1.0.0-beta.13(~/.local/bin/unity)
Unity エディタ 6000.3.24f1 + Linux Build Support (IL2CPP)
Pipeline パッケージ com.unity.pipeline 0.7.0-exp.1
Claude Code Unity プラグイン unity@unity-agent-plugin 0.1.2-beta

本書では、次のパスと名前を例として使います。環境に合わせて読み替えてください。

例 意味
/srv/work/unitygame 作業ルート(Claude Code のセッションもここで起動する)
/srv/work/unitygame/tools エディタ本体・補助スクリプト・ログの置き場
<project> Unity プロジェクトのフォルダ名(/srv/work/unitygame/<project>)
<editor-version> プロジェクトが使う Unity エディタのバージョン(例:6000.3.24f1)
unitygame Claude Code Remote Control のインスタンス名(作業ルートのフォルダ名と同じ)

1. OS の準備

1.1 パッケージ

sudo apt install -y \
  git libxml2-16 libglu1-mesa libopengl0 mesa-utils \
  xvfb dbus-x11 xdotool imagemagick \
  gnome-keyring python3-gi \
  clang lld cmake
パッケージ 用途
libxml2-16, libglu1-mesa, libopengl0, mesa-utils エディタの実行に必要なライブラリと、GL の動作確認
xvfb, dbus-x11 仮想ディスプレイと D-Bus セッション
xdotool, imagemagick 画面のないダイアログをクリックする、import で画面を撮る
gnome-keyring, python3-gi Unity CLI のサインイン情報の保存先と、その解錠スクリプト
clang, lld, cmake IL2CPP ビルド用(ビルドしないなら省略可)

Ubuntu 26.04 では libxml2 は libxml2-16 という名前です。古いリリースでは libxml2 を入れてください。

1.2 ユーザーとディレクトリ

sudo mkdir -p /srv/work/unitygame
sudo chown root:dev /srv/work/unitygame && sudo chmod 2775 /srv/work/unitygame
mkdir -p /srv/work/unitygame/tools

1.3 ログアウト後もユーザーサービスを動かす

sudo loginctl enable-linger claude
loginctl show-user claude -p Linger    # Linger=yes になればよい

2. Unity CLI

2.1 インストール

cd /tmp
curl -fsSL -o unity-cli-install.sh https://public-cdn.cloud.unity3d.com/hub/prod/cli/install.sh
UNITY_CLI_CHANNEL=beta bash unity-cli-install.sh
~/.local/bin/unity --version
  • 実行ファイルは ~/.local/bin/unity に入ります。~/.local/bin に PATH を通しておいてください。
  • 古い版が入るときは、unity self-update -y で最新にできます。直前の版には unity self-update --rollback で戻せます。

2.2 キーリング(サインイン情報の保存先)

Unity CLI は、サインイン情報を Secret Service(gnome-keyring)に保存します。
画面のない環境では起動時にキーリングを解錠する人がいないため、パスワードをファイルに置き、systemd で解錠します。

  1. パスワードファイルを作ります(本人だけが読める権限にします)。

    mkdir -p ~/.config/unity-headless && chmod 700 ~/.config/unity-headless
    (umask 077; printf '<任意のパスワード>' > ~/.config/unity-headless/keyring-pass)
    
  2. そのパスワードで login キーリングを作ります。

    systemctl --user stop gnome-keyring-daemon.service gnome-keyring-daemon.socket
    cat ~/.config/unity-headless/keyring-pass | gnome-keyring-daemon --daemonize --login --components=secrets,pkcs11
    cat ~/.config/unity-headless/keyring-pass | gnome-keyring-daemon --start --components=secrets,pkcs11
    ls ~/.local/share/keyrings/          # login.keyring ができていればよい
    systemctl --user start gnome-keyring-daemon.socket
    

    printf '\0' などで「空パスワード」を渡すと、NUL 文字がパスワードになってしまいます。その結果、既定のキーリングが作られず、unity auth login が KEYRING_WRITE_FAILED で失敗します。

  3. 解錠スクリプト tools/keyring-unlock.py を置きます(内容は付録 A)。
    gnome-keyring-daemon --unlock は、D-Bus 上の既存デーモンではなく別のデーモンを起動してしまいます。そのため、このスクリプトは D-Bus 経由で UnlockWithMasterPassword を呼んで解錠します。

  4. systemd ユニット unity-keyring-unlock.service を置きます(付録 B)。

2.3 サインインとライセンス

# ブラウザを開かせず、表示された URL を手元のブラウザで開いてサインインする
unset DISPLAY; export BROWSER=/bin/true
unity auth login
unity auth status          # "Signed in as ..." になればよい

# Personal ライセンス(オンライン有効化。Personal はオフライン(.alf)での有効化に対応していない)
unity license activate --personal --accept-eula
unity license status       # License: active
  • unity auth login は、URL を表示した後、サインインが終わるまで待ち続けます。リモートから実行する場合は、script -qfc "unity auth login" auth.log のように出力をファイルに残すと、URL を確認しやすくなります。
  • サインインは、再起動後も残ります(2.2 の解錠が前提です)。CLI beta.13 で再起動をまたいで保たれることを確認済みです。

3. Unity エディタ

3.1 インストール

unity editors install-path /srv/work/unitygame/tools/editors
unity install <editor-version> --list-modules          # モジュール ID を確認する
unity install <editor-version> -m <Linux Build Support (IL2CPP) の ID> --accept-eula -y
unity editors default <editor-version>
unity editors list
  • <editor-version> は、プロジェクトの ProjectSettings/ProjectVersion.txt の m_EditorVersion に合わせます(4.3)。
  • Unity Hub など別の方法で入れたエディタは、unity editors add <エディタのフォルダ> で CLI に登録できます。

3.2 初回起動時のダイアログ

画面がないため、初回起動ではモーダルダイアログでエディタが止まることがあります。止まっている間は Pipeline の API も応答しません。
これまでに出たダイアログは次のとおりです。

  • 利用規約(EULA)の確認
  • プロジェクトのアップグレード確認(エディタのバージョンを上げたとき)
  • URP マテリアルのアップグレード確認
  • 「シーンが外部で変更されました」

対処の流れは次のとおりです。

export DISPLAY=:99
import -window root /tmp/screen.png                      # 画面を撮って状況を見る
xdotool search --name "Unity" | while read w; do echo "$w $(xdotool getwindowname $w)"; done
xdotool mousemove <x> <y> click 1                         # ボタンをクリック

一度応答すれば、以降は出ません。


4. プロジェクト(git 管理された Unity プロジェクト)

ここでは、GitHub や GitLab などで git 管理されている既存の Unity プロジェクトを、このサーバーで開けるようにします。

4.1 git ホスティングへの接続(SSH)

  1. このサーバー用の鍵を作り、ホスティング側に登録します。

    ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_<host> -C "<サーバー名>"
    cat ~/.ssh/id_ed25519_<host>.pub
    
    登録先 向いている用途
    リポジトリの Deploy Key(push もするなら書き込み権限を付ける) そのリポジトリだけ触れればよい場合。権限が最小で済む
    ユーザーアカウントの SSH 鍵 複数のリポジトリを扱う場合
    • GitHub:リポジトリの Settings → Deploy keys(Allow write access)
    • GitLab:リポジトリの Settings → Repository → Deploy keys(Grant write permissions)
  2. ~/.ssh/config を書きます。22 番ポートが塞がれていることがあるので、各サービスの 443 番ポート用のホストを使うと確実です。

    # GitHub の例
    Host github.com
      HostName ssh.github.com
      Port 443
      User git
      IdentityFile ~/.ssh/id_ed25519_<host>
      IdentitiesOnly yes
    
    # GitLab の例
    Host gitlab.com
      HostName altssh.gitlab.com
      Port 443
      User git
      IdentityFile ~/.ssh/id_ed25519_<host>
      IdentitiesOnly yes
    
  3. 接続を確認します。GitHub は successfully authenticated、GitLab は Welcome to GitLab と出れば成功です。

    ssh -T git@github.com     # または git@gitlab.com
    

ssh だけが外へつながらない場合
構築実績のある環境で、ssh コマンドの外向き接続だけがタイムアウトし、Python からは同じ宛先に接続できる現象がありました(原因は不明)。
その場合は、各 Host に次の行を加えて、Python の中継スクリプト(付録 A.3)を経由させます。

  ProxyCommand /usr/bin/python3 /srv/work/unitygame/tools/ssh-tcp-relay.py %h %p

SSH が使えない場合は、HTTPS とアクセストークンでも取得できます。その場合、トークンは git credential の仕組みで管理し、リポジトリの設定ファイルや URL に直接書かないでください。

4.2 取得

cd /srv/work/unitygame
git clone git@<host>:<owner>/<repo>.git <project>
cd <project>
git switch <作業ブランチ>          # 必要なら
git lfs install && git lfs pull    # リポジトリが Git LFS を使っている場合(apt で git-lfs を入れておく)
  • AI 用の作業ブランチを分けておくと、人の作業と混ざらず、レビューもしやすくなります。
  • コミットする人の名前とメールは、サーバーの共通設定にせず、リポジトリ単位(git config user.name ...)かコミットごと(git -c user.name=... -c user.email=... commit)で指定すると、誰の変更か区別しやすくなります。

4.3 エディタのバージョンを合わせる

cat ProjectSettings/ProjectVersion.txt     # m_EditorVersion: <editor-version>
unity editors list                         # 同じバージョンが入っているか
  • 同じバージョンが入っていなければ、3.1 の手順で入れます。
  • 別のバージョンで開くと、プロジェクトのアップグレードが走り、多数のファイルが書き換わります。アップグレードする場合は、専用のブランチを作り、動作を確認してからコミットしてください。

4.4 Pipeline パッケージ(AI からエディタを操作するためのもの)

unity pipeline install --project-path /srv/work/unitygame/<project>
grep pipeline /srv/work/unitygame/<project>/Packages/manifest.json

Packages/manifest.json(と packages-lock.json)が変わります。この変更をコミットするかどうかは、運用に合わせて決めてください。

方針 利点 欠点
コミットして共有する このサーバーで特別な管理が要らない 他の開発者のエディタでも実験版のパッケージが読み込まれる。Windows でファイアウォールの確認画面が出ることがある
コミットしない 他の開発者に影響しない このサーバーで manifest.json の差分を手元に持ち続ける必要があり、pull のたびに衝突に気を付ける必要がある

パッケージはマシン内からの接続しか受け付けず、トークン認証も必須なので、どちらの方針でもセキュリティ上の差はほとんどありません(8 章)。

4.5 git 管理の前提

Unity プロジェクトの .gitignore で、少なくとも次が除外されていることを確認します(Unity 公式テンプレートの .gitignore に含まれています)。

[Ll]ibrary/
[Tt]emp/
[Oo]bj/
[Bb]uild/
[Bb]uilds/
[Ll]ogs/
[Uu]ser[Ss]ettings/
  • Library/ は、clone 直後には存在しません。初回にエディタで開いたときに作られ、規模によっては数分〜数十分かかります。
  • Pipeline の接続情報(Library/Pipeline/)も Library/ の中に作られるので、リポジトリには入りません。
  • UserSettings/ は人ごとの設定です。共有しないでください。

4.6 補助スクリプト

tools/ に次のファイルを置き、実行権限を付けます(内容は付録 A)。

ファイル 用途
keyring-unlock.py キーリングの解錠(2.2)
wait-editor-ready.sh エディタが Pipeline のツール一覧を返すまで待つ。中のプロジェクトのパスを書き換える
ssh-tcp-relay.py ssh の中継(4.1、必要な場合のみ)
chmod +x /srv/work/unitygame/tools/*.sh /srv/work/unitygame/tools/*.py

5. systemd で常駐させる

  1. 付録 B のユニットを ~/.config/systemd/user/ に置きます。

  2. 有効にして起動します。

    systemctl --user daemon-reload
    systemctl --user enable --now unity-xvfb.service unity-keyring-unlock.service unity-editor.service
    systemctl --user enable --now claude-rc@unitygame.service     # Claude Code を入れた後(6 章)
    

起動順は次のとおりです。

  1. Xvfb とキーリングの解錠
  2. エディタの起動
  3. wait-editor-ready.sh がツール一覧を確認するまで待つ(最大 5 分)
  4. Claude Code の起動

Claude Code の方が先に起動すると、MCP のツールが読み込まれません。そのため、エディタの準備完了を待ってから起動する順にしています。

ログの出力先は次のとおりです。

  • エディタ:/srv/work/unitygame/tools/editor.log
  • 各サービス:journalctl --user -u <ユニット名>

6. Claude Code

6.1 インストールと Remote Control

curl -fsSL https://claude.ai/install.sh | bash      # ~/.local/bin/claude
claude                                              # 初回のみ対話的にログイン

付録 B の claude-rc@.service と、その drop-in 2 つを置いて有効にします(5 章)。
インスタンス名(@ の後ろ)が /srv/work/<名前> の作業ディレクトリになります。
同じマシンで別のプロジェクト(例:claude-rc@webapp)を並べて動かせます。

6.2 Unity の MCP サーバー(unitygame のセッションだけで有効にする)

cd /srv/work/unitygame
claude mcp add -s local unity-editor-mcp -- /home/claude/.local/bin/unity mcp --project-path /srv/work/unitygame/<project>
claude mcp list        # unity-editor-mcp: ... ✔ Connected
  • -s local で登録すると、/srv/work/unitygame で起動したセッションでだけ有効になります。
  • unity mcp configure claude-code を使うと、ユーザー単位(全フォルダ共通)で登録されます。その場合、別プロジェクトのセッションにも Unity のツールが読み込まれてしまうので注意してください。

6.3 Unity 公式プラグイン(スキル)

Claude Code の中で次を実行します。

/plugin marketplace add Unity-Technologies/unity-agent-plugin
/plugin install unity@unity-agent-plugin

unity:unity-cli、unity:ui などのスキルが使えるようになります。構築実績は 0.1.2-beta です。


7. 動作確認

systemctl --user --no-pager list-units 'unity-*' 'claude-rc*'
journalctl --user -b -u unity-keyring-unlock -u unity-editor | grep -E "unlocked|ready"
#   → "keyring unlocked" と "editor ready: 151 tools"
unity auth status          # Signed in as ...
unity license status       # License: active
unity status               # 7800  ready  /srv/work/unitygame/<project>  <editor-version>
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:7800/api/status   # 401(トークンなしは拒否)で正常

Claude Code のセッションでは、editor_status などのツールが "status": "ready" を返せば完了です。

最後に、マシンを再起動して同じ結果になることを確認してください。


8. 既知の注意点

症状 原因と対処
unity auth login が KEYRING_WRITE_FAILED 既定のキーリングがない、または解錠されていない。2.2 の手順で login キーリングを作る
エディタが起動したまま API が応答しない モーダルダイアログで止まっている。3.2 の手順で画面を撮り、xdotool でクリックする
セッションに Unity のツールがない Claude Code がエディタより先に起動した。wait-unity-editor.conf の drop-in があるか確認し、セッションを再起動する
pkill -f <パターン> で自分のシェルまで終了する コマンドラインにパターン文字列を含むシェル自身にも一致するため。PID を確認してから kill する
unity license 実行時に、ライセンス管理ツールのログへ Unsupported protocol version エディタ同梱の古いライセンス管理ツールが、CLI beta.13 の通信方式を受け付けないため。CLI が別に起動したツールで処理するので、結果は正常
editor.log に Access token is unavailable; failed to update エディタを CLI を通さずに直接起動しているため。ライセンスはその後正常に読み込まれるので、無視してよい
Windows で Unity の通信許可を求めるファイアウォールの確認画面が出る Pipeline が全インターフェイスで待ち受けるため。外部からの接続はパッケージ側で 403 にするので、「許可しない」でよい

Pipeline パッケージのセキュリティ

  • 待ち受けは 0.0.0.0:7800 です(Mono の HttpListener の制約)。ただし、次の 3 つで守られています。
    • ループバック以外からの接続は 403 で拒否する
    • 全リクエストに Bearer トークンを要求する(なければ 401)
    • ブラウザからの Origin 付きリクエストは拒否する
  • トークンは、エディタを起動するたびに作り直されます。保存先は Library/Pipeline/.unity-pipeline-port(権限 600、git 管理外)です。

9. 秘密情報(リポジトリや共有場所に置かないもの)

もの 場所
キーリングのパスワード ~/.config/unity-headless/keyring-pass(600)
Unity のサインイン情報 gnome-keyring の login キーリング(~/.local/share/keyrings/)
Unity のライセンス unity license activate で取得(マシンに紐づく)
git ホスティング用の SSH 鍵 ~/.ssh/id_ed25519_<host>
Claude Code の認証 ~/.claude/

付録 A. 補助スクリプト

A.1 tools/keyring-unlock.py

#!/usr/bin/env python3
# gnome-keyring-daemon --unlock spawns a separate daemon instead of unlocking the one
# owning the session bus, so unlock through gnome-keyring's internal D-Bus interface.
import sys
import time
from pathlib import Path

from gi.repository import Gio, GLib

SVC = "org.freedesktop.secrets"
ROOT = "/org/freedesktop/secrets"
COLLECTION = f"{ROOT}/collection/login"
PASSWORD = Path.home().joinpath(".config/unity-headless/keyring-pass").read_bytes().strip()


def call(bus, path, iface, method, args, reply_type=None):
    return bus.call_sync(SVC, path, iface, method, args,
                         GLib.VariantType(reply_type) if reply_type else None,
                         Gio.DBusCallFlags.NONE, 10000, None)


def unlock():
    bus = Gio.bus_get_sync(Gio.BusType.SESSION, None)
    locked = bus.call_sync(SVC, COLLECTION, "org.freedesktop.DBus.Properties", "Get",
                           GLib.Variant("(ss)", ("org.freedesktop.Secret.Collection", "Locked")),
                           GLib.VariantType("(v)"), Gio.DBusCallFlags.NONE, 10000, None).unpack()[0]
    if not locked:
        return
    _, session = call(bus, ROOT, "org.freedesktop.Secret.Service", "OpenSession",
                      GLib.Variant("(sv)", ("plain", GLib.Variant("s", ""))), "(vo)").unpack()
    call(bus, ROOT, "org.gnome.keyring.InternalUnsupportedGuiltRiddenInterface",
         "UnlockWithMasterPassword",
         GLib.Variant("(o(oayays))", (COLLECTION, (session, b"", PASSWORD, "text/plain"))))


for attempt in range(30):
    try:
        unlock()
        print("keyring unlocked")
        sys.exit(0)
    except GLib.Error as e:
        print(f"attempt {attempt + 1}: {e.message}", file=sys.stderr)
        time.sleep(2)
sys.exit(1)

A.2 tools/wait-editor-ready.sh

#!/usr/bin/env bash
# Exit 0 once the Editor serves its Pipeline tool list (port open alone is too early: the main thread may still be loading).
deadline=$((SECONDS + ${1:-300}))
while (( SECONDS < deadline )); do
  n=$(timeout 20 /home/claude/.local/bin/unity list --project-path /srv/work/unitygame/<project> --format tsv 2>/dev/null | tail -n +2 | wc -l)
  if (( n > 0 )); then
    echo "editor ready: $n tools"
    exit 0
  fi
  sleep 3
done
echo "editor not ready before timeout" >&2
exit 1

A.3 tools/ssh-tcp-relay.py

#!/usr/bin/env python3
"""TCP relay for ssh ProxyCommand.

The ssh binary on some machines cannot open outbound sockets (connect() times out
for every host/port), while Python can reach the same endpoints fine. Relaying the
transport through stdio sidesteps ssh's own connect.
"""
import socket
import sys
import threading

host, port = sys.argv[1], int(sys.argv[2])
sock = socket.create_connection((host, port), timeout=20)
sock.settimeout(None)


def pump(read, write, close):
    try:
        while True:
            chunk = read()
            if not chunk:
                break
            write(chunk)
    finally:
        close()


threading.Thread(
    target=pump,
    args=(lambda: sys.stdin.buffer.read1(65536),
          lambda b: (sock.sendall(b)),
          lambda: sock.shutdown(socket.SHUT_WR)),
    daemon=True,
).start()

pump(lambda: sock.recv(65536),
     lambda b: (sys.stdout.buffer.write(b), sys.stdout.buffer.flush()),
     lambda: None)

付録 B. systemd ユーザーユニット

置き場所はすべて ~/.config/systemd/user/ です。

unity-xvfb.service

[Unit]
Description=Xvfb virtual display :99 for Unity Editor

[Service]
Type=simple
ExecStart=/usr/bin/Xvfb :99 -screen 0 1920x1080x24 -nolisten tcp
Restart=on-failure
RestartSec=5

[Install]
WantedBy=default.target

unity-keyring-unlock.service

[Unit]
Description=Unlock login keyring for Unity CLI tokens
After=dbus.socket gnome-keyring-daemon.socket

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/bin/python3 /srv/work/unitygame/tools/keyring-unlock.py

[Install]
WantedBy=default.target

unity-editor.service

[Unit]
Description=Unity Editor (<project>) with Pipeline API on port 7800
Requires=unity-xvfb.service
After=unity-xvfb.service unity-keyring-unlock.service
Wants=unity-keyring-unlock.service

[Service]
Type=simple
Environment=DISPLAY=:99
WorkingDirectory=/srv/work/unitygame/<project>
ExecStartPre=/bin/bash -c 'for i in $(seq 1 30); do [ -S /tmp/.X11-unix/X99 ] && exit 0; sleep 1; done; exit 1'
ExecStart=/srv/work/unitygame/tools/editors/<editor-version>/Editor/Unity -projectPath /srv/work/unitygame/<project> -logFile /srv/work/unitygame/tools/editor.log
# Started = Pipeline tools listable. Non-fatal and bounded, so units ordered after this never wait more than ~5 min.
ExecStartPost=-/srv/work/unitygame/tools/wait-editor-ready.sh 300
TimeoutStartSec=360
TimeoutStopSec=60
Restart=on-failure
RestartSec=15

[Install]
WantedBy=default.target

claude-rc@.service

[Unit]
Description=Claude Code Remote Control (%i)
[Service]
WorkingDirectory=/srv/work/%i
ExecStart=%h/.local/bin/claude remote-control --name "%i"
Restart=always
RestartSec=10
[Install]
WantedBy=default.target

claude-rc@.service.d/wait-net.conf(全インスタンス共通:名前解決ができるまで待つ)

[Service]
ExecStartPre=/bin/sh -c 'until getent hosts api.anthropic.com >/dev/null; do sleep 2; done'
TimeoutStartSec=300

claude-rc@unitygame.service.d/wait-unity-editor.conf(unitygame だけ:エディタの準備完了を待つ)

[Unit]
Wants=unity-editor.service
After=unity-editor.service
2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?