Skip to content
Game dev

Godot 4 guard against Godot 3 code

Stop agents writing yield, export var, KinematicBody2D and other Godot 3 code into Godot 4 projects, with a scanner that finds every case and Godot's own check.

Verified install Not agent-tested v1.0.0 MIT updated godotgdscriptpython
Equips
  • 2 2 Rules
  • 1 1 Skills
  • – 0 MCP

The most common complaint from people making games in Godot with coding agents is the same every week: ask for Godot 4 code and get Godot 3. yield(), export var, onready, setget, KinematicBody2D, move_and_slide(velocity) and connect("signal", self, "method") all come from years of Godot 3 tutorials, and Godot 4 refuses to parse most of them. A rules file alone helps, but it is a request, not a check.

This loadout pairs a short Cursor rule and AGENTS.md (a never/always table) with a skill that checks the work. A standard-library Python scanner reports every Godot 3 keyword and renamed API in one pass, using the official upgrade guide and the engine's own 3-to-4 rename map. Godot's parser stops at the first error, so the scanner is what saves the extra runs. Then `godot --headless --check-only --script` confirms each changed script really loads in Godot 4. The verifier runs both against a Godot 4 project and a Godot 3 sample with the pinned Godot 4.7.2 binary.

What's in it

  • Never/always table for GDScript 2.0 in a .gd-scoped Cursor rule
  • Scanner finds 30+ Godot 3 keywords and renamed APIs in one pass, read-only
  • Godot's --check-only confirms each changed script parses in Godot 4
  • CharacterBody2D traps: built-in velocity, move_and_slide() with no arguments
  • Rename table built from the official upgrade guide and engine rename map

You'll need

  • Tools: godot-4, python3
Level 1 · automated

Verification

Verified install

Checked at commit 42646ff · 9 passed, 0 failed, 1 not applicable

CheckResultWhat we found
Listinglisting.yaml matches the schema and declares every file in the folder. Pass
Schema valid; 8 files declared, none missing or extra
  • tested.status: untested (agent tests are separate from Level 1)
Files parseJSON parses; SKILL.md and .mdc front matter is valid YAML with the fields each agent needs; code fences are closed. Pass All 8 files parse; front matter valid
AdaptersCLAUDE.md imports @AGENTS.md, GEMINI.md imports @./AGENTS.md, and both match the generator. Pass
CLAUDE.md and GEMINI.md import AGENTS.md and are up to date
  • CLAUDE.md: matches the generator
  • GEMINI.md: matches the generator
Install pathsEvery file lands where each agent documents it, with no collisions, and the laid-out project is re-discovered correctly. Pass
Correct paths for all 6 agents; layouts re-discovered
  • Claude Code: 6 files, 1 skill in .claude/skills/
  • Codex CLI: 5 files, 1 skill in .agents/skills/
  • Cursor: 6 files, 1 skill in .agents/skills/, 1 rule
  • Gemini CLI: 6 files, 1 skill in .agents/skills/
  • Antigravity CLI: 5 files, 1 skill in .agents/skills/
  • GitHub Copilot: 5 files, 1 skill in .agents/skills/
MCP configmcp.json is structurally valid, converts to each agent's format, declares no literal secrets, and referenced packages exist on npm/PyPI. Not applicable No MCP servers in this loadout
Safety scanThe intake scanner finds no credentials, risky shell, hidden Unicode, instructions hidden in HTML comments or after long blank padding, or prompt-injection and covert wording (such as telling the agent to keep something from you), and the listing's network flag is honest. Pass
Clean: no credentials, risky shell, hidden text or covert instructions
  • Scanner intake-precheck/2: verdict clean; network no (listing says no)
ReferencesLinks and file paths between the loadout's files resolve, including for each agent's installed layout. Pass 3 references between files resolve for every agent
Size limitsFile count and sizes are within the submission limits; SKILL.md bodies stay under 500 lines. Pass
8 files, 19.7 KB total; largest skills/godot-4-guard/scripts/gd3_guard.py 7.7 KB
  • Limits: 30 files, 32 KB per file, 128 KB total, 500 lines per skill or rule
