unreagent
A small launcher / orchestrator for Unreal Engine projects: it starts and supervises the Unreal Editor and an agent (e.g. Claude Code), and gives the agent an MCP server to drive the editor, trigger builds, read logs, and run Python/Node code in prepared runtimes.
A single .exe — cross-compilable from Linux to Windows. Stdlib Go only,
plus one vendored YAML parser; vendor/ is checked in, so builds are
offline-reproducible.
Full working example — a real UE project wired to Claude Code with both MCP servers running: unreagent-example.
Architecture
+----------------------- unreagent.exe -----------------------+
| Supervisor MCP server (HTTP :8765) |
| * ue (Job Object) <--- status / logs |
| * agent (Job Object) ue_start / stop / restart |
| * restart policy + backoff run_command (compile, ...) |
| * ring-buffer logs run_python / run_node |
| | approve (permissions) |
| v ^ |
| UnrealEditor.exe | |
| +- in-editor MCP plugin | |
| (e.g. UE LLM Toolkit, | |
| HTTP :3000) | |
| ^ | |
| Node bridge (stdio MCP) | |
| ^ | |
| Claude Code ----------+--------- unreagent-MCP |
| +----------> in-editor MCP |
| (both via --mcp-config) |
+------------------------------------------------------------+
Two MCP servers serve the agent:
- unreagent-MCP (this launcher, HTTP) — process layer: start / stop / restart the editor, compile, logs, run_python / run_node, permissions.
- In-editor MCP (e.g. UE LLM Toolkit, running inside the engine) — content layer: blueprints, assets, levels, UE Python. Setup: In-editor MCP setup.
The launcher hands the agent both via --mcp-config (mcp.extraServers in
the config), so the agent does not have to wire anything up itself. Stdio
bridges are fully checked before the agent starts, with each step logged
loudly, instead of letting the agent run into a meaningless -32000
connect error: binary on PATH? bridge script present? node_modules there
(or run npm install)? does the bridge answer a real initialize handshake
(smoke test)? And once the in-editor server (UNREAL_MCP_URL) is reachable,
that's logged too — or a warning with the usual suspects if it isn't.
The same config can additionally be written to a file for external clients
(your own Claude session, Cursor, VS Code). Via mcp.writeConfig (a list of
{path, format}, format mcp_json or vscode) or the flag
-write-mcp-config <path>. Handy with -no-agent: the launcher writes the
.mcp.json and keeps UE + MCP running, you connect externally.
Building
Requirement: Go (>= 1.19). Dependencies are vendored (vendor/); builds run
offline.
make windows # -> dist/unreagent.exe (cross-compile from Linux)
make linux # -> dist/unreagent (for local tests)
make all
Or directly:
GOOS=windows GOARCH=amd64 go build -ldflags "-s -w" -o dist/unreagent.exe ./cmd/launcher
Windows: metadata & "Unknown Publisher"
The .exe carries an icon + version info (CompanyName, ProductName,
description, copyright) and an asInvoker manifest (no UAC elevation
prompt). This fills Properties -> Details and the program name in dialogs.
The "Unknown Publisher" warning itself comes from the signature, not from metadata. Two paths:
A) Self-signed (in the repo, for your own team / known users) — the
.exe is signed with a self-signed certificate from "conflict.industries
digital GmbH". Users import the public certificate once:
powershell -ExecutionPolicy Bypass -File signing\import-cert.ps1 # as admin
After that: no publisher warning, the GmbH appears as publisher. Details
and trade-offs: signing/. (The private key is not in the
repo.)
B) CA certificate (for a wider audience) — an OV / EV code-signing
cert does not need user-side import (EV gives immediate SmartScreen
reputation). With such a cert you also sign the .exe from Linux
(osslsigncode).
Change publisher / version: cmd/launcher/versioninfo.json -> make resource.
Try it now (demo)
The release zip ships a ready-to-go demo unreagent.yaml that runs without
UE and without an agent (ping as a placeholder "editor"). Just unpack the
zip and start unreagent.exe — the MCP server will then run on
http://127.0.0.1:8765/mcp. Details: example/. In the console:
status, logs, c hello, r ue, q.
The "LLM thing" (Claude Code) is not included — it is a CLI you install separately. The demo therefore leaves the agent off; to arm it, set
agent.enabled: true(see below).
Use example/ to poke at the MCP server without installing UE or an agent.
For the real thing — a UE project with Claude Code driving both
unreagent-MCP and the in-editor MCP together — clone the standalone
unreagent-example repo:
git clone --recursive https://github.com/Getty/unreagent-example
Setup
The launcher is dropped into the UE project and the config is committed:
- Put
unreagent.exe(from the release) next to the.uproject— gitignored. - Rename
unreagent.example.yaml->unreagent.yaml, place it next to the.uprojectand commit it to the repo. This file is portable. - Start
unreagent.exe(double-click or console). Alternative config path via-config <path>.
unreagent.yaml contains no machine-specific paths. Editor, compile and
package are auto-derived. A minimal config is enough:
agent: { enabled: true, command: claude, claudeIntegration: true }
permissions: { enabled: true, mode: allow_all, deny: ["Bash(rm -rf *)"] }
runtimes: { python: { enabled: true }, node: { enabled: true } }
mcp: { enabled: true }
Portability: paths are resolved at runtime
Machine-specific overrides go in a git-ignored unreagent.local.yaml,
which is overlaid on top of unreagent.yaml:
# unreagent.local.yaml (DO NOT commit)
engineRoot: "D:/UE/UE_5.7" # if auto-detect fails
agent: { command: "C:/.../claude.cmd" } # if claude is not on PATH
Recommended .gitignore entry in the UE project:
unreagent.exe
unreagent.local.yaml
When the agent ends (/quit or crash)
In window mode the agent is the lead process — if it ends (e.g. /quit) and
is not restarted, the launcher prompts on the console (agent.onExit: ask,
the default):
Agent ended.
[Enter] shut down everything (UE + launcher)
[k] keep editor running, drop to launcher console
[r] restart the agent
> _ (30s -> shut down everything)
Without a TTY (headless -p), it shuts down cleanly instead. Alternatives:
onExit: shutdown (always shut everything down immediately) or onExit: leave
(everything keeps running, warning only — manual Ctrl-C required).
In-editor MCP: UE LLM Toolkit setup (Windows)
For the agent to work inside the editor (blueprints, assets, levels, UE
Python), the UE project needs the in-editor plugin. The short version:
drop the plugin into the Plugins/ folder — done. No .uproject patching,
no enabling in the plugin browser.
What the machine must have
Steps
-
Drop the plugin into the project — copy or clone the repo/folder to
<Project>/Plugins/UELLMToolkit/. The.upluginhasEnabledByDefault: trueand activates its own engine dependencies (EditorScriptingUtilities, Niagara, ControlRig, IKRig, ...) — nothing to change on the.uproject. -
First editor start — UE reports "Missing Modules ... rebuild?" -> Yes. The build needs Visual Studio (once; afterwards the binaries live in the plugin). Alternatively ahead of time via the launcher: console ->
c compile. -
Done. The plugin's HTTP server starts with the editor automatically on port
3000. The launcher forwards the bridge via--mcp-configto the agent — for that, inunreagent.yaml:mcp: enabled: true extraServers: ue-llm-toolkit: command: node args: ["${PROJECT_DIR}/Plugins/UELLMToolkit/Resources/mcp-bridge/index.js"] env: UNREAL_MCP_URL: "http://127.0.0.1:3000" -
Optional: For
unreal_execute_scriptwith Python scripts, enable the UE plugin "Python Editor Script Plugin" (Edit -> Plugins) — the other tools don't need it.
What the launcher does automatically on startup (see above): install
bridge node_modules, check binary / script, initialize smoke test, wait
for port 3000 — every step with a clear message in unreagent.log.
Access from another machine
The UE HTTP server binds to localhost by default. If a remote Claude session
should reach the in-editor server (port 3000) directly:
; Config/DefaultEngine.ini of the UE project
[HTTPServer.Listeners]
DefaultBindAddress=any
Plus a Windows firewall rule for port 3000 — and only in trusted networks,
the server controls the entire editor. On the other side UNREAL_MCP_URL
then points to http://<windows-ip>:3000.
Troubleshooting: "Failed to reconnect: -32000"
-32000 from Claude Code just means "bridge process immediately gone again"
— the real cause is in unreagent.log (check chain) or in Claude Code's MCP
logs (%LOCALAPPDATA%\claude-cli-nodejs\Cache\<project>\mcp-logs-*). Usual
suspects, in this order:
- Bridge path wrong (
Cannot find module ...) — check the path inextraServersagainst the real plugin layout. - Node missing or not on PATH in the agent's session.
node_modulesmissing — this no longer happens, the launcher installs them.- Editor (not yet) reachable — port 3000 only appears once the editor is fully up; the launcher logs once it is.
Configuration (quick reference)
No process zombies (Windows)
Every running instance is attached to a Windows Job Object with
KILL_ON_JOB_CLOSE. If the handle closes — by clean stop, restart,
or crash / hard-kill of the launcher — the operating system terminates
the entire process tree (including ShaderCompileWorker etc.). OS-enforced,
no external dependency. On Linux (development) only the direct child is
killed.
Crashes & recovery (unattended)
Two UE dialogs otherwise block agent-driven operation: the crash reporter
("Send and Restart / Close") and the recovery prompt on restart
("not closed cleanly, restore?"). The launcher therefore starts the editor
with -unattended — this suppresses both in UE 5.7 code (crash
dialog and both recovery systems: PackageAutoSaver and disaster recovery)
and discards the unclean state instead of asking.
If a human is working interactively in the editor in parallel, set
unattended: false. In addition (so the reporter never even sends/starts)
in the project:
; Config/DefaultEngine.ini
[CrashReportClient]
bAgreeToCrashUpload=false
bImplicitSend=false
[/Script/DisasterRecoveryClient.DisasterRecoverClientConfig]
bIsEnabled=false
Permission layer
With permissions.enabled + agent.claudeIntegration the launcher starts
the agent with --permission-prompt-tool mcp__unreagent__approve — but
only in headless mode (agent.args contains -p/--print, or
agent.window: false). Every permission prompt from Claude Code then goes
through the approve tool, which decides per the configured policy.
Interactive window mode (the default) does not enforce the policy this way. With
mode: allow_allthe launcher passes--dangerously-skip-permissionsinstead (Claude Code's own prompting is skipped entirely); with any other mode no flag is added and Claude Code prompts the user directly in the window, same as without unreagent. To have the policy actually enforced, run headless or drive Claude Code externally against the MCP server (-no-agent).
Modes: allow_all = auto-approve everything, deny rules still apply (e.g.
Bash(rm -rf *)); allowlist (the default when mode is unset) = deny
everything except what an allow rule matches — the only mode that fails
closed by construction; deny_all = reject everything.
Bash(...) rules are matched per shell segment, not against the whole
command line: the command is split on ;, &&, ||, |, newlines, on
$(...)/backtick bodies, { ... } groups, and on the script handed to a
nested bash -c/sh -c. A deny rule matches if any segment matches
(so cd /tmp && rm -rf / cannot walk past a deny rule for Bash(rm -rf *));
an allow rule requires every segment to be covered by some allow rule
(so git status | head needs both Bash(git *) and Bash(head *), and
cd /proj && git status needs both Bash(cd *) and Bash(git *)). Deny
matching additionally normalizes whitespace/quotes/escapes, reduces a
path-qualified command to its base name (/bin/rm -> rm), and looks
through wrapper commands (sudo, env, eval, xargs, ...) at what they
wrap. This is a heuristic, not a shell parser — it cannot follow variable
indirection, aliases, or data piped into an interpreter (python -c ...). A
deny list under allow_all is a guard rail, not a security boundary.
Bracket rules also work on non-Bash tools, matched against the tool's
path-like input — Write(/etc/*), Read(/secrets/*),
mcp__unreagent__write_file(/etc/*). A rule whose tool carries no evaluable
input field fails closed: a deny rule still blocks, an allow rule still does
not grant.
Windows: claudeIntegration sets CLAUDE_CODE_USE_POWERSHELL_TOOL=1 by
default (see the agent row above), so Claude Code's shell tool is named
PowerShell, not Bash — write rules as PowerShell(...) there (e.g.
deny: ["PowerShell(Remove-Item *)"]). The segment splitter above is
POSIX-shaped; there is no PowerShell-specific tokenizer yet.
Note: the exact input schema of
--permission-prompt-toolis not officially documented by Anthropic. Theapprovehandler reads the tool fields defensively (tool_name/toolName/name,tool_input/input/arguments). If a Claude Code update changes the format, only that one handler incmd/launcher/main.goneeds adapting.
Hermes Agent (alternative agent)
Hermes Agent (Nous Research)
is an autonomous agent with its own MCP client — useful as an alternative to
Claude Code. unreagent plugs in by registering the same MCP server
(ue_start / ue_stop / logs / run_command / run_python / …) under
Hermes' mcp_servers: block. The launcher does not auto-merge on
normal runs; you opt in once with a sub-command:
# unreagent.yaml
agent:
hermes:
enabled: true
unreagent hermes-setup # merges MCP entry into ~/.hermes/config.yaml
What hermes-setup does, in order:
- Resolves the target config: explicit
agent.hermes.configPath→$HERMES_HOME/config.yaml→~/.hermes/config.yaml. - If
mcp.tokenis empty, generates 32 random bytes (hex) and writesmcp.token:intounreagent.local.yaml(git-ignored). The launcher reads it back on the next start, so the token survives restarts. - Backs up an existing Hermes config to
<path>.bak(once; never overwrites a previous backup). - Sets
mcp_servers.unreagent: { url, headers: { Authorization: Bearer } }in the Hermes config. A second run overwrites the entry rather than duplicating it — the operation is idempotent. - Writes the file atomically (temp + rename) so a crash mid-write cannot leave a half-written config.
The MCP server enforces Authorization: Bearer <token> whenever
mcp.token is set — including for the embedded Claude Code agent, which
gets the matching headers: block in its --mcp-config automatically.
The token check uses crypto/subtle.ConstantTimeCompare; the empty-token
default (the historic behaviour) keeps the server open on 127.0.0.1.
Restart Hermes after running hermes-setup so it picks up the new entry.
Runtimes for the agent
run_python executes code via uv run python — uv builds / syncs the venv
automatically from pyproject.toml / requirements and installs the right
Python version on demand. run_node runs JS in the project context. Both
tools write the script into the runtime's working directory (not a system
temp directory) and run it from there, so run_python can import modules
that sit next to pyproject.toml and run_node resolves bare imports
through the project's node_modules. The agent does not have to analyse
or set up the environment itself; the instructions are in the MCP tool
description and are therefore automatically in context.
MCP tools
MCP server hardening
The /mcp HTTP endpoint rejects malformed or unexpected requests instead of
guessing:
- Every POST must carry
Content-Type: application/json— anything else gets415 Unsupported Media Type(the manual test below already sets it). - A request that carries an
Originheader is rejected with403 Forbiddenunless the origin is loopback (http/httpsonlocalhost,127.0.0.1or::1, any port) — a defence against a web page in the user's browser driving the launcher (CSRF/DNS-rebinding). Requests without anOriginheader — every non-browser MCP client, including Claude Code andcurl— are unaffected. The allowlist is not yet configurable, so a browser-hosted client on another origin cannot be pointed at unreagent today. initializenegotiatesprotocolVersionagainst2025-06-18(default) and2025-03-26; any other value is answered with the default rather than echoed back, since this server implements only the Streamable-HTTP transport (no SSE, no sessions).- Connection timeouts guard against a wedged socket:
ReadHeaderTimeout10s,ReadTimeout60s,IdleTimeout120s.WriteTimeoutis 4h — the ceiling for a single tool call, so a long-runningrun_command(e.g. a full UE build) has room; it is a backstop, not a request budget.
CLI flags
Sub-commands
How the agent runs (three modes)
Claude Code is an interactive TUI and needs a TTY — as a raw background subprocess it won't start. Hence three ways:
- Interactive in the foreground (
agent.window, on by default) — the agent inherits the real console (TTY) and runs as an interactive TUI in the window you startedunreagentin. UE + MCP run in the background, launcher logs go tounreagent.log(next to the project) so the agent has the window to itself. Disable withwindow: false; off in-pmode. - Headless / task (
agent.args: ["-p", "<task>"]) — one-shot task without a window, then the agent exits. - External (
-no-agent) — the launcher does not start an agent; you connect your own Claude session with the.mcp.json/ the MCP server.
Headless / external agent
With -no-agent everything runs without an embedded agent — UE + MCP server.
That's how you hook up your own Claude Code to work on the project "without
anyone being present locally":
unreagent.exe -no-agent -files # UE + MCP + file tools, no embedded agent
# in your Claude Code session:
claude --mcp-config '{"mcpServers":{"unreagent":{"type":"http","url":"http://127.0.0.1:8765/mcp"}}}'
For access from another machine set mcp.address to 0.0.0.0:8765 (only
in trusted networks — the server then has file-write access).
Manual control (stdin)
status · r [name] (restart) · start <name> · stop <name> ·
c <name> (run command) · q (quit)
Manual MCP test
curl -s -X POST http://127.0.0.1:8765/mcp -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'