Install
Requirements
| What | Version |
|---|---|
| Godot | 4.7.0 or newer |
| Node, or Bun | Node 22 or newer for npx, or Bun 1.4 or newer for bunx |
One of the two runtimes, not both: npx ships with Node, bunx with Bun, and the server runs
under either.
setup finds Godot itself, in the usual install locations. Set GODOT_PATH to the binary if
yours is somewhere else, and it will write that path into the config it generates. Without either,
upgrade and the other commands use the engine the project's own config names, and in a git
worktree with no config of its own, the one the main checkout's config names.
Hand it to your agent
Install gdharness here by following https://aureliolo.github.io/gdharness/agent.md,
then tell me what it asked you to recommend back to me.The agent reads agent.md, runs the one command below, answers the questions it asks from what it finds in the project, and comes back with what it chose and why.
Or one command yourself
npx -y gdharness@1.3.45 setupThat installs the addons, enables the editor plugins, registers the runtime autoload, rebuilds the class list, writes the gdharness skill, and registers the server with your harnesses.
Then reconnect the harness so it spawns the server, and check editor_status answers.
It asks
Run at a terminal with no harness named, it asks about each one it finds, and writes nothing you did not answer for:
Claude Code is set up here. Add gdharness to it? [Y/n]
Cursor is installed. Set it up for this project? [Y/n]
Codex CLI is on this machine and has no project-level config.
Write ~/.codex/config.toml? That affects every project. [y/N]Name harnesses by flag and it asks nothing, which is how a script or an agent runs it. With no terminal and no flags, it writes the harnesses this project already uses and names the rest, so it can never hang waiting for an answer nobody can give.
# exactly these two, no questions
npx -y gdharness@1.3.45 setup --cursor --vscode
# yes, write the machine-wide one
npx -y gdharness@1.3.45 setup --codex
# no questions, and nothing outside the project
npx -y gdharness@1.3.45 setup --yes
# the addons only, no configuration at all
npx -y gdharness@1.3.45 setup --no-connectWhat it writes, and where
Nothing outside the project directory without a flag or a typed yes. 11 of the 35 have no project-level config at all, and for those a yes writes the machine-wide file, because that is their limitation rather than a choice we can make better.
Claude Code, Copilot CLI, Qoder, Command Code: written /home/you/game/.mcp.json
skill: written /home/you/game/.agents/skills/gdharness
skill: written /home/you/game/.claude/skills/gdharness4 harnesses read the same .mcp.json, so it is written once and all of them are
named. The skill goes to .agents/skills/, which 32 of them read, and to its
own directory for each of the 3 that do not. --no-skill leaves it out.
How it works has every harness, its file, and what setup decides.
The entry is the same everywhere:
{
"mcpServers": {
"gdharness": {
"command": "npx",
"args": ["-y", "gdharness@1.3.45"],
"env": {
"GODOT_PATH": "/path/to/godot",
"GDHARNESS_PROJECT": "/path/to/project",
},
},
},
}The version is pinned rather than latest. The server and the addons it installed have to match,
and latest is how they drift apart: editor_status reports that as addonIsStale.
GDHARNESS_PROJECT is which project this server was set up for. It writes the editor bridge's
address into that project so the editor can find it wherever it landed, which is what lets two
projects be open at once: see how it works. Leave it out, as a
config written by hand does, and the bridge holds out for its fixed port.
Every tool call still carries its own projectPath. Nothing else is read from the environment.
An existing config keeps everything already in it, including other servers. One that does not parse is refused rather than replaced.
Committing what it installed
The four addons land in addons/, and they are yours to commit like any other Godot addon.
Neither Godot's version-control page nor GitHub's Godot.gitignore excludes addons/, so a
project that has not chosen otherwise commits them and everybody on the team has the same version
without running anything.
Two of them, gdharness_editor and auto_reload, are also named in project.godot's
editor_plugins/enabled, which almost every project commits.
That is why the files are worth committing alongside it. A clone that has the entries without
the addons opens the editor, prints one warning per missing addon, Addon ... failed to load. No directory found, and carries on: a warning rather than an error, and project.godot is not
rewritten. So a teammate who has not installed gdharness loses nothing but quiet.
Every script arrives with the .uid file that fixes its identity, so two clones commit the same
one and no editor has to invent it.
If your project gitignores addons/, which is what the package-manager add-ons like
godam and GLAM are
for, keep the version somewhere committed and install from it instead. Each addon records its own
at addons/<name>/.gdharness-version, which is what gdharness doctor reads.
The runtime autoload does not stand in the way. project.godot is committed, and its runtime
entry names res://gdharness/loader.gd rather than the addon, so a clone or a CI runner without
addons/ boots cleanly: the loader is committed with it and does nothing until the addon is there.
Commit the loader. gdharness doctor asks git which of the files your autoloads name are in the
index, which is what a clone gets. A file nobody added is a problem, which fails setup and
doctor. A file your .gitignore matches is named on a line of its own and is not a problem,
because ignoring it says your own install step puts it there before setup runs. Outside a
repository, or with no git on the machine, it says nothing rather than guessing. See
the runtime autoload.
Check it works
| Call | Expected |
|---|---|
editor_status, nothing open |
Reports no editor. Does not fail. |
editor_status, editor open |
connected true, addonVersion equal to serverVersion. |
project_info |
The project name and main scene. |
editor_run start, editor_output, stop |
The game starts, its console comes back, it stops. |
gdharness doctor exits 1 on any problem and names it.
A windowed game this server starts goes on a desktop of its own on Windows, out of sight, and makes
no sound: it is started with --audio-driver Dummy, and so is an editor editor_launch opens.
Headless runs and test runs are silent already. silent: false on editor_run start or on
editor_launch asks for sound. The editor takes no engine flags for a game it plays, so a silent
windowed run is started by the server, whose own debugger holds it, and the debug_* tools answer
for it as for one the editor plays; silent: false plays it through the editor, with sound.
On that desktop Windows refuses to say where the pointer is, since it answers only for the desktop
in use. Injected pointer input reaches the controls under it, their hover and tooltips, and the
game's input handlers, but get_mouse_position() on the root window, and the positions derived
from it, read a meaningless position: the engine asks Windows and does not check the refusal. The
pointer answers from runtime_input say so under pointer_note for a game on that desktop.
That desktop is in the logon session's non-interactive window station, which keeps a game's
ClipCursor and SetCursorPos off the pointer in use and has no compositor:
DwmIsCompositionEnabled answers false there. A layered window (WS_EX_LAYERED, as a click-through
overlay makes) takes SetLayeredWindowAttributes once, and after it has been hidden and shown again
the call fails with ERROR_GEN_FAILURE (31). A game that makes its window layered can tell it is
there by that answer, or by GDHARNESS_DESKTOP, which names the desktop to every process started
on it, and leave the window plain.
editor_launch restart waits ninety seconds for the old editor to close and ninety more for the new
one to connect. A project whose import takes longer than that sets
GDHARNESS_EDITOR_RESTART_TIMEOUT_MS in the server's environment.
The editor's language server reads every script in the project before it answers the first request
after the editor starts, and the server waits three minutes for that. A project whose language
server reads for longer sets GDHARNESS_LSP_INITIALIZE_TIMEOUT_MS.
Three shorter waits can be set the same way, in milliseconds: GDHARNESS_SCAN_WAIT_MS, how long a
start waits for the editor to finish scanning before it starts anyway and says so (30 seconds);
GDHARNESS_STOP_WAIT_MS, how long a stop waits for what it ended to be gone before answering that it
is still running (10 seconds); and GDHARNESS_GREETING_WAIT_MS, how long a restart waits for a newly
connected editor to say who it is (10 seconds).
GDHARNESS_DEBUG=1 writes the server's debug lines to stderr, where your harness's MCP log keeps
them, and runs headless engines with --debug-godot.
Updating
The server asks the npm registry every ten minutes, in the background, while it is being used, and
once when it starts, and tells your agent when there is a newer release. One request to one host,
carrying nothing about you or the project. GDHARNESS_NO_UPDATE_CHECK=1 in the server's environment
stops it, and GDHARNESS_REGISTRY=https://your.mirror asks somewhere else instead, https only.
npx -y gdharness@<new> upgradeupgrade asks nothing: it reinstalls the addons at the new version and re-pins every config that
already names gdharness, and touches no config that does not. Then two things it cannot do for you,
because the old version keeps answering until they are done:
- Reconnect the MCP server so the harness re-spawns it. In Claude Code,
/mcpand reconnect. Restarting the harness is not required. - Restart an open editor if
upgradesays the editor addons changed:editor_launch restart, about seven seconds. An editor gdharness opened comes back visible, and on Windows shown without taking the focus, or headless and out of sight when the restart is givenhidden: true. A headless editor you started by hand cannot be restarted this way and has to be started again by hand.
editor_status confirms: addonIsStale false. addonVersion stays at the older number when the
upgrade left the editor code unchanged, because staleness is decided by the code the editor
loaded, not by the version number on it.
A git worktree of the project keeps its own untracked addons/, which an upgrade of the main
checkout does not reach. upgrade names each worktree whose addons are on another version, with the
command to run there; it does not upgrade them itself, since a worktree pins its own version and
can be on a branch that wants the one it has.
The rest of the CLI
| Command | What it does |
|---|---|
setup |
Addons in, editor plugins on, class list rebuilt |
setup --no-connect |
The same, and no harness configuration written |
upgrade |
The same project, on this version |
uninstall |
All of it back out again |
doctor |
Says what holds and what does not; exits 1 on a problem |
classes |
Rebuilds the class cache from disk |
runtime on / off |
Registers the runtime autoload, or takes it out again |
harnesses |
Every harness, its flag and the file it reads |
Each of these except harnesses takes the project directory as its argument and defaults to the
one you are in. It has to hold a project.godot either way, so the default cannot act on somewhere
that is not a Godot project. An unknown flag stops the command before it does anything.
setup copies each addon whole and writes the version beside it, so doctor can tell an old copy
from the shipped one. An editor that was already open keeps serving the addon it loaded at startup
until it is restarted.
Uninstalling
gdharness uninstallThe addons, the editor plugin entries, the runtime autoload, the skill, and gdharness's own entry
in every config it can parse. Other servers in those files keep their entries and the file stays; a
file that held nothing but gdharness goes with it, and so does the .agents/skills directory it
created. A harness's own directory is left alone, empty or not, because it is theirs.
A machine-wide config may be serving another project, so it is named rather than edited:
Codex CLI: left alone. Its config is machine-wide and may serve
another project; pass --codex to remove it.Writes to project.godot go through the engine, so the file is written the way the editor writes
it. That includes what the editor does to it: a comment you added by hand is dropped, and the file
gets Godot's own header back, as on any save of the project settings.
The runtime autoload
setup registers it, because without it the runtime_* tools have nothing to talk to. Like
everything else setup installs it belongs to that project alone: its own copy of the addon, its
own entry in that project's project.godot. Twenty Godot projects means twenty independent
installs, each pinned to its own version and its own engine. --no-runtime leaves it out.
The entry names res://gdharness/loader.gd, a loader setup writes there, outside addons/, so
that it is committed with project.godot. The loader brings the runtime up only in a debug build
that has the addon, as a child of its own node, and does nothing anywhere else: a release export
carries it and nothing comes up, and the runtime itself also refuses to serve outside a debug build.
The runtime comes up before the main scene's _ready, so a scene that does its work there can be
reached while it does. gdharness doctor says when the loader differs from the one this gdharness
ships, and setup and upgrade write it again.
An entry naming the addon's own script, which earlier versions wrote, boots a build without
addons/ with a missing script, so setup, upgrade and runtime on point it at the loader. An
entry naming a loader of your own is left where you put it, and each of them says so and how to use
the shipped one instead: take your entry out of [autoload], delete your loader, and run
gdharness runtime on. An upgrade once repointed such a loader and mentioned five other
replacements but not that one, and nothing failed: the loader was still on disk and still correct,
so every gate the project had went on passing while project.godot no longer named it.
Autoloads come up for godot -s too, which is how a test tier and most batch tools are run. It
refuses to serve those as well: a script run has no game in it to answer about, and a project whose
suite starts sixteen engines at once would otherwise announce sixteen games under its own path and
answer a runtime_* call from whichever replied first. Set
gdharness/runtime/serve_script_runs to true in project.godot to drive a -s script on purpose.
The loader's node, /root/GdharnessRuntime, is there in every engine all the same, a script run's
included, so whether it exists says nothing about whether gdharness can reach the game. A test that
needs to know asks get_node("/root/GdharnessRuntime").serving(), which is true only while the
runtime under it is listening.
The loader lives in its own top-level folder, gdharness/, and the export carries it, since the
autoload names it. A check that refuses unexpected top-level folders in an exported pack has to
allow that one.
A scene that is not the game, such as a bench, is handed to the engine the way a screen is, so it
is served like the game unless you list its folder. Set gdharness/runtime/quiet_scenes_under to
the res:// folders of such scenes, for example PackedStringArray("res://tests/"); a run started
on a scene under one of them neither serves nor announces, and nor do the engines it starts. A run
started to be profiled from its start is served all the same, since only the runtime hands its
totals back as it quits.
gdharness runtime on
gdharness runtime offInstalling from the signed archive
For a pinned or offline install, and for anything that verifies its own supply chain. The archive on the release is the same bytes npm serves, so either source verifies against the same attestation.
VERSION=1.3.45
gh release download "v${VERSION}" --repo Aureliolo/gdharness
sha256sum -c "gdharness-${VERSION}.tgz.sha256"
gh attestation verify "gdharness-${VERSION}.tgz" --repo Aureliolo/gdharness \
--bundle "gdharness-${VERSION}.intoto.jsonl" \
--signer-workflow Aureliolo/gdharness/.github/workflows/release-build.yml \
--source-ref "refs/tags/v${VERSION}" \
--deny-self-hosted-runnersWithout gh, with cosign instead:
IDENTITY="https://github.com/Aureliolo/gdharness"
IDENTITY="$IDENTITY/.github/workflows/release-build.yml@refs/tags/v${VERSION}"
cosign verify-blob-attestation "gdharness-${VERSION}.tgz" \
--bundle "gdharness-${VERSION}.intoto.jsonl" \
--new-bundle-format \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity "$IDENTITY"With neither, hash the file and look the digest up in a browser at github.com/Aureliolo/gdharness/attestations. That needs nothing installed, and it trusts GitHub over TLS rather than verifying a signature, which is weaker than either command above.
macOS: shasum -a 256 -c. PowerShell: Get-FileHash gdharness-$VERSION.tgz -Algorithm SHA256.
A failed check means stop. Then unpack and run it from where it landed:
mkdir -p .tools/gdharness
tar -xzf "gdharness-${VERSION}.tgz" -C .tools/gdharness --strip-components=1
node .tools/gdharness/build/cli.js setup /path/to/projectBefore the first change
Nothing. The skill the install writes is what your agent reads, and the rest is refusals: a tool that cannot answer says which state it is in and what changes it, rather than answering emptily.