gda
SkillWeb & browsingDrive the Godot game engine from the command line with `gda`, an agent-first CLI with structured JSON output. Use when building, editing, or inspecting a Godot project — create/edit scenes, nodes, GDScript, resources, shaders, themes; run static analysis; export builds (all headless, no editor) — or to control a running game live (runtime scene tree, input simulation, screenshots, performance, runtime logs/errors) via the gda daemon. Use when the user mentions gda, Godot automation, headless Godot, or asks an agent to make/modify a Godot game. Always pass `--json` and read the single result object; run `gda --help` or `gda schema` to discover the full command surface.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the gda skill
What this skill tells your AI
The instructions your AI receives, as published by aigengame/godot-agent in src/gda/skill/SKILL.md and read by ahel’s review.
gda is an agent-first CLI for the Godot engine: every operation is one command
with structured JSON output, so you build and inspect a game without opening the
editor. Two kinds of operation: headless (a one-shot godot --headless per
call — scenes, scripts, exports) and live (against a running game via the
gda-daemon).
Grammar
gda <group> <command> [options] --json
Exactly one JSON result is printed to stdout; all engine noise (warnings,
progress, the engine banner) goes to stderr. Read stdout, ignore stderr unless
debugging. info / version / help / schema / skill are top-level meta
commands (no group).
Setup
- Engine — set
GDA_GODOTto your Godot binary (or pass--godot PATH). - Project — resolved by
--project DIR→$GDA_PROJECT→ the current directory; the directory must containproject.godot. Resolving a project runs that project's autoloads at engine startup. - Projectless — file-path-only operations run with no project; they resolve
filesystem paths but not
res://. Meta commands never inherit a project ($GDA_PROJECT/cwd is ignored, so an invalid one cannot break them);gda infostill accepts an explicit--project, validated as anywhere else, while the other meta commands take none. - Provenance —
gda --version --jsonreports whichgdais running: its version, the executable and interpreter paths,package_path(the directory the running code was imported from),install_kind(wheel,editable, orunknownwhen the install metadata cannot be read — gda will not guess), and, for an editable install, thesourcecheckout with its Gitrevisionanddirtyflag. No Godot is spawned, so it also works where an engine spawn fails. Run it first in a long session and keep the output: an editable install can change revision under you mid-run, so this is what ties your results to the code that produced them. Baregda --versionstays one human-readable line. - User data (headless runs) — each headless run's engine log goes to a private
temporary file, so a read-only Godot application-data directory is not fatal and
concurrent runs never contend. Run ANY script that writes
user://— a save, a settings file, a test suite's fixtures — under a root of your own:gda --user-data-root DIR script run res://tests/all.gd --json. The option is GLOBAL, so it comes before the group, and$GDA_USER_DATA_ROOTis the same switch; both place the log anduser://underDIR, and a targetgdacannot create is refused asuser_data_unwritablebefore the engine starts. A successfulscript runreports what it actually got:engine_data_path(the directory the engine resolveduser://beneath) always, plususer_data_rootandlog_filewhen you gave a root — the one case in which the log survives the run, so read it there rather than hunting for it. Check those before you read a failed save as a game bug; a FAILURE envelope (--strict, a timeout) does not carry them. Two limits: Godot reads the export templates from that same directory, so arelease/debugexport rununder it reports none installed unless you put templates there — theexport_templates_missingfailure then names both directories and carries them asevidence.templates_root_checked/evidence.templates_root_host, and that second key is how you tell "hidden by the redirect" (drop it, or use--mode pack, which needs no templates) from "not installed anywhere" (install them); and Engine sessions are unaffected either way — the daemon owns their log.
Structured output & errors
Always pass --json. It selects the channel for BOTH outcomes: without it a
failure prints as human-readable lines instead of the envelope. A success is the
operation's result object. A failure is
{"error": {"category": "...", "code": "...", "message": "..."}}
Branch on the stable category/code and the exit code, never on prose:
| Exit | Meaning |
|---|---|
0 | success |
2 | gda could not resolve what you asked for: unknown_command, unknown_option |
127 | environment unusable: binary_not_found, user_data_unwritable, live_unsupported_platform, live_windowed_unavailable, live_windowed_permission_denied, harness_install_permission_denied |
124 | engine timed out: launch_timeout — gda ended a run that had not returned (read it as below) |
3 | engine version too old |
4 | operation-reported failure |
5 | could not parse the engine's output |
6 | live operation failed (e.g. daemon_not_running) |
Reading a 124. The environment category describes how the run ENDED, not the
host. Read the captured partial output in diagnostics for how far the run got, then
raise the ceiling with --timeout where the command exposes one; where it does not the
ceiling is gda's own, so reduce the work or give the machine more headroom. Suspect the
binary or the machine only when the capture shows the engine never started. Any engine
error inside that capture is advisory — the verdict is the timeout.
A command or option gda does not recognize is reported the same way — an
unknown_command / unknown_option envelope at exit 2 — and when gda recognizes
the mistake the envelope carries a hint naming the invocation to run instead
({"error": {"code": "unknown_command", "hint": "gda scene get", …}}). Re-issue the
hint; when there is none, gda schema lists every command and
gda help <command> describes one.
A failure that computed evidence also carries it as DATA, under the envelope's
optional evidence key — omitted, never null, on the failures that computed none.
Read it instead of parsing the message: elapsed_seconds / termination_phase
(launched = the engine wrote nothing at all, so suspect the binary or the host;
output_seen = it was alive and did not finish, so raise the ceiling;
aborted_on_error) on a run gda ended, plus timeout_seconds — the reached
ceiling — on the timeout verdict only (an abort stopped short of its ceiling, so
it omits the field; your own --timeout stays in the message), exit_status
(the CHILD's, not gda's exit code) on script run --strict's script_failed,
and script_errors on the script run failures that parse the run's stderr —
the never-ran verdicts, --strict's script_failed, and both runs gda ended. Under a launch_timeout those errors
stay ADVISORY — the verdict is the timeout, so branch on code and read
evidence for the cause.
Each script_errors entry is a {kind, message, path, line} record — the same
four keys, always present, that a successful script run reports as
diagnostics; path and line are null where the engine named neither, which
is the normal case for a load error. The list itself has three states worth
telling apart: absent means this failure's channel does not parse stderr at all,
so read diagnostics; [] means it parsed and recognized nothing, which is
itself a finding; a non-empty list is what it recognized.
Some commands carry a verdict inside a successful result. For
gda script validate --json, read the result's valid field: it is the AGGREGATE
verdict over every script the call validated, false as soon as one of them fails.
Each script's own verdict is an entry under scripts (path, valid,
error_string, diagnostics). A script that does not compile exits 0 with no
top-level error, so do not treat exit 0 or the absence of an Error envelope as a
pass for this command. Validate the whole set you changed in ONE call —
gda script validate a.gd b.gd c.gd --json uses a single engine launch, and
--all validates every script in the project. Check the result's project_root
before you act on a valid=false: it names the project the scripts were compiled
against, and a verdict full of missing-res:// errors (plus the type errors derived
from them) usually means the wrong project, not a broken script — null means no
project was resolved at all. Pass --project for the project that owns the files
and re-read the verdict. gda refuses up front, with target_outside_project, when
the resolved project plainly does not own a path — outside its tree, or claimed by
a nested project.godot, and with no project resolved too (script run and
resource import refuse the same way). It names the owner it found
(evidence.owning_project) but never switches to it, and the message states the
whole re-issue: --project <owner> AND the target respelled relative to that
owner — a relative path anchors at the project, so your original spelling would
not be found under the new one. --all needs no such check: every project-wide listing
(script list, scene list, script validate --all, project analysis) walks the
res:// tree with the engine's own skip rule, declining the engine cache, a directory
holding a nested project.godot, and one holding a .gdignore (hidden and dot-prefixed
directories gda still enumerates, unlike the engine). So --all reports only
what the resolved project owns; to work on a nested project's files, run with
--project <that project>.
Discovery
gda --help— every group.gda <group> --help— a group's commands.gda help <group> <command>— the same help from the command form; with--jsonit comes back as{command, text}.gda <group> <command> --schema— one command's input/output/error JSON Schema (no Godot spawned), plusargv: how each parameter is written on a command line (kindpositional or option, itspositionor--optionspelling, whether it isrequired, a valuelessflag,multiple— repeat it per value — or ajson_value— one token carrying the value's JSON). Build the command line fromargv;input_propertylinks each binding to the input property it fills.gda schema— the whole surface as one JSON manifest,argvincluded.gda version --json— whichgdais installed and where from;gda info— the engine's version.
--json placement. Every parser takes it: gda --json <group> <command>,
gda <group> --json <command> and gda <group> <command> --json mean the same thing — a --json
written before the command applies to the command it invokes — so any spelling works, as do several
at once. gda schema --json is accepted too, and idempotent: the manifest is already JSON. Two
limits: the --help FLAG always renders TEXT (gda --json --help returns the same help, never
JSON — use gda help <command> --json for a structured payload), and the flag is not a command, so
gda <group> --json and a bare gda --json with no command are both the usage error
Missing command. (exit 2).
Headless commands (Godot 4.4+, all platforms)
| Group | Commands |
|---|---|
scene | create, get, list, get-exports, delete, validate, preflight (.tscn files; validate is the STATIC verdict get does not give — a scene loads fine with its script and texture missing, so check dependencies resolve and attached scripts compile before trusting it; invalid exits 0 with valid: false plus one problem per problem file — COMPOSED over the sub-scenes it references — normally the ones it instances, but a reference is followed when its path ends in .tscn/.scn OR its [ext_resource] line declares type="PackedScene", a union because Godot loads every [ext_resource] whatever it is called while ResourceSaver will put a PackedScene in a plain .res (one saved under a non-scene extension AND declared as something else stays outside) — so a parent whose child is broken is invalid too and every problem carries scene, the file it was found in, in the same canonical spelling as the result's path (read its path/nodes against THAT file); three kinds of edge are reported instead of followed — cyclic_instance for a cycle, unreadable_sub_scene for a scene that loads but carries no [gd_scene] text to walk (a binary .scn, or a PackedScene in a .res), and instance_depth_exceeded for a scene no route reaches within 16 levels of sub-scenes; the last two say that subtree is UNCHECKED, not sound (validate it directly, or re-save it as .tscn, for its own verdict), and the depth bound is on the shortest route so the verdict does not depend on declaration order, and it covers gda's walk only, not the engine's own load of the chain; staged: unresolved dependencies suppress the script compile/binding pass, so repair them and rerun for the rest. preflight is the DYNAMIC one: it boots the scene headless, waits for _ready, and reports status (ready/not_ready/timeout) plus the script errors seen during startup — read started; a timeout verdict also carries elapsed_seconds and timeout_seconds (the --timeout it reached), the same evidence pair the launch_timeout envelope gives elsewhere — it names the consumed ceiling, not the cause: a stuck scene and a healthy one whose --frames window outruns the same ceiling read alike, so pick a larger --timeout or fewer --frames from what the scene should do, and rerun — both keys are on that verdict only and omitted from every other. Passing validate is not "it works": check both) |
node | add, get, list, set, remove, duplicate, move, connect-signal, disconnect-signal (nodes within a scene) |
script | create, get, list, set, delete, attach, validate, run (.gd files; validate takes SEVERAL paths at once — one engine launch for the whole batch, one aggregate valid plus a per-file entry under scripts — or --all for every script in the project; run executes a project script one-shot (address it project-relative or as res:// — the two portable forms, which script validate takes too; run alone refuses absolute paths) and passes its exit_status/stdout/stderr through — stdout above 64 KiB is truncated to its leading bytes with the COMPLETE stream spilled to the file named in stdout_file (stdout_bytes/stdout_truncated disclose it; a spill gda cannot write is the typed stdout_spill_failed, never an unbounded result), and a non-zero quit() is still success, so read exit_status, or pass --strict to get a script_failed failure (exit 4) whose diagnostics carries the script's own stdout and stderr; a script that never ran — missing, or a failed parse/compile — always fails; --timeout <s> sets the ceiling (default 120) and a run that reaches it fails with launch_timeout carrying the captured partial output, the elapsed seconds and a termination phase; add --completion-marker <line> naming a line your script prints when its work is done — a caller-declared liveness contract, not a death detector: gda ends the run once it observes a recognized error attributable to the entry script, no marker line yet, and then silence on both streams — script_aborted (exit 4) with the captured error, in seconds rather than at the ceiling; declaring the marker asserts the script keeps printing until that line, so have it print progress during quiet stretches longer than ~3s, or omit the marker; a script that writes user:// belongs under gda --user-data-root DIR script run … (see Setup) — the result then names user_data_root and log_file beside the always-present engine_data_path) |
project | info, get, set, list, add-autoload, remove-autoload, add-input-action, remove-input-action, find-references, dependencies, find-unused-resources, statistics (a WRITE saves through the engine, which reserializes the whole project.godot: it deletes explicit lines whose value equals the engine default, adds or rewrites application/config/features, and reorders the sections. gda restores the deleted lines and reports added_settings, rewritten_settings, restored_settings and sections_reordered on every write result — read them when the file is tracked, and expect the section order to be the engine's) |
resource | create, get, set, delete, uid, import (.tres files and project assets; import ensures importable assets — PNGs and other files the engine imports — are in the project cache: clean-worktree loading; a script needs no import and reports not_importable) |
export | list, get, run (export a preset by name; --mode release/debug/pack; get also reports templates_root, the export-templates directory it checked, and templates_root_host, set when a --user-data-root redirect hid templates installed on the host) |
shader | create, get, set (.gdshader files) |
theme | create (a loadable .tres Theme) |
Every headless reply carries its floats at full binary64 precision, so a value read back through node get, scene get-exports, project get/project list, resource get, or the echo of a set is the exact number the project holds — 1e-300 reads back as 1e-300, not 0.0; the one residual belongs to the ENGINE's writer — a negative zero reads back as 0.0 (#771). The --value string you send IN is coerced by the engine's own parser, and gda refuses what that parser would destroy: a literal it reads as 0.0 when you did not write zero, or as NaN at all, fails with uncoercible_value (exit 4, target untouched) instead of writing a number you never sent — 2.2250738585072014e-308 and 5e-324, and also 0.000000000000000001, whose 18 leading zeros fill the parser's whole mantissa window (1e-18 is exact). The rule follows the LITERAL, not the property type, so a number inside a Dictionary or Array --value is refused the same way and names the offending literal: --value '{"a": 1e-320}' fails, --value '{"a": 1e-18}' stores exactly. Only real JSON numbers are read — a numeric-looking STRING value ({"a": "1e-320"}) and a numeric-looking KEY ({"1e-320": 1.0}) are stored unchanged (#805). So spell a small or many-digit value in SCIENTIFIC notation carrying only the digits it needs: that also avoids the low-digit loss the parser inflicts on a full-precision literal between 1e-4 and 1e-2, which is disclosed rather than refused (#772). Read the set echo when the exact bits matter.
Live operations (via the daemon; Godot 4.6+, macOS/Linux)
Prerequisites: the project defines application/run/main_scene, or you pass --scene.
For settings it can determine from the project files, daemon start refuses an empty main
scene with live_main_scene_undefined, or a uid:// main scene without its UID cache with
live_main_scene_unresolved — run gda resource import <any existing res:// asset> once,
or open the project in the editor. Main-scene feature overrides, default/custom settings
overlays (override.cfg, or a nonempty application/config/project_settings_override path,
including feature overrides of that path), and escaped application keys defer to the engine; feature overrides
or unrecognized boolean values for application/config/use_hidden_project_data_directory defer only
the UID-cache check. Deferred cases can still show a native alert on macOS even headless
until the readiness deadline tears down the session. An explicit valid --scene res://<scene>.tscn
avoids main-scene resolution. Run gda daemon start first (optionally --scene <res://...> to boot a
specific scene instead of the project's main scene); the engine session launches lazily on
the first operation that requires one. To establish the session deterministically, run
gda daemon wait-ready (--timeout budgets daemon waits and new-work decisions;
a synchronous launch call can delay expiry observation; idempotent while the session is
alive) — success means live reads serve. This matters for the read-only diagnostics: diag errors /
logger tail never launch a session themselves, so right after daemon start they report
engine_session_not_running by design — expected, not a defect; run wait-ready first.
A live_timeout discards the session (its late reply can no longer be attributed), so the
next operation starts a fresh game and the runtime state you had set is gone. Most often
it means the game stopped returning to its main loop — look for a blocking loop or wait in
game code. But the 30s bound is a wall clock while a multi-frame window (--frames,
--await-frames) waits that many ENGINE frames, so on a slow-ticking game a window op
outruns it with the loop running normally: ask for fewer frames when gda logger tail
shows the log kept advancing. A paused SceneTree is NOT a cause; see "paused vs
suspended" below.
screen capture needs a windowed session
(gda daemon start --windowed).
A windowed session needs the host's real desktop session — an on-console GUI login on
macOS, $DISPLAY / $WAYLAND_DISPLAY on Linux. Over SSH, on a headless CI box, or from
a sandbox that blocks the window server, daemon start --windowed refuses before
spawning Godot. Branch on the code, not the sentence:
live_windowed_unavailable— nothing refused the probe and no session is reachable, so this host cannot show a window. Skip the rendered check; headless live ops (game,perf,input,diag,logger) still work.live_windowed_permission_denied— this process is not allowed to even look up the window server (e.g. a sandbox). It does NOT mean the host has one: macOS refuses the lookup before resolving it, so a broadly-confined process is refused either way. Re-run outside the restriction to find out; do not record the machine as display-less on this code alone.
A refusal from gda daemon start --windowed carries error.probe {name, platform}
naming the OS call that decided — including when the refusal is relayed from an
already-running daemon's lazy Engine-session launch; only the outer
{stdout, stderr, exit_code} transport shape is probe-less.
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 39
- Forks
- 6
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
gda- Source
- github.com/aigengame/godot-agent