Repository navigation
Releases: unabated-games/godot-cli
Release list
v0.26.0
Added
- The installer works on Windows.
install.shrefused on anything but macOS and Linux, so a Windows user unpacked the archive and wired up the env file, the skill and the docs by hand. It now detects Git Bash, MSYS2 and Cygwin, fetches the.ziprather than the tarball, unpacks it with whichever ofunzip,taror PowerShell is present, and installsgodot-cli.exe. Verified end to end by faking the platform on macOS — real Windows archive, real checksum, real unpack — and thegodot-cli.exeit installed was then run under wine. install.ps1, for Windows without a POSIX shell:irm https://raw.githubusercontent.com/unabated-games/godot-cli/main/install.ps1 | iex, with-InstallSkilland-AddToPath. Same checksum verification, same layout. It has not been run on Windows — there is no PowerShell on any machine its author can reach — so the Git Bash path is the recommended one and the docs say so. Its assumptions about the archive were checked against the real artifact, which caught two mistakes before it shipped.project resave <file>: Godot's own save of a scene or resource, as the reference forscene compare-godot. It imports first, unless--no-import, then has Godot headless load the file and save a copy. The copy goes to.godot/godot-cli/resave/<its path>by default, never over the file itself, which it refuses asresave_over_source. Trial 35 wrote that GDScript by hand and ran Godot from the shell to get the same thing. A copy saved to a new path has its ext_resource ids renumbered and its uids left out. That is how Godot writes a save to a new path, andcompare-godotallows for both.camera_3dandplace_3drecipes place and aim a 3D node.camera_3dadds aCamera3Datpositionfacinglook_at, with optionalcurrentandfov.place_3dsets an existing 3D node'stransformthe same way. Each point is[x, y, z]or"Vector3(x, y, z)". The basis is Godot'slooking_at(-Z toward the point, +Y up), written row by row. Trial 35's camera, worked out by hand, comes out ofcamera_3ddigit for digit, and a Godot re-save of the recipe's output matches it.uid read <file>: the UID a file records, read from the file itself, so it needs no uid cache and works on a fresh clone. It reads a scene or resource header, binary (.res,.scn) or text, a script's.uidsidecar, or an imported asset's.importfile, andsourcesays which. For a binary resource it also reports the class, whether the file is compressed, and the Godot version that saved it. A file that records no UID fails withno_uid_recordedand says why. Until now the only route to a binary resource's UID wasuid cache lookup, which needs the project opened in Godot since the file last changed. Asked for by a user who needed a UID out of a binary resource and had no way to get one.project runsays when it ran a 3D scene with no camera:no_camera_3d: true, and a message saying that unless a script adds aCamera3D, the frame shows only the clear colour, so the run proves the scene loads and not how it looks. It checks the scene and every scene it instances, and says nothing for a headless run or a scene it cannot read. Trials 31 to 34 each had to work out why their frame was grey. Adding a camera to trial 34's level made the note disappear and the meshes appear.scene compare-godotcompares UIDs, and says what differs. A scene whose mesh uid had been swapped still reportedmatches_godot_save: true. The scene's uid and each ext_resource's uid are now compared wherever both files carry one. A reference saved from a script leaves them out, so a missing uid is not a difference. On a mismatch,differencenames the first one. The descriptions now say what is compared and what is not (ext_resource ids,load_steps, sub_resources), and that the second positional and--referenceare the same thing. A node'sinstanceis now compared by the scene it names, not by its ext_resource id. Compared literally, every instanced node was a mismatch against a save to a new path, which renumbers those ids;project resave's own test caught it.scene normalizereportschanged: whether the save changes the file. So--dry-runanswers "is this already written the way a save writes it?". A dry run also returnspreview_sections, and says when a save would change only blank lines or section order.scene apply --dry-runnow also runs the uid refresh a write does, so both previews show a write's exact text.scene validatewarns about a 3D node placed withposition,rotation,scale,quaternion,basisor aglobal_*property (property_not_stored). Godot loads such a line but never writes one, because a 3D node stores its placement astransform(node_3d.cppgives those properties editor-only or no storage). So the editor's next save turns the line intotransform = Transform3D(...), a diff nobody asked for. The warning gives thetransformform to use. Before shipping it was swept against Godot 4.8's own output: every instantiableNode3D,Node2DandControlclass (218 of them), moved, rotated and scaled, then saved. Godot wrotetransformfor all 111 3D nodes andpositionfor the 2D ones, and the check flagged nothing. NoNode3Dsubclass in the class table declares its own property by any of those names.scene diffreports resources: ext_resources added, removed, or changed in type or uid, keyed by path because their local ids are renumbered freely; and sub_resources, keyed by id. With--properties, a sub_resource's changed properties are listed too, addressed asSubResource("id"). Added and removed nodes carry theirunique_id, and a node whoseunique_idchanged at the same path is reported asunique_id_changed. Trial 32 had to find an added ext_resource throughscene describe.- Every scene and resource write takes
--snapshot <path>: a copy of the file as it was before the write, forscene difforscene restoreafterwards. It is taken once per command, before anything is written, whether the command comes from the shell, abatchstep, or MCP. A dry run takes none, and onscene restore, where--snapshotnames the file to restore from, nothing is copied over it.scene apply --auto-snapshotnow defaults to.godot/godot-cli/snapshots/<the scene's path>under the project root, where Godot imports nothing, instead of beside the scene. Trials 31 to 33 each kept a "before" copy by hand, and trial 32 left MCP to do it. scene apply --dry-runreturnspreview_sections: the exact text of every section a write would add or change, after the same uid refresh and save preparation a write does. Trial 33 needed a patch's text before writing it, and onlyscene node addcould show one. On a patch that set a property and added a node, both previewed sections appear verbatim in the file the real write produced.scene node add --dry-runandscene instance add --dry-runreturnsection_text: the section a write would put in the file, after the same preparation a write does,unique_idincluded. Trial 32 dry-ran an add to see how a property would be written and got no text back. The descriptions say the preview'sunique_idis the one a write assigns, being seeded from the scene's path. Trial 33 could not tell that, and worked around it with a patch op that pins the id.
Fixed
- Saving a scene rewrote correct UIDs on its references to binary resources (
.res,.scn) whenever the project had no.godot/uid_cache.bin, which is every fresh clone, since.godot/is gitignored. Any write with--project-rootdid it. In the reproduction, aset-propertyon an unrelated node replaceduid://bc628hhe4x5ypwithuid://byggqned6p7ihand exited 0. Godot then warnedinvalid UID … using text path insteadon every load, and the reference no longer survived the file being moved, which is what the UID is for. A binary resource keeps its UID inside its own header, and godot-cli read headers only from.tscnand.tresfiles. For anything else it computed a UID from the file's bytes, and those bytes contain the UID, so the hash can never equal it.scene ext addof a.reswith no cache wrote a wrong UID the same way. godot-cli reads the binary header now, including the compressed form the editor saves by default, and a save repairs references the old behaviour damaged. Found while looking into the request above. - Rewriting a scene put a blank line between consecutive
[ext_resource]lines, so even a no-opscene normalizeof a file Godot saved was not byte-identical. Godot writes that block as adjacent lines and puts one blank line after it (ResourceFormatSaverTextInstance::save), so every scene with two or more ext_resources got a diff on lines nobody had touched, the first time godot-cli wrote it. No Godot-saved fixture had more than one ext_resource, which is how this went unnoticed. A new one,multi_ext_godot_saved.tscn, saved by Godot 4.8, has three. Files godot-cli wrote before lose those blank lines on their next edit, as they would on Godot's next save. - An added reference put
uidafterid, and an instanced node given aunique_idput it afterinstance=. Godot writestype, uid, path, idon an ext_resource and ends a node line withinstance. Both now land where the editor writes them. scene diff --propertiessaid nothing about an added node's properties, and gave an instanced node an empty type. Trial 31 added a mesh with atransformand amesh, gotproperty_diff_count: 0, and fell back todiff -ufor its answer. An added node now lists the properties it arrived with, and a removed node the properties it took with it. An instanced node reports its scene's root class when there is a project root, asscene describedoes, andPackedScenewhen there isn't;instance_path_aorinstance_path_bnames the scene. `scene...
v0.25.1
Fixed
scene validate --project-rootsegfaulted on Windows whenever the project had a.godot/uid_cache.bin. Not scene-specific: every scene in every such project. The validation context held a pointer to the loaded uid cache, and that pointer was taken inside a function that then returned the struct by value — so the caller's copy kept the address of a stack frame that had been left. macOS and Linux survive it because the return slot happens to land where the local was; Windows places the copy elsewhere, and the dereference is a crash with no message. The context is filled in the caller's own storage now, so the pointer is inside the struct it belongs to by construction. Reported by a Windows user who bisected it to the uid cache themselves.
Changed
- CI runs the test suite on Windows. It was built for and never run on, which is how a pointer bug that only manifests there reached a user. Reported rather than blocking for now, until the shell-driven smoke tests are known to behave on that runner.
- Release binaries are built
ReleaseSaferather thanReleaseFast. Undefined behaviour now stops with a panic naming a line instead of a segfault naming nothing — the bug above cost a session of bisection to place, and a message would have placed it in one run. Measured on this workload: 3.0 ms perscene validateeither way, with the binary 1% larger.
Install
curl -fsSL https://raw.githubusercontent.com/unabated-games/godot-cli/main/install.sh | bashInstalls to ~/.godot-cli (binary, scene templates, agent docs, examples,
shell completions, man page). Then:
source "$HOME/.godot-cli/env.sh"
godot-cli --versionOr download an archive below and run bin/godot-cli from it.
Verify
shasum -a 256 -c SHA256SUMS --ignore-missing
gh attestation verify godot-cli-0.25.1-<target>.tar.gz --repo unabated-games/godot-cliv0.25.0
Fixed
scene validatecalled everyPacked*Arrayproperty a type mismatch, including on files the editor wrote.Line2D.points,Polygon2D.polygon,Gradient.offsetsandcolors— anything typedPackedVector2Array,PackedFloat32Arrayand the rest. Every one of those is documented as a class, so the generated table gave them the kinds of a resource reference and validate demanded anExtResource(...)where Godot writesPackedVector2Array(0, 0, 10, 10). A repo fixture saved by Godot itself had been failing validation this whole time. Found by sweeping 98 scenes before shipping the table below, which is the whole reason for sweeping.- The class table was missing 195 classes — every class that lives in an engine module or platform rather than
doc/classes: the tilemap family,GridMap, the CSG nodes, the audio streams. The generator only ever scanneddoc/classes, soscene validateskipped all of them in silence. It scansmodules/*/doc_classesandplatform/*/doc_classesnow. Godot moved the tilemap classes into a module for 4.8, so regenerating without this would have droppedTileMap,TileMapLayer,TileSetandTileSetAtlasSourcefrom the table.
Changed
- The class table is regenerated against Godot 4.8: 520 classes to 715. Strictly additive — no class removed, no property rule made stricter, so nothing that passed before can fail now except the two fixture files that are meant to.
Added
scene validatereports how wide its result is:classes_checked, andunknown_classesfor every type the class table does not carry, with a message naming them. A class the table lacks is skipped rather than misjudged, so a clean result read exactly like approval of it — another session had to enumerate its node types by hand to find out whether "all scenes validate" meant anything for the scenes it had. A script'sclass_nameor an addon class is expected in that list; a core class means the table is behind the engine.- CI reports drift between the committed class table and the engine it tests against, from
tools/check_class_table.sh.src/godot/class_table.zigcarries agodot_versionand nothing read it, so the table backing everyscene validateproperty and signal check could fall behind the engine in silence. It has: against Godot 4.8-dev4 the engine's own class reference carries classes the table does not. Reported, never blocking — regenerating changes whatscene validateenforces, so it stays a deliberate change. Prompted by another session finding the same shape in their own repo: a content-hash pin nothing verified.
Fixed
- An intermittent test-suite failure, roughly one run in twenty, that had gone unexplained for two days. One CLI smoke test took its temp path from
mktemp /tmp/godot_cli_scene_XXXXXX.tscn. BSDmktempdoes not substitute when a suffix follows theXs: it returns that literal path, and a second concurrent call fails withFile exists. The two test binaries run in parallel, so the step raced with itself and passed on every retry. It uses a temp directory now.
Install
curl -fsSL https://raw.githubusercontent.com/unabated-games/godot-cli/main/install.sh | bashInstalls to ~/.godot-cli (binary, scene templates, agent docs, examples,
shell completions, man page). Then:
source "$HOME/.godot-cli/env.sh"
godot-cli --versionOr download an archive below and run bin/godot-cli from it.
Verify
shasum -a 256 -c SHA256SUMS --ignore-missing
gh attestation verify godot-cli-0.25.0-<target>.tar.gz --repo unabated-games/godot-cliv0.24.0
Fixed
- A scene transition inside
project runleft the old screen on top of the new one.SceneTree.change_scene_to_fileremovescurrent_sceneand nothing else, and the generated driver added the scene withroot.add_childwithout ever settingcurrent_scene— so the old screen stayed in the tree and both drew at once, for the whole rest of the run. Every--click- or--type-driven transition looked broken. Found while checking a trial's claim that the overlap was a one-frame capture artifact; it was still there twenty frames later.
Added
project run --type '<node-path>@<frame>=<text>'fills a form, so the screen behind a sign-in can be watched rather than code-reviewed.--presssends input actions and--clickclicks a node; neither could type, which left every form-gated transition undrivable. The field is focused and emptied, then the text goes in as real key events, sotext_changedfires and a validating form runs the way it does for a person — assigningLineEdit.textemits nothing, which is why this does not do that. Works headless. Requested by another agent's session with a sign-in, a create-account and three password-reset screens it could reach and screenshot but not drive.project run --focus '<node-path>@<frame>'moves keyboard focus without typing, for a frame that shows a focus ring or proves a tab order.- The frame number is read from between the last
@of the node path and the first=of the text, sosomeone@example.comand a password containing=both survive. An empty value clears the field. Typing into anything but aLineEditorTextEditfails the run naming the class it found, and a field that will not take focus fails rather than sending the keys to whatever was focused before.
Install
curl -fsSL https://raw.githubusercontent.com/unabated-games/godot-cli/main/install.sh | bashInstalls to ~/.godot-cli (binary, scene templates, agent docs, examples,
shell completions, man page). Then:
source "$HOME/.godot-cli/env.sh"
godot-cli --versionOr download an archive below and run bin/godot-cli from it.
Verify
shasum -a 256 -c SHA256SUMS --ignore-missing
gh attestation verify godot-cli-0.24.0-<target>.tar.gz --repo unabated-games/godot-cliv0.23.3
Fixed
uid cache listanduid cache lookupanswered a damaged cache with the bare wordCorrupt. 0.23.2 stoppedscene validatefailing on it, but left the two commands someone reaches for to diagnose that saying nothing: no path, no cause, no remedy. They now fail withuid_cache_unreadable, naming.godot/uid_cache.binand how to rebuild it. Failing is still right there — the cache is what those commands are about — but the answer has to say which file. Prompted by the reporting session's observation that the original failure named the file you passed, "which is exactly where the problem is not".
Install
curl -fsSL https://raw.githubusercontent.com/unabated-games/godot-cli/main/install.sh | bashInstalls to ~/.godot-cli (binary, scene templates, agent docs, examples,
shell completions, man page). Then:
source "$HOME/.godot-cli/env.sh"
godot-cli --versionOr download an archive below and run bin/godot-cli from it.
Verify
shasum -a 256 -c SHA256SUMS --ignore-missing
gh attestation verify godot-cli-0.23.3-<target>.tar.gz --repo unabated-games/godot-cliv0.23.2
Fixed
- A gap in the ext_resource numbering closed a scene to further additions. The generated id took the count of ext_resources plus one, which is only free while the numbering is dense. Remove
2_abc12from a file holding1..4and every laterscene ext add,scene instance add,assign_extandinstance_addproposed4_abc12— already taken — and failed withDuplicateResourceId. Neither--no-id-sessionnorscene normalizeoffered a way back. The id is now allocated past the highest index present and stepped forward until free, in one function both the command and the patch paths call. Reported by another agent's session, which had to abandon a scene node and instantiate from code instead — the exact shape this tool exists to avoid. scene validatereportedCorrupton scenes that were not corrupt. The word came from the project's.godot/uid_cache.bin, not the file being validated: an unreadable cache failed the whole command with{"kind": "command_failed", "message": "Corrupt"}on ascene validate <file>call, whilescene describeand Godot itself read the same scene without complaint. The cache belongs to the project and feeds one check, so an unreadable one now costsstale_uid_for_pathand says so inmessages, naming the cache path and how to rebuild it.catalog validatealready tolerated this; the two paths disagreed.- An unknown option on a command that has subcommands and no options of its own answered
this command takesand stopped. It names the subcommands now. Introduced in 0.22.0 by the message that fixed the previous version of this problem.
Notes
scene normalizestill does not renumber ids to close a gap, and should not. Godot renumbers only as a side effect of rebuilding a file from a live scene tree, where it also discards every resource nothing references — checked against 4.8-dev4, which dropped two unreferencedext_resourcelines and renumbered the third. Reproducing half of that would write a file Godot would not have written; reproducing all of it would silently delete resources. Allocating past the highest index removes the need.
Install
curl -fsSL https://raw.githubusercontent.com/unabated-games/godot-cli/main/install.sh | bashInstalls to ~/.godot-cli (binary, scene templates, agent docs, examples,
shell completions, man page). Then:
source "$HOME/.godot-cli/env.sh"
godot-cli --versionOr download an archive below and run bin/godot-cli from it.
Verify
shasum -a 256 -c SHA256SUMS --ignore-missing
gh attestation verify godot-cli-0.23.2-<target>.tar.gz --repo unabated-games/godot-cliv0.23.1
Fixed
- A component with no script read as a component whose script could not be parsed.
catalog showreportedexports_source: "gdscript_heuristic"andscript_parse_complete: falsefor a scene carrying no script at all — the same answer a script the parser choked on would give — so a correct empty export list was indistinguishable from a failure to read one. There are three states now:gdscript_heuristic/true(a script was read),none/true(there is no script, so nothing to export), andnone/false(a script is named and could not be read, which is the only one worth acting on). Found by another agent's session reasoning around it correctly on a bareLinkButtonentry. script_parse_completemeant "a script interface was obtained", not what its name and the documentation said. The heuristic parser's ownparse_completewas hardcoded true and never read, so no value of the field ever indicated a partial parse; the dead flag is gone and the documented meaning now matches the behaviour.
Install
curl -fsSL https://raw.githubusercontent.com/unabated-games/godot-cli/main/install.sh | bashInstalls to ~/.godot-cli (binary, scene templates, agent docs, examples,
shell completions, man page). Then:
source "$HOME/.godot-cli/env.sh"
godot-cli --versionOr download an archive below and run bin/godot-cli from it.
Verify
shasum -a 256 -c SHA256SUMS --ignore-missing
gh attestation verify godot-cli-0.23.1-<target>.tar.gz --repo unabated-games/godot-cliv0.23.0
Added
- Every manifest field the catalog reads can be written by
catalog add.export_root_script,function_docsandprefer_over_idswere read by the schema, surfaced bycatalog validate, and settable only by hand-editing the manifest — in a tool whose first rule is not to hand-edit. New flags:--export-doc <property>=<meaning>,--function-doc <function>=<meaning>,--prefer-over-ids,--export-root-script. Reported by another agent's session with 12 catalogued components, six carrying root scripts with 21 exports between them. @exportdocumentation, scaffolded the way signals already were.catalog addleaves a row per export the root script declares,--export-docfills it in, andcatalog showreturns each export'sdocalongside its name, type and default, withdoc_sourcesaying whether a person wrote it or it is just what the parser found. The script parse gives a caller the names of what it can set; only a person can say what setting one does. The result also reportsexports_scaffoldedand names the rows still blank, since an undocumented export reads to the next caller exactly like one nobody needed to explain.unresolved_catalog_reference: arelated_idsorprefer_over_idsentry naming no component in the project and no builtin is a warning fromcatalog validaterather than a pointer that silently goes nowhere. Listed in the catalog design doc since the beginning and never implemented.
Changed
- The validation guide and the skill's reference say plainly what a clean
scene validatedoes not prove.unknown_propertyskips more than it checks — any node with a script, any instanced node, and every namespaced name — and a partial check that says nothing reads exactly like a complete one that found nothing. Raised by the session the check came from. - The components guide and the skill cover documenting exports, the four new flags, and what
doc_sourcemeans.export_root_scripthad never been documented anywhere a user would look: it is the script to read exports and signals from when they are not on the root node's own.
Install
curl -fsSL https://raw.githubusercontent.com/unabated-games/godot-cli/main/install.sh | bashInstalls to ~/.godot-cli (binary, scene templates, agent docs, examples,
shell completions, man page). Then:
source "$HOME/.godot-cli/env.sh"
godot-cli --versionOr download an archive below and run bin/godot-cli from it.
Verify
shasum -a 256 -c SHA256SUMS --ignore-missing
gh attestation verify godot-cli-0.23.0-<target>.tar.gz --repo unabated-games/godot-cliv0.22.0
Fixed
unknown_optionnamed neither the option nor the command.{"kind": "unknown_option", "message": "unknown option", "details": null}with an emptycommandwas the whole answer, which is close to invisible in a piped--jsonworkflow. It now names the flag, the command it was given to, every option that command accepts, and the nearest accepted spelling when there is one — and the envelope'scommandis filled in, which it could not be before because the failure happened before the invocation existed. The same details reach MCP clients, where the envelope is all a client gets. Reported by another agent's session, which lost a scene: a rejectedscene node reparent --towent unnoticed, and ascene node remove --recursiveran next on the subtree the reparent had not moved.- A missing required option answered with the bare word
Usage. No usage text, no name of the option. Required options are enforced in the parser now rather than in each handler, so every command answers with the missing option, the synopsis--helpwould print, and the full set the command requires.
Added
scene validatereports a property the class does not have —unknown_property, the thing Godot keeps in the file and silently ignores, so the setting does nothing and nothing says so. A warning rather than an error, because the class table cannot see everything a property may come from: a node with a script attached or an instanced node is skipped entirely, along with namespaced names (theme_override_constants/…,metadata/…) and the properties Godot registers as internal.layout_modeandanchors_presetare in that last group, are written into every scene the editor touches, and are absent from the class reference — an earlier draft of this check called every editor-saved scene broken, which is why the exclusions are there and tested.
Install
curl -fsSL https://raw.githubusercontent.com/unabated-games/godot-cli/main/install.sh | bashInstalls to ~/.godot-cli (binary, scene templates, agent docs, examples,
shell completions, man page). Then:
source "$HOME/.godot-cli/env.sh"
godot-cli --versionOr download an archive below and run bin/godot-cli from it.
Verify
shasum -a 256 -c SHA256SUMS --ignore-missing
gh attestation verify godot-cli-0.22.0-<target>.tar.gz --repo unabated-games/godot-cliv0.21.0
Fixed
--frames Ndid not mean N frames, so input scheduled late in a run never happened. Godot paces physics off the wall clock while--quit-aftercounts main-loop iterations, and only--write-movieforces the two together — which a windowed run gets and a headless one did not. A 40-frame headless run reached physics frame 18 on the machine this was found on, and fewer on a busier one, so--click …@20was silently dropped:ok: true,errors: 0,clicks: 1, and a handler that never ran.--press move@10..30was held for 9 frames on one run and 10 on the next instead of 21. The run now passes--fixed-fpsset to the project'sphysics/common/physics_ticks_per_second, so one iteration is one physics step and one frame means the same thing in both modes, on any machine. Reported from another agent's session against 0.20.1.--clickworks under--headlessnow, rather than being documented as not working. The headless display server reports no window size, which leaves the root viewport at 64×64 with everyControllaid out in that corner — so a click computed from a real layout landed outside it. The run puts the project's viewport size back before the first click, and layout and input picking then match a windowed run: aButtoncentred at (960, 540) in a 1920×1080 project is pressed at (960, 540) and itspressedsignal fires. This makes a button's wiring checkable on a machine with no display. What headless still cannot give you is the frame, which is what the run now says instead.- The claim that clicks need a window is gone from the
--headlessand--clickhelp, the verification guide, the skill's troubleshooting table and the MCP cheat sheet. It was mine, and it was wrong twice over: the 64×64 viewport is fixable, and the guard that was supposed to catch a click landing outside it could not fire, because the frame it was scheduled on was never reached.
Install
curl -fsSL https://raw.githubusercontent.com/unabated-games/godot-cli/main/install.sh | bashInstalls to ~/.godot-cli (binary, scene templates, agent docs, examples,
shell completions, man page). Then:
source "$HOME/.godot-cli/env.sh"
godot-cli --versionOr download an archive below and run bin/godot-cli from it.
Verify
shasum -a 256 -c SHA256SUMS --ignore-missing
gh attestation verify godot-cli-0.21.0-<target>.tar.gz --repo unabated-games/godot-cli