godot-cli-control
WebSocket bridge for headless / scripted control of Godot 4 scenes — Python client + CLI.
Install
pipx install godot-cli-control
# or, for unreleased main:
pipx install "git+https://github.com/ClaymanTwinkle/godot-cli-control.git"
The wheel ships the Godot plugin source so the init command can drop it into your project.
Requires Python ≥ 3.10.
One-shot setup of a Godot project
cd path/to/your_godot_project
godot-cli-control init # copies plugin, patches project.godot, detects Godot binary
godot-cli-control daemon start
godot-cli-control tree 3
godot-cli-control daemon stop
Re-running init refreshes both addons/godot_cli_control/ and the skill files to match the installed CLI version (the plugin directory is wiped and re-copied; project.godot patching stays idempotent). Pass --keep-addon to keep an existing addons/godot_cli_control/ untouched.
init also writes .claude/skills/godot-cli-control/ and .codex/skills/godot-cli-control/ (a lean SKILL.md core + references/*.md detail files) so AI agents working in your Godot project can pick up this CLI surface automatically without flooding their context. Use --no-skills to skip, or --skills-only to refresh just those files after a CLI upgrade. See the top-level README for details.
Async API
import asyncio
from godot_cli_control import GameClient
async def main():
# Omitting port lets GameClient auto-discover from .cli_control/port (written by daemon start)
async with GameClient() as client:
tree = await client.get_scene_tree(depth=3)
await client.click("/root/MyScene/Button")
await client.action_press("jump")
await client.wait_game_time(0.5)
await client.action_release("jump")
png_bytes = await client.screenshot()
open("frame.png", "wb").write(png_bytes)
asyncio.run(main())
Sync API (for scripts and tests)
# script.py
def run(bridge):
bridge.click("/root/MyScene/StartButton")
bridge.wait(2)
bridge.tap("attack")
godot-cli-control run script.py --headless # auto-starts and stops the daemon
pytest fixtures
pip install "godot-cli-control[pytest]"
The package ships a pytest plugin (auto-loaded via pytest11 entry-point):
# tests/test_jump.py — no fixture boilerplate needed
def test_jump(godot_daemon, bridge):
bridge.click("/root/Game/Start")
bridge.tap("jump")
assert bridge.get_property("/root/Player", "on_floor") is False
-
godot_daemon(session-scoped) starts headless Godot once and stops it after all tests; if a daemon is already running it's reused (and not stopped at teardown — keeps your IDE workflow alive). -
bridge(function-scoped) gives a freshGameBridge; on teardown it best-effort restores global engine state —unpause(),time_scaleback to its setup-time snapshot,release_all()— so ahold/pause/speed-up left behind by one case can't bleed into the next, then closes the connection. -
fresh_scene(function-scoped, opt-in) callsscene_reload()at setup so the case starts on a pristine scene. Node references cached before the reload are invalid afterwards. -
no_push_errors(function-scoped, opt-in) records anerrorsmarker at setup and fails the case if any newpush_errorwas emitted during it (warnings don't fail). Requires Godot 4.5+ (Logger API) — older engines raiseRpcError1012 at setup, loudly. -
godot_instances(scope configurable, default function) is a multi-instance factory for multiplayer e2e — start a named server and clients inside one test and get connectedGameBridgeobjects back; teardown stops everything the fixture started (and only that):def test_join(godot_instances): server = godot_instances.start("server") client = godot_instances.start("client1")
start(name)is idempotent get-or-start (headless/time_scalefollow the global options, overridable per call;portalways defaults to 0 = OS-assigned);stop(name)stops one instance mid-test (restartable);daemon(name)exposes the underlyingDaemon.--godot-cli-instances-scope sessionshares one set of instances across the whole suite (faster, no state isolation between tests). -
On a test failure (non-headless daemon only) a best-effort screenshot is saved to
.cli_control/failures/<nodeid>.pngand the path is attached to the pytest report.
CLI options:
--godot-cli-port=N # GameBridge port (default: read from .cli_control/port)
--godot-cli-no-headless # open a real Godot window
--godot-cli-project-root=DIR # default: pytest rootdir
--godot-cli-time-scale=X # Engine.time_scale applied at daemon startup (e.g. 5 to speed up the suite)
--godot-cli-instances-scope=function|session # godot_instances fixture scope (default: function)
CLI
The CLI is the canonical surface — every GameClient method has a one-line equivalent. Default output is a JSON envelope (--text for legacy strings).
# Lifecycle
godot-cli-control init [--path DIR] [--keep-addon]
godot-cli-control daemon start [--headless | --gui] [--port N --idle-timeout 30m]
godot-cli-control daemon start --record --movie-path X [--fps N] # 录制需真实渲染器,不能与 --headless 同用
godot-cli-control daemon stop [--all | --project PATH]
godot-cli-control daemon status
godot-cli-control daemon ls # list running daemons across all projects
godot-cli-control daemon logs [--tail N] # last N lines of .cli_control/godot.log (works post-mortem)
godot-cli-control run <script.py> [--headless ...]
# Read
godot-cli-control tree [depth]
godot-cli-control get <node_path> <prop> [prop2 ...] # multi-prop = atomic same-frame read
godot-cli-control text <node_path>
godot-cli-control exists <node_path> # exit 0=true, 1=false, 2=infra
godot-cli-control visible <node_path> # exit 0=true, 1=false, 2=infra
godot-cli-control children <node_path> [type-filter]
godot-cli-control pressed
godot-cli-control actions [--all]
godot-cli-control sprite-info <node_path> # Sprite2D/AnimatedSprite2D/TextureRect render state in one call
godot-cli-control errors [--since MARKER] [--limit N] # structured push_error/push_warning log (Godot 4.5+)
# Write / call
godot-cli-control set <node_path> <prop> <json-value>
godot-cli-control call <node_path> <method> [json-args...]
godot-cli-control click <node_path>
# Input
godot-cli-control press|release <action>
godot-cli-control tap <action> [duration]
godot-cli-control hold <action> <duration>
godot-cli-control combo --steps-json '[...]' # or `combo file.json` / `combo -` (stdin)
godot-cli-control combo-cancel
godot-cli-control release-all
# Wait (exit 0=hit, 1=timeout)
godot-cli-control wait-node <node_path> [timeout]
godot-cli-control wait-prop <node_path> <prop> <value> [--op gt|lt|ge|le|ne] [--timeout S] [--tolerance T]
godot-cli-control wait-signal <node_path> <signal> [timeout] # arm BEFORE triggering the action
godot-cli-control wait-frames <n> [--physics]
godot-cli-control wait-time <seconds> # game time — scales with time-scale
# Scene isolation
godot-cli-control scene-reload [--timeout S] # reload current scene, block until ready
godot-cli-control scene-change <res://path.tscn> # switch scene, block until ready
# Time control
godot-cli-control time-scale [value] # read (no arg) or set Engine.time_scale, (0, 100]
godot-cli-control pause | unpause
godot-cli-control step-frames <n> [--physics] # deterministic stepping while paused
# Render (path is required as of 0.2.0)
godot-cli-control screenshot <output.png> [--node <node_path>] # --node crops to that node's screen rect
Output contract
- success:
{"ok": true, "result": <data>}on stdout, exit 0 - error:
{"ok": false, "error": {"code": N, "message": "..."}}on stdout, exit 1 (RPC), 2 (connection / infra), or 64 (usage) --text/--no-jsonswitches back to the legacy human-readable strings; errors then go to stderr.
exists / visible and the wait-* commands propagate their boolean / hit-or-timeout result to the exit code, so shell if works:
if godot-cli-control exists /root/Main/Boss; then
godot-cli-control click /root/Main/Boss
fi
The port is read from .cli_control/port if you don't pass --port, so RPC calls just work after daemon start.
Testing
# Python unit tests + coverage (fails if below 80%)
pip install -e ".[test]"
coverage run -m pytest python/tests/
coverage report
# GUT tests for the Godot plugin (needs GODOT_BIN env var)
# bash (Linux/macOS):
GODOT_BIN=/path/to/godot ./addons/godot_cli_control/tests/run_gut.sh
# cross-platform (Linux/macOS/Windows) — what CI runs:
GODOT_BIN=/path/to/godot python addons/godot_cli_control/tests/run_gut.py
Documentation
See the Godot plugin README for the full RPC reference, activation modes, security model, and known limitations.
Metadata
Release files for godot-cli-control 0.5.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| godot_cli_control-0.5.0.tar.gz | 396.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| godot_cli_control-0.5.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 700.4 kB
Release files / godot_cli_control-0.5.0.tar.gz
| Download URL | godot_cli_control-0.5.0.tar.gz |
|---|---|
| Size | 396.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6877275e4a9fbbd9350cd9adbcd1902bd61c15390c3f73bbb262c9b18ec296ef
|
|
BLAKE2b-256 checksum How to use checksums |
1196d710ea3c7aed33cd2ef164de2ff0371febabe2ea1e3815cba5e57602bed7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 8, 2026.
Transparency logRelease files / godot_cli_control-0.5.0-py3-none-any.whl
| Download URL | godot_cli_control-0.5.0-py3-none-any.whl |
|---|---|
| Size | 304.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
dba4ea4661c7e1b63a1430683649f516c624beaba50255172a97802feb2a3533
|
|
BLAKE2b-256 checksum How to use checksums |
dd8f32bedd01c9467865e707b25aa5bb501d1db751300b7da2bb1c13ad6c95d8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 8, 2026.
Transparency log