LicenceA recognised SPDX licence, with the licence text available. Pass MIT, by SetupBay; text in the repository LICENSE
Starter buildA minimal fixture project with the loadout applied installs and builds or type-checks, and the loadout's own code examples compile. Pass
The godot-4-guard scanner passes a Godot 4 project and flags every Godot 3 construct in the skill's sample; Godot 4.7.2 itself parses the project's scripts with --check-only (the autoload-using player through the headless run), rejects the Godot 3 sample, and runs the project headless with no script
  • Applied 12 loadout files at every agent's paths
  • godot 4.7.2-stable
  • ✓ scanner: Godot 4 project is clean: bash -c python3 .agents/skills/godot-4-guard/scripts/gd3_guard.py; echo "exit=$?"
  • ✓ scanner: Godot 3 sample is flagged line by line: bash -c mkdir -p /tmp/gd3-sample && cp .agents/skills/godot-4-guard/references/godot3-sample.gd.txt /tmp/gd3-sample/player3.gd && python3 .agents/skills/godot-4-guard/scripts/gd3_guard.py /tmp/gd3-sample; echo "exit=$?"
  • ✓ import project (headless): ${GODOT_BIN} --headless --import --path .
  • ✓ godot --check-only on scripts that use no autoload: bash -c for s in res://scenes/player/stats.gd res://autoload/game_state.gd; do "$GODOT_BIN" --headless --path . --check-only --script $s 2>&1 || echo "FAILED $s"; done; echo checked
  • ✓ godot --check-only rejects the Godot 3 sample: bash -c mkdir -p legacy && cp .agents/skills/godot-4-guard/references/godot3-sample.gd.txt legacy/player3.gd && "$GODOT_BIN" --headless --path . --check-only --script res://legacy/player3.gd 2>&1; echo "exit=$?"; rm -rf legacy
  • ✓ run headless (covers the player script, which uses the GameState autoload): ${GODOT_BIN} --headless --path . --quit-after 300
  • Covers: The scanner's rules against real Godot 3 code: keywords (tool, export, onready, setget, yield), connect with string targets, KinematicBody2D, move_and_slide(velocity), renamed functions
  • Covers: No false positives on a typed Godot 4 project with an inline setter, annotations, await and callables
  • Covers: Godot's own --check-only as the second check: project scripts load, the Godot 3 sample does not
  • Covers: --check-only does not load autoloads, so scripts that use one are checked by the headless run instead (the skill says so)
  • Covers: Not covered: .tscn scenes saved by Godot 3 and shaders (the skill tells the agent to hand those to the editor)

What this means: a script checked the files themselves: they parse, they install where each agent looks, any MCP config is sound, the safety scan is clean, references between files resolve, and, where a free and repeatable fixture exists, a small starter project builds with the loadout applied. No AI agent was run. Per-agent results from agent runs are separate and stay untested until an agent actually runs.

Install

Choose your agent. Run the commands from your project root, or download a zip already laid out in the right folders.

Claude Code reads CLAUDE.md (imports AGENTS.md with @AGENTS.md) and skills from .claude/skills/.

Download zip
curl -fsSL --create-dirs -o AGENTS.md https://setupbay.com/raw/godot-4-api-guard/1.0.0/AGENTS.md
curl -fsSL --create-dirs -o CLAUDE.md https://setupbay.com/raw/godot-4-api-guard/1.0.0/CLAUDE.md
curl -fsSL --create-dirs -o .claude/skills/godot-4-guard/SKILL.md https://setupbay.com/raw/godot-4-api-guard/1.0.0/skills/godot-4-guard/SKILL.md
curl -fsSL --create-dirs -o .claude/skills/godot-4-guard/scripts/gd3_guard.py https://setupbay.com/raw/godot-4-api-guard/1.0.0/skills/godot-4-guard/scripts/gd3_guard.py
curl -fsSL --create-dirs -o .claude/skills/godot-4-guard/references/rename-table.md https://setupbay.com/raw/godot-4-api-guard/1.0.0/skills/godot-4-guard/references/rename-table.md
curl -fsSL --create-dirs -o .claude/skills/godot-4-guard/references/godot3-sample.gd.txt https://setupbay.com/raw/godot-4-api-guard/1.0.0/skills/godot-4-guard/references/godot3-sample.gd.txt
FileGoes to
AGENTS.mdAGENTS.md
CLAUDE.mdCLAUDE.md
skills/godot-4-guard/SKILL.md.claude/skills/godot-4-guard/SKILL.md
skills/godot-4-guard/scripts/gd3_guard.py.claude/skills/godot-4-guard/scripts/gd3_guard.py
skills/godot-4-guard/references/rename-table.md.claude/skills/godot-4-guard/references/rename-table.md
skills/godot-4-guard/references/godot3-sample.gd.txt.claude/skills/godot-4-guard/references/godot3-sample.gd.txt

Already have an AGENTS.md or CLAUDE.md? The commands above overwrite it — download the file instead and merge the sections you want.

Files

Rules AGENTS.md Godot 4 version facts, rules and the done checklist. 28 lines
Download Raw
# AGENTS.md: Godot 4 project (Godot 3 guard)

This is a **Godot 4** project. Much of what you remember about Godot is Godot 3, and Godot 4 does not parse it.
Treat every GDScript line you write as suspect until the guard and Godot have both checked it.

