An agent (Codex, Claude Code, a CI job) can run a shell command, but it cannot click inside the Godot editor. So when the task is "put the game into this state" — progress/stage = 7, tutorial_done = true, audio/master = 0.42 — the agent ends up editing the save file directly. The editor itself can do the same thing through a dock; a terminal cannot.
The naive way is json.load → change → json.dump. On a Godot project that quietly changes data you did not ask to change. Below is what actually moved in the files I measured, and what an edit has to preserve to be safe.
1. Numbers in a Godot save are not "numbers"
JSON.parse_string() returns every JSON number as a float. A test file:
{"level":3,"big_int":9007199254740993,"float_one":1.0,"exp":1e10,"neg_zero":-0.0,"imprecise":0.30000000000000004,"tiny":1.0000000000000002e-300,"long_i64":9223372036854775807}
Any round trip through a float parser turns 1.0 into 1, 1e10 into 10000000000, -0.0 into 0, and 9007199254740993 (2^53+1) into a neighbour. 9223372036854775807 does not survive at all. A game that compares -0.0, prints a value back, or hashes its own save will notice the difference.
The safe rule: read numbers as source literals (text), not as floats, and write back the literals you did not touch.
2. ConfigFile is not INI
ConfigFile.save() writes sections and ; comments, but if the file is project.godot with an input map it also contains whole Object(InputEventKey,...) values. Measured: a 1040-byte project.godot with two input events. An INI library re-serialises the file without those Object(...) lines, and the project no longer opens in the editor.
3. BOM, CRLF, and the bytes nobody looks at
A 73-byte ConfigFile with a UTF-8 BOM, CRLF line endings and a ; comment: od -c starts with 357 273 277 and every line ends \r\n. Parsers that normalise newlines or strips the BOM change lines you never selected. Keep them or the diff is a lie.
4. Binary saves are out of scope
var_to_bytes() files and .res are not text. An agent should refuse them, not "repair" them.
The approach that held up: move the byte span, don't re-serialise
The tool I built opens the file with the engine's own parser to get labels and types, records the byte span of the literal being changed, and replaces exactly that span. Everything else is copied through untouched. Measured on the packaged build:
- ConfigFile, two values changed: 78 → 78 bytes, only the two spans replaced, read-back SHA-256 matched the reported one.
- JSON with an i64 literal and Japanese text:
9007199254740993→1,"Player One"→"ニュー 名前", rest of the file byte-identical. -
project.godotwith an input map: 1040 → 1038 bytes, bothObject(InputEventKey,...)lines byte-identical afterwards, editor opened the project with no error. - Same values,
--restore --write: original SHA-256 back, exactly.
What an agent needs in order to trust the result
Exit codes that separate the failure modes, so the agent can branch instead of guessing from prose:
-
0wrote (or dry run OK),1usage / relative path,2missing or unreadable (binary) file,3label missing,nullleaf, or a type change,4file is read-only. - Dry run by default. It prints the SHA-256 of the bytes it would write;
--writethen produces exactly those bytes. I checked that equality on Godot 4.4.1, 4.5.2, 4.6.3 and 4.7.2 with files that each engine wrote itself. - A backup (
.sve.bak) only on a real write, and no second backup when re-running the same edit (no_change). -
--jsonoutput. Note: the engine banner is the first line of stdout, so read the last line, not the whole output.--quietis not a workaround because it takes the JSON with it.
godot --headless --path <project> --script res://addons/save_value_editor/edit.gd -- \
--file=user://settings.cfg --set=audio/master=0.42 --set=progress/stage=7 --write
Limits I would hold against it
- Windows only, standard (non-.NET) builds only. macOS, Linux and .NET were not run.
- 8 MiB file / 20,000 entry limits in the document layer; not re-derived in the agent path.
- A concurrent writer is refused by comparing the on-disk bytes with the opened snapshot, but two writers in the same instant are still unsafe.
-
--setwrites the literal as given, so a string needs its own quotes. That is deliberate (no number reformatting) and it is the one place where the agent has to know the format.
If you would rather click
The same document code runs as a dock inside the editor, Godot 4.4+ — read a value list, filter it, change a value, see the diff before saving: Save Value Editor for Godot. The command/exit-code contract and its limits are public here: onpont/godot-save-value-cli.
Top comments (0)