## Version facts
- Engine: Godot 4.x (see `project.godot`: `config_version=5`; the 4.x minor is in `config/features`).
- GDScript 2.0: annotations (`@export`, `@onready`, `@tool`, `@rpc`), `await`, first-class signals and callables,
  typed arrays (`Array[Node]`), property setters/getters inline.
- If you are not sure an API exists in Godot 4, look it up in the class reference for this version
  (https://docs.godotengine.org/en/stable/classes/) instead of guessing. Do not copy Godot 3 tutorials or answers.

## Rules
- Write Godot 4 only. The `godot-4-guard` skill has the full Godot 3 → 4 rename table; the common traps are:
  `@export`/`@onready`/`@tool` (not keywords), `await` (not `yield`), `CharacterBody2D` (not `KinematicBody2D`),
  `instantiate()`, `is_empty()`, `deg_to_rad`, `randf_range`, `snapped`, `change_scene_to_file`, `Time.get_ticks_msec()`.
- Static types everywhere: `var speed: float = 200.0`, `func hit(amount: int) -> void:`.
- `CharacterBody2D`/`3D` have a built-in `velocity`. Do not declare your own; call `move_and_slide()` with no arguments.
- Signals: `signal died`, `died.emit()`, `button.pressed.connect(_on_pressed)`.
- Never hand-edit `.tscn` or `.tres` files saved by Godot 3 (`format=2`), and never edit `.godot/`.
- Never run the project upgrade tool or convert a Godot 3 project yourself. Ask the human, after a commit.

## Before you say "done"
1. Run the `godot-4-guard` scanner on the folders you touched; it must report `0 error(s)`.
2. Run `godot --headless --path . --check-only --script res://<file>.gd` for every changed script; no `SCRIPT ERROR`.
3. Run the project headless for a few seconds (`godot --headless --path . --quit-after 300`) and read the output.
4. Tell the human what to try by hand (controls, feel) in one or two lines.
Adapter CLAUDE.md Imports AGENTS.md for Claude Code. 11 lines
Download Raw
# CLAUDE.md

@AGENTS.md

## Claude Code notes

- Skills from this loadout live in `.claude/skills/`: `godot-4-guard`. Use them when their description matches the task.
- Follow AGENTS.md above; where this file and AGENTS.md disagree, AGENTS.md wins.

<!-- From SetupBay "Godot 4 guard against Godot 3 code" v1.0.0 — https://setupbay.com/loadouts/godot-4-api-guard/ -->
Adapter GEMINI.md Imports AGENTS.md for Gemini CLI. 11 lines
Download Raw
# GEMINI.md

@./AGENTS.md

## Gemini CLI notes

- Skills from this loadout live in `.agents/skills/`: `godot-4-guard`. Use them when their description matches the task.
- Follow AGENTS.md above; where this file and AGENTS.md disagree, AGENTS.md wins.

<!-- From SetupBay "Godot 4 guard against Godot 3 code" v1.0.0 — https://setupbay.com/loadouts/godot-4-api-guard/ -->
Cursor rule .cursor/rules/godot-4-only.mdc Never/always table for .gd files. 25 lines
Download Raw
---
description: Godot 4 GDScript only; never Godot 3 syntax or APIs
globs: "**/*.gd"
alwaysApply: false
---

This project is Godot 4 (`config_version=5`). Never write Godot 3 GDScript.

| Never (Godot 3) | Always (Godot 4) |
|---|---|
| `tool`, `export var`, `onready var` | `@tool`, `@export var`, `@onready var` |
| `yield(timer, "timeout")` | `await get_tree().create_timer(t).timeout` |
| `setget` | `var x: int: set = _set_x` |
| `connect("sig", self, "_m")` | `sig.connect(_m)` |
| `KinematicBody2D`, `Spatial` | `CharacterBody2D`, `Node3D` |
| `move_and_slide(velocity, Vector2.UP)` | set `velocity`, then `move_and_slide()` |
| `.instance()`, `.empty()` | `.instantiate()`, `.is_empty()` |
| `deg2rad`, `rand_range`, `stepify` | `deg_to_rad`, `randf_range`, `snapped` |
| `change_scene(path)` | `change_scene_to_file(path)` |
| `OS.get_ticks_msec()`, `File.new()` | `Time.get_ticks_msec()`, `FileAccess.open()` |
| `Tween.new()` + `interpolate_property` | `create_tween().tween_property()` |

- Type every variable, parameter and return value.
- After editing a `.gd` file, run the `godot-4-guard` skill's scanner and `godot --headless --check-only --script res://<file>`.
Skill skills/godot-4-guard/SKILL.md Scan for Godot 3 code, then let Godot check each script. 54 lines
Download Raw
---
name: godot-4-guard
description: Check GDScript for Godot 3 syntax and renamed APIs before saying a Godot 4 change is done. Use after writing or editing any .gd file, when Godot reports a parse error, or when porting Godot 3 code, tutorials or snippets into a Godot 4 project.
---

# Godot 4 guard

Models trained on years of Godot 3 tutorials still write `yield`, `export var`, `onready`, `KinematicBody2D` and
`move_and_slide(velocity)` into Godot 4 projects. Godot 4 refuses to parse most of it, but its parser stops at the
first error, so fixing one line at a time wastes runs. This skill finds them all at once, then lets Godot itself confirm.

## 1. Confirm the project is Godot 4
- `project.godot` must contain `config_version=5`. If it says `config_version=4`, this is a Godot 3 project:
  stop and tell the human. Porting a whole project is the editor's job (Project → Tools → Upgrade Project), after a commit.
- `godot --version` should print `4.x`. If `godot` is not on the PATH, ask where the Godot 4 binary is.

## 2. Scan for Godot 3 code
```bash
python3 .agents/skills/godot-4-guard/scripts/gd3_guard.py scenes autoload
```
(Use the path the skill was installed to: `.claude/skills/...` for Claude Code, `.agents/skills/...` for the others.
With no arguments it scans the whole project, skipping `.godot/`, `addons/` and agent folders.)

- Every `error` line is Godot 3 code. Fix each one with the replacement the line gives. `references/rename-table.md`
  has the full table with examples.
- `warn` lines are style: `emit_signal("x")` still works but `x.emit()` is the Godot 4 form; untyped functions
  should get `-> void` or a return type; `TileMap` is deprecated since 4.3 in favour of `TileMapLayer`.
- Run it again until it prints `0 error(s)`.

## 3. Let Godot check every changed script
```bash
godot --headless --path . --check-only --script res://scenes/player/player.gd
```
- Run it for each `.gd` file you changed. Any `SCRIPT ERROR` or `Parse Error` line means the script does not load,
  whatever the exit code says.
- If it complains about an unknown `class_name`, run `godot --headless --path . --import` once so the global class
  cache exists, then check again.
- `--check-only` does not load autoloads. A script that names one (`GameState.score = 10`) reports
  `Identifier not found: GameState` even though the game runs. For those scripts, the headless run below is the check.
- Then run the project briefly: `godot --headless --path . --quit-after 300` and read the output for errors.

## 4. Fixes that need care
- **`move_and_slide`**: `CharacterBody2D` already has `velocity` and `up_direction`. Delete any `var velocity`
  you added (it now clashes with the built-in), set `velocity`, call `move_and_slide()` with no arguments.
- **`yield` with a function state**: `await some_function()` waits for a coroutine; `await signal` waits for a signal.
- **`setget`**: move the setter into the property (`var hp: int = 3: set = set_hp`). In Godot 4 the setter also
  runs when the class assigns `hp` itself (Godot 3 skipped it without `self.`), so check code that relied on that.
  Inside an inline `set(value):` block, `hp = value` stores the value directly without calling the setter again.
- **Scenes (`.tscn`)**: never rewrite a Godot 3 scene by hand. Ask the human to open it in the Godot 4 editor.

## 5. Report
Say how many Godot 3 lines you replaced, which files, and that the guard and `--check-only` were clean. If something
could only be checked by playing (input feel, physics), say what the human should try.
Skill file skills/godot-4-guard/scripts/gd3_guard.py Read-only scanner for Godot 3 GDScript and APIs. 138 lines
Download Raw
#!/usr/bin/env python3
"""Find Godot 3 GDScript and API names in a Godot 4 project.

Usage: python3 gd3_guard.py [PATH ...]        (default: the current folder)
       python3 gd3_guard.py --list            (print every rule)

Scans .gd files (skips .godot/ and addons/ unless named). Prints one line per finding:
  path:line: [rule] what is wrong -> what Godot 4 uses
Exit 0 when nothing at "error" level is found, 1 when something is, 2 on bad usage.
Standard library only. Read-only: it never edits a file.
"""
import os
import re
import sys

# (rule id, level, regex, message). Patterns are matched against code with comments and strings removed.
RULES = [
    ("yield", "error", r"\byield\s*\(", "yield() was removed -> await signal (await get_tree().create_timer(t).timeout)"),
    ("onready-keyword", "error", r"^\s*onready\s+var\b", "onready keyword -> @onready var"),
    ("export-keyword", "error", r"^\s*export\b(\s*\(.*\))?\s+var\b", "export keyword -> @export var (@export_range, @export_enum, ...)"),
    ("tool-keyword", "error", r"^\s*tool\s*$", "tool keyword -> @tool annotation"),
    ("setget", "error", r"\bsetget\b", "setget -> property syntax: var x: int: set = _set_x, get = _get_x"),
    ("rpc-keywords", "error", r"^\s*(remote|remotesync|master|mastersync|puppet|puppetsync)\s+func\b", "network keywords -> @rpc(\"any_peer\") / @rpc(\"authority\") annotations"),
    ("connect-string-target", "error", r"\.connect\(\s*\"[A-Za-z_]\w*\"\s*,\s*[A-Za-z_]\w*\s*,", "connect(\"signal\", obj, \"method\") -> signal_name.connect(obj.method)"),
    ("kinematic-body", "error", r"\bKinematicBody(2D)?\b", "KinematicBody / KinematicBody2D -> CharacterBody3D / CharacterBody2D"),
    ("spatial", "error", r"\b(Spatial|SpatialMaterial)\b", "Spatial -> Node3D, SpatialMaterial -> StandardMaterial3D"),
    ("move-and-slide-args", "error", r"\bmove_and_slide\(\s*[^)\s]", "move_and_slide(velocity, ...) -> set velocity (and up_direction), then move_and_slide()"),
    ("instance", "error", r"\.instance\(\s*\)", "PackedScene.instance() -> instantiate()"),
    ("array-empty", "error", r"\.empty\(\s*\)", ".empty() -> .is_empty()"),
    ("deg2rad", "error", r"\b(deg2rad|rad2deg)\s*\(", "deg2rad / rad2deg -> deg_to_rad / rad_to_deg"),
    ("rand-range", "error", r"\brand_range\s*\(", "rand_range -> randf_range (floats) or randi_range (ints)"),
    ("stepify", "error", r"\bstepify\s*\(", "stepify -> snapped"),
    ("str2var", "error", r"\b(str2var|var2str|bytes2var|var2bytes)\s*\(", "str2var / var2str -> str_to_var / var_to_str"),
    ("linear-interpolate", "error", r"\.linear_interpolate\s*\(", "linear_interpolate -> lerp"),
    ("pool-arrays", "error", r"\bPool(Byte|Int|Real|String|Vector2|Vector3|Color)Array\b", "PoolXArray -> PackedXArray (PoolRealArray -> PackedFloat32Array)"),
    ("change-scene", "error", r"\.change_scene\s*\(", "change_scene(path) -> change_scene_to_file(path)"),
    ("change-scene-to", "error", r"\.change_scene_to\s*\(", "change_scene_to(packed) -> change_scene_to_packed(packed)"),
    ("os-ticks", "error", r"\bOS\.get_(ticks_msec|ticks_usec|unix_time|datetime)\s*\(", "OS time functions -> Time.get_ticks_msec(), Time.get_unix_time_from_system(), ..."),
    ("mouse-button", "error", r"\bBUTTON_(LEFT|RIGHT|MIDDLE|WHEEL_UP|WHEEL_DOWN)\b", "BUTTON_LEFT -> MOUSE_BUTTON_LEFT"),
    ("file-directory", "error", r"\b(File|Directory)\.new\(\s*\)", "File.new() / Directory.new() -> FileAccess.open(...) / DirAccess.open(...)"),
    ("funcref", "error", r"\bfuncref\s*\(", "funcref(obj, \"m\") -> Callable(obj, \"m\") or obj.m"),
    ("tween-node", "error", r"\.interpolate_property\s*\(|\bTween\.new\(\s*\)", "Tween node -> var t := create_tween(); t.tween_property(...)"),
    ("editor-hint", "error", r"\bEngine\.editor_hint\b", "Engine.editor_hint -> Engine.is_editor_hint()"),
    ("rect-props", "error", r"\brect_(position|size|min_size|rotation|scale|global_position|pivot_offset)\b", "Control rect_* -> position, size, custom_minimum_size, rotation, scale, pivot_offset"),
    ("raycast-cast-to", "error", r"\bcast_to\b", "RayCast cast_to -> target_position"),
    ("renamed-nodes", "error", r"\b(Position2D|Position3D|VisibilityNotifier2D|VisibilityNotifier|YSort|Navigation2D|Particles2D|Light2D\.MODE_)\b", "renamed or removed node -> Marker2D/3D, VisibleOnScreenNotifier2D/3D, y_sort_enabled, NavigationAgent2D, GPUParticles2D"),
    ("set-input-handled", "error", r"get_tree\(\)\.set_input_as_handled\s*\(", "get_tree().set_input_as_handled() -> get_viewport().set_input_as_handled()"),
    ("os-window", "error", r"\bOS\.(window_size|window_fullscreen|set_window_title|window_position)\b", "OS window API -> get_window().size / DisplayServer.window_set_mode(...)"),
    ("emit-signal-string", "warn", r"\bemit_signal\(\s*\"", "emit_signal(\"x\") still works, but Godot 4 style is x.emit(...)"),
    ("tilemap-node", "warn", r"\bextends\s+TileMap\b|:\s*TileMap\b", "TileMap is deprecated since Godot 4.3 -> one TileMapLayer node per layer"),
    ("untyped-func", "warn", r"^\s*func\s+\w+\([^)]*\)\s*:", "function without a return type -> add -> void (or the type it returns)"),
]
COMPILED = [(rid, lvl, re.compile(rx), msg) for rid, lvl, rx, msg in RULES]
SKIP_DIRS = {".godot", ".import", ".git", ".agents", ".claude", ".cursor", "addons", "build", "export"}


def code_only(line):
    """Drop string contents and trailing comments, keeping quote marks so patterns can see a string was there."""
    out, i, q = [], 0, None
    while i < len(line):
        c = line[i]
        if q:
            if c == "\\":
                i += 2
                continue
            if c == q:
                q = None
                out.append(c)
            i += 1
            continue
        if c in "\"'":
            q = c
            out.append(c)
        elif c == "#":
            break
        else:
            out.append(c)
        i += 1
    return "".join(out)


def keep_strings_for(rule_id):
    # A few rules need to see that the first argument is a string literal (not its contents).
    return rule_id in ("connect-string-target", "emit-signal-string")


def scan_file(path):
    found = []
    with open(path, encoding="utf-8", errors="replace") as fh:
        for n, raw in enumerate(fh, 1):
            stripped = code_only(raw.rstrip("\n"))
            if not stripped.strip():
                continue
            for rid, lvl, rx, msg in COMPILED:
                # Strings are reduced to "" by code_only; patterns that need a quoted first arg match `""`.
                text = stripped
                if keep_strings_for(rid):
                    text = re.sub(r"\"\"", "\"s\"", stripped)
                if rx.search(text):
                    found.append((path, n, rid, lvl, msg))
    return found


def iter_files(paths):
    for p in paths:
        if os.path.isfile(p):
            if p.endswith(".gd"):
                yield p
            continue
        for root, dirs, files in os.walk(p):
            dirs[:] = sorted(d for d in dirs if d not in SKIP_DIRS)
            for f in sorted(files):
                if f.endswith(".gd"):
                    yield os.path.join(root, f)


def main(argv):
    if argv[:1] == ["--list"]:
        for rid, lvl, _, msg in RULES:
            print(f"{lvl:5} {rid:22} {msg}")
        return 0
    if any(a.startswith("-") for a in argv):
        print(__doc__.strip(), file=sys.stderr)
        return 2
    paths = argv or ["."]
    files = list(iter_files(paths))
    findings = [f for path in files for f in scan_file(path)]
    for path, n, rid, lvl, msg in findings:
        print(f"{os.path.relpath(path)}:{n}: {lvl} [{rid}] {msg}")
    errors = sum(1 for f in findings if f[3] == "error")
    warns = len(findings) - errors
    print(f"gd3-guard: {len(files)} file(s), {errors} error(s), {warns} warning(s)")
    return 1 if errors else 0


if __name__ == "__main__":
    sys.exit(main(sys.argv[1:]))
Skill file skills/godot-4-guard/references/rename-table.md Godot 3 to 4 table of syntax, nodes and methods. 65 lines
Download Raw
# Godot 3 → Godot 4: what agents get wrong most

Sources: the official "Upgrading from Godot 3 to Godot 4" guide and the engine's own rename map
(`editor/project_upgrade/renames_map_3_to_4.cpp`). The guard script checks every row marked ✓.

## GDScript syntax

| Godot 3 | Godot 4 | ✓ |
|---|---|---|
| `tool` | `@tool` | ✓ |
| `export var x = 1`, `export(int) var x` | `@export var x: int = 1`, `@export_range(0, 10) var x: int` | ✓ |
| `onready var s = $Sprite` | `@onready var s: Sprite2D = $Sprite2D` | ✓ |
| `yield(obj, "signal")` | `await obj.signal` | ✓ |
| `yield(get_tree().create_timer(1), "timeout")` | `await get_tree().create_timer(1.0).timeout` | ✓ |
| `var hp setget set_hp, get_hp` | `var hp: int: set = set_hp, get = get_hp` (or inline `set(value):`) | ✓ |
| `connect("pressed", self, "_on_pressed")` | `pressed.connect(_on_pressed)` | ✓ |
| `emit_signal("died")` | `died.emit()` (the old form still works; warning only) | ✓ |
| `remote func`, `master func`, `puppet func` | `@rpc("any_peer")`, `@rpc("authority")` | ✓ |
| `funcref(obj, "m")` | `Callable(obj, "m")` or `obj.m` | ✓ |

## Nodes and classes

| Godot 3 | Godot 4 | ✓ |
|---|---|---|
| `KinematicBody2D`, `KinematicBody` | `CharacterBody2D`, `CharacterBody3D` | ✓ |
| `Spatial`, `SpatialMaterial` | `Node3D`, `StandardMaterial3D` | ✓ |
| `Position2D` / `Position3D` | `Marker2D` / `Marker3D` | ✓ |
| `VisibilityNotifier2D` | `VisibleOnScreenNotifier2D` | ✓ |
| `YSort` | any `CanvasItem` with `y_sort_enabled = true` | ✓ |
| `Navigation2D` | `NavigationAgent2D` + `NavigationRegion2D` | ✓ |
| `Particles2D` | `GPUParticles2D` (or `CPUParticles2D`) | ✓ |
| `Tween` node, `interpolate_property` | `create_tween().tween_property(node, "position", to, 0.3)` | ✓ |
| `File.new()`, `Directory.new()` | `FileAccess.open(path, FileAccess.READ)`, `DirAccess.open(path)` | ✓ |
| `PoolStringArray`, `PoolRealArray`, ... | `PackedStringArray`, `PackedFloat32Array`, ... | ✓ |
| `TileMap` (4.0–4.2) | `TileMapLayer`, one node per layer (TileMap deprecated in 4.3) | warn |

## Methods and properties

| Godot 3 | Godot 4 | ✓ |
|---|---|---|
| `move_and_slide(velocity, Vector2.UP)` | set `velocity` (built in) and `up_direction`, then `move_and_slide()` | ✓ |
| `scene.instance()` | `scene.instantiate()` | ✓ |
| `array.empty()` | `array.is_empty()` | ✓ |
| `deg2rad`, `rad2deg` | `deg_to_rad`, `rad_to_deg` | ✓ |
| `rand_range(a, b)` | `randf_range(a, b)` / `randi_range(a, b)` | ✓ |
| `stepify(x, s)` | `snapped(x, s)` | ✓ |
| `str2var`, `var2str` | `str_to_var`, `var_to_str` | ✓ |
| `a.linear_interpolate(b, t)` | `a.lerp(b, t)` | ✓ |
| `get_tree().change_scene(path)` | `get_tree().change_scene_to_file(path)` | ✓ |
| `change_scene_to(packed)` | `change_scene_to_packed(packed)` | ✓ |
| `OS.get_ticks_msec()` | `Time.get_ticks_msec()` | ✓ |
| `OS.window_size`, `OS.window_fullscreen` | `get_window().size`, `DisplayServer.window_set_mode(...)` | ✓ |
| `BUTTON_LEFT` | `MOUSE_BUTTON_LEFT` | ✓ |
| `Engine.editor_hint` | `Engine.is_editor_hint()` | ✓ |
| `rect_position`, `rect_size`, `rect_min_size` | `position`, `size`, `custom_minimum_size` | ✓ |
| `RayCast2D.cast_to` | `target_position` | ✓ |
| `get_tree().set_input_as_handled()` | `get_viewport().set_input_as_handled()` | ✓ |

## Things the guard can't see

- Signal argument changes (for example `body_entered` handlers that expected extra binds): read the class reference.
- `.tscn` files saved by Godot 3 (`format=2`). Never hand-convert scenes; open the project in the Godot 4 editor
  and let it convert, after a commit.
- Shader changes (`hint_color` → `source_color`, renamed built-ins). Check shaders against the shading reference.
Skill file skills/godot-4-guard/references/godot3-sample.gd.txt A Godot 3 player script for checking the guard. 34 lines
Download Raw
# A Godot 3 player script, the kind agents still write into Godot 4 projects.
# Kept as .txt so Godot never tries to load it. The guard's own check copies it to a .gd file.
tool
extends KinematicBody2D

signal died

export(float) var speed = 200.0
export var jump_force = 400.0
onready var sprite = $Sprite
var hp = 3 setget set_hp
var motion = Vector2()

func _ready():
	$HurtBox.connect("body_entered", self, "_on_hurt")
	yield(get_tree().create_timer(0.5), "timeout")

func _physics_process(delta):
	motion.x = Input.get_action_strength("move_right") - Input.get_action_strength("move_left")
	motion.x *= speed
	motion.y += 980 * delta
	motion = move_and_slide(motion, Vector2.UP)

func set_hp(value):
	hp = value
	if hp <= 0:
		emit_signal("died")
		get_tree().change_scene("res://scenes/ui/game_over.tscn")

func _on_hurt(body):
	var puff = preload("res://scenes/fx/puff.tscn").instance()
	puff.rotation = deg2rad(rand_range(0, 360))
	add_child(puff)

All files: source zip · Licence MIT · By SetupBay · First listed

Installing and checks

How do I install Godot 4 guard against Godot 3 code in Claude Code?

From your project root, put AGENTS.md, CLAUDE.md, skills/godot-4-guard/SKILL.md at .claude/skills/godot-4-guard/SKILL.md, skills/godot-4-guard/scripts/gd3_guard.py at .claude/skills/godot-4-guard/scripts/gd3_guard.py, skills/godot-4-guard/references/rename-table.md at .claude/skills/godot-4-guard/references/rename-table.md, skills/godot-4-guard/references/godot3-sample.gd.txt at .claude/skills/godot-4-guard/references/godot3-sample.gd.txt. The "Claude Code" tab on this page has the curl commands, or download https://setupbay.com/download/godot-4-api-guard/claude-code.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.

How do I install Godot 4 guard against Godot 3 code in Codex CLI?

From your project root, put AGENTS.md, skills/godot-4-guard/SKILL.md at .agents/skills/godot-4-guard/SKILL.md, skills/godot-4-guard/scripts/gd3_guard.py at .agents/skills/godot-4-guard/scripts/gd3_guard.py, skills/godot-4-guard/references/rename-table.md at .agents/skills/godot-4-guard/references/rename-table.md, skills/godot-4-guard/references/godot3-sample.gd.txt at .agents/skills/godot-4-guard/references/godot3-sample.gd.txt. The "Codex CLI" tab on this page has the curl commands, or download https://setupbay.com/download/godot-4-api-guard/codex.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.

How do I install Godot 4 guard against Godot 3 code in Cursor?

From your project root, put AGENTS.md, .cursor/rules/godot-4-only.mdc, skills/godot-4-guard/SKILL.md at .agents/skills/godot-4-guard/SKILL.md, skills/godot-4-guard/scripts/gd3_guard.py at .agents/skills/godot-4-guard/scripts/gd3_guard.py, skills/godot-4-guard/references/rename-table.md at .agents/skills/godot-4-guard/references/rename-table.md, skills/godot-4-guard/references/godot3-sample.gd.txt at .agents/skills/godot-4-guard/references/godot3-sample.gd.txt. The "Cursor" tab on this page has the curl commands, or download https://setupbay.com/download/godot-4-api-guard/cursor.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.

How do I install Godot 4 guard against Godot 3 code in Gemini CLI?

From your project root, put AGENTS.md, GEMINI.md, skills/godot-4-guard/SKILL.md at .agents/skills/godot-4-guard/SKILL.md, skills/godot-4-guard/scripts/gd3_guard.py at .agents/skills/godot-4-guard/scripts/gd3_guard.py, skills/godot-4-guard/references/rename-table.md at .agents/skills/godot-4-guard/references/rename-table.md, skills/godot-4-guard/references/godot3-sample.gd.txt at .agents/skills/godot-4-guard/references/godot3-sample.gd.txt. The "Gemini CLI" tab on this page has the curl commands, or download https://setupbay.com/download/godot-4-api-guard/gemini-cli.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.

How do I install Godot 4 guard against Godot 3 code in Antigravity CLI?

From your project root, put AGENTS.md, skills/godot-4-guard/SKILL.md at .agents/skills/godot-4-guard/SKILL.md, skills/godot-4-guard/scripts/gd3_guard.py at .agents/skills/godot-4-guard/scripts/gd3_guard.py, skills/godot-4-guard/references/rename-table.md at .agents/skills/godot-4-guard/references/rename-table.md, skills/godot-4-guard/references/godot3-sample.gd.txt at .agents/skills/godot-4-guard/references/godot3-sample.gd.txt. The "Antigravity CLI" tab on this page has the curl commands, or download https://setupbay.com/download/godot-4-api-guard/antigravity-cli.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.

How do I install Godot 4 guard against Godot 3 code in GitHub Copilot?

From your project root, put AGENTS.md, skills/godot-4-guard/SKILL.md at .agents/skills/godot-4-guard/SKILL.md, skills/godot-4-guard/scripts/gd3_guard.py at .agents/skills/godot-4-guard/scripts/gd3_guard.py, skills/godot-4-guard/references/rename-table.md at .agents/skills/godot-4-guard/references/rename-table.md, skills/godot-4-guard/references/godot3-sample.gd.txt at .agents/skills/godot-4-guard/references/godot3-sample.gd.txt. The "GitHub Copilot" tab on this page has the curl commands, or download https://setupbay.com/download/godot-4-api-guard/copilot.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.

What has been checked in Godot 4 guard against Godot 3 code?

It passed the Level 1 automated install checks on 2026-10-24: the files parse, install to each agent's documented paths, and pass the safety scan. No AI agent has run it yet, so no agent pass or fail is claimed.

More for Claude Code, Codex, Cursor, Gemini CLI, Antigravity CLI and GitHub Copilot.