Godot’s GDScript looks close enough to Python that an AI coding agent will happily fill in the parts your CLAUDE.md doesn’t specify — and that’s exactly the problem. Optional static typing means untyped code still runs, so Claude Code has no compiler forcing it toward var speed: float = 300.0 over a bare var speed = 300.0. The TileMap node still works even though it’s been deprecated in favor of TileMapLayer since Godot 4.3, so an agent trained on years of pre-4.3 tutorials will reach for the class most of its training data still uses. This guide covers a full CLAUDE.md template for Godot 4.x projects: static typing rules, autoload/singleton architecture, signal-based decoupling, and the version-drift traps specific to a fast-moving open-source engine — the gallery’s own Godot 4 GDScript Game Development rule set is a .cursorrules file with no CLAUDE.md equivalent, which is the gap this template exists to close.
Why Godot Needs More Explicit Rules Than It Looks Like
Optional typing means nothing enforces the convention Claude Code should default to. GDScript lets you write var health = 100 or var health: int = 100 and both execute identically at the interpreter level — there’s no linter running by default, no compiler error for the untyped version. The official GDScript style guide recommends static typing for the IDE support and error detection it enables, but an agent has no signal to prefer it unless CLAUDE.md states it as a hard rule, not a suggestion.
The engine changes faster than most tutorials get updated. TileMap was marked deprecated and replaced by one-TileMapLayer-node-per-layer in Godot 4.3 (August 2024) — a change significant enough that the editor ships an automatic migration tool, but TileMap still works and still appears in the majority of tilemap tutorials published before that release. An agent asked to “add a tilemap” has a good chance of writing the deprecated node unless told otherwise.
Community .cursorrules and CLAUDE.md files for Godot skew toward AI-native workflows, not baseline conventions. Searching for Godot AI-coding content in 2026 mostly surfaces MCP servers that give an agent live access to the running editor’s scene tree, and Claude Code Skills that scaffold entire projects — genuinely useful, but none of it substitutes for a CLAUDE.md that states your project’s own naming conventions, scene organization, and signal architecture. The gallery’s Godot 4 GDScript Game Development entry covers exactly that ground — strict typing, scene organization, signal patterns — but it’s written for Cursor’s .cursorrules format, not CLAUDE.md.
Complete CLAUDE.md Template for Godot Projects
This targets Godot 4.3+ with GDScript as the primary scripting language.
# Godot Project: [ProjectName]
## Engine & Run
- Godot version: 4.3+ (state exact version if the project pins one via `.godot/`)
- Open editor: `godot --editor` (or launch the Godot app directly)
- Run headless (no display): `godot --headless`
- Pre-import assets before any headless command in a fresh checkout: `godot --headless --import`
- Syntax-check a single script without running it: `godot --headless --check-only -s res://path/to/script.gd`
## Static Typing (required, not optional)
- Every new `var`, function parameter, and return type is typed — either explicitly (`var speed: float = 300.0`) or inferred with `:=` (`var speed := 300.0`)
- Untyped GDScript still runs — there is no compiler forcing this, so do not default to bare `var x = value`
- Function signatures always declare parameter and return types: `func take_damage(amount: int) -> void:`
- Use `@export` with an explicit type for anything tunable from the Inspector: `@export var max_health: int = 100`
## Naming Conventions (from the official GDScript style guide)
- Files and functions: `snake_case` (`player_controller.gd`, `func load_level():`)
- Classes and nodes: `PascalCase` (`class_name PlayerController`, node named `Player`)
- Constants and enum members: `CONSTANT_CASE` (`const MAX_SPEED = 200`)
- Signals: `snake_case`, named as a past-tense event (`signal door_opened`, not `signal open_door`)
- Private functions/variables: prefix with a single underscore (`_internal_state`, `func _process_input():`)
- Code order inside a script: decorators → `class_name`/`extends` → signals/enums/constants → variables (`@export`, then regular, then `@onready`) → methods (`_ready`/`_process` first, public, then private)
## Scene Architecture
- One responsibility per scene — a scene that both defines a reusable entity and wires up level-specific logic should be split
- Global managers (game state, audio, save/load) are autoload singletons registered in Project Settings > AutoLoad, not `get_node("/root/...")` lookups scattered through scripts
- Reference sibling/child nodes with `@onready var sprite: Sprite2D = $Sprite2D` set once at the top of the script — not repeated `get_node()` or `$Path/To/Node` calls inside `_process()`
- `TileMapLayer` for all new tilemap work — `TileMap` is deprecated since Godot 4.3 and only appears here if the project predates that version and hasn't migrated yet
## Signals for Decoupling
- A node never reaches up into its parent or a sibling to call a method directly — it emits a signal and lets the listener decide what to do
- Connect signals in code (`node.signal_name.connect(_on_signal_name)`) for anything wired up dynamically; use the editor's Signal panel only for static, scene-authored connections
- Signal callback naming: `_on_<emitter>_<signal_name>` (e.g., `_on_door_opened`)
## Testing
- [GUT](https://github.com/bitwes/Gut) or [GdUnit4](https://github.com/MikeSchulze/gdUnit4) — state which one the project uses; do not mix both
- Headless test runs in CI: `godot --headless --import` first (cold checkouts have no `.godot/` import cache), then the test framework's CLI runner
- Pure logic (damage calculation, inventory rules) gets unit tests; scene-dependent behavior (physics, input) is tested through the framework's scene-tree test scenes, not asserted from a script running outside the tree
Static Typing: The Rule an Agent Won’t Infer on Its Own
This is the highest-leverage rule in a Godot CLAUDE.md, because untyped GDScript is not an error — it’s a silently worse default that only costs you later, when a typo in a property name or a wrong type passed to a function surfaces as a runtime error instead of an edit-time one.
# Bad — runs fine, but the editor can't catch a typo'd property name
# or a wrong argument type until this code actually executes
var speed = 300.0
var target = null
func move_toward_target(delta):
if target == null:
return
position += (target.position - position).normalized() * speed * delta
# Good — typed. The editor flags a bad assignment or a nonexistent
# property before you ever press play.
var speed: float = 300.0
var target: Node2D = null
func move_toward_target(delta: float) -> void:
if target == null:
return
position += (target.position - position).normalized() * speed * delta
The official static typing docs note that in most cases you can let the compiler infer the type with := instead of writing it out explicitly — var speed := 300.0 is exactly as strict as var speed: float = 300.0, just shorter. CLAUDE.md should say which convention the project prefers (many teams use := for local variables and explicit types for function signatures and exported properties), because otherwise Claude Code mixes both inconsistently across a session.
The TileMap → TileMapLayer Migration Trap
TileMap isn’t removed — it still compiles, still runs, and still shows up correctly in the editor. That’s exactly why an agent has no signal to avoid it: nothing fails, nothing warns, unless the CLAUDE.md says so directly.
## Tilemaps (do not use the deprecated TileMap node)
- All new tile-based levels use TileMapLayer — one node per visual/collision layer, sharing a single TileSet resource
- TileMap is deprecated as of Godot 4.3 and appears in this codebase only in [list any legacy scenes not yet migrated]
- The editor's built-in migration tool (right-click a TileMap node → convert to TileMapLayers) is the correct path for legacy scenes — do not hand-roll a replacement
Before Godot 4.3, TileMap supported multiple layers within a single node. TileMapLayer inverts that: each layer is now its own node in the scene tree, which — per the engine’s own release notes — means less inspector clutter and a simpler API, but it also means “add a tilemap layer” now means “add a node,” not “add a layer inside an existing node.” An agent that hasn’t been told this explicitly will often reach for the older, single-node pattern, because that’s what the overwhelming majority of published Godot tutorials — written before August 2024 — still show.
Autoload Singletons: Where Global State Actually Belongs
Godot doesn’t have a built-in dependency injection system, and the natural failure mode without one is global state scattered across get_node("/root/GameManager") calls sprinkled through unrelated scripts.
# res://autoload/game_manager.gd — registered as an Autoload named "GameManager"
extends Node
signal score_changed(new_score: int)
var score: int = 0:
set(value):
score = value
score_changed.emit(score)
func reset_run() -> void:
score = 0
# Any script, anywhere in the tree — GameManager is globally accessible
# once registered as an autoload, no get_node() lookup needed
func _on_enemy_defeated(points: int) -> void:
GameManager.score += points
The rule worth stating explicitly in CLAUDE.md: autoloads are for genuinely global, single-instance state — game state, save/load, audio bus management, scene transitions. A per-level or per-entity concern (an individual enemy’s health, a specific door’s open/closed state) belongs on the node it describes, not promoted to an autoload because it was easier to reach from everywhere. An agent without this boundary stated will sometimes solve “component A needs data from component B” by routing everything through a growing autoload, which turns a scene-tree-shaped problem into a tangle of one giant singleton.
Hook-Driven Verification for Godot Projects
gdtoolkit provides gdformat and gdlint — a formatter and static linter for GDScript that run outside the editor, which makes them a natural fit for a PostToolUse hook.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "gdlint \"$CLAUDE_FILE_PATH\" 2>&1 | head -30"
}
]
}
]
}
}
# Install (Godot 4.x)
pip3 install "gdtoolkit==4.*"
# CI: reject unformatted or non-compliant code
gdformat --check res://
gdlint res://
gdlint catches missing type annotations and naming-convention violations — the same rules stated in the CLAUDE.md naming section above — as a static check, not just a written policy Claude Code might forget mid-session. For scripts that need an execution-level check rather than a style check, godot --headless --check-only -s res://path/to/script.gd parses the file for errors without running it, which is fast enough to run per-edit and catches things gdlint won’t (an undeclared variable, a call to a method that doesn’t exist on the referenced type when static typing makes that inferrable).
AGENTS.md Compatible Version
# AGENTS.md — Godot Project
## Commands
- editor: `godot --editor`
- headless import (run first on a fresh checkout): `godot --headless --import`
- syntax check a script: `godot --headless --check-only -s res://path/to/script.gd`
- lint: `gdlint res://`
- format check: `gdformat --check res://`
## Critical Rules
1. Static typing is required on all new code — explicit types or `:=` inference, never a bare untyped `var`
2. TileMapLayer for all new tilemap work — TileMap has been deprecated since Godot 4.3
3. snake_case for files/functions/variables/signals, PascalCase for classes/nodes, CONSTANT_CASE for constants
4. Nodes communicate up/sideways only through signals — never a direct parent/sibling method call
5. @onready var references set once at the top of a script, not repeated get_node()/$Path calls inside _process()
6. Autoload singletons are for genuinely global state only (game state, audio, save/load) — not a shortcut for cross-node data access
7. Run `godot --headless --import` before any other headless command on a fresh checkout — the .godot/ import cache isn't committed
## Testing
- GUT or GdUnit4 (state which one — do not mix both in the same project)
- Pure logic: unit tests. Scene-dependent behavior: the test framework's scene-tree test scenes
Common AI + Godot Mistakes to Watch For
Untyped var declarations that compile fine and only fail at runtime, weeks after being written, when a typo’d property name or wrong type finally gets exercised by a specific game state. gdlint catches this at edit time if it’s wired into a hook; an unenforced style-guide mention in CLAUDE.md alone gets forgotten mid-session.
TileMap instead of TileMapLayer on any new tile-based scene, because it’s still what most training data shows and it doesn’t error. Worth a periodic check of whether new scenes are using the current node.
Direct get_node("/root/GameManager") or $"../../SiblingNode" calls scattered through unrelated scripts instead of either an @onready reference (for stable child/sibling nodes) or a signal (for anything that should stay decoupled). Both compile and both work, which is exactly why nothing flags the pattern as it accumulates.
A signal connected in code and also wired in the editor’s Signal panel for the same event, causing a handler to fire twice. This is easy to introduce when an agent adds a .connect() call without checking whether the scene file already has a static connection for the same signal.
Growing an autoload into a god-object because it was the easiest place to reach from anywhere in the scene tree, when the state in question was actually scoped to one entity or one level and should have lived on that node instead.
The gallery’s own Godot 4 GDScript Game Development rule set is a solid starting point for the conventions above — strict typing, @onready over direct references, signal-based coupling — written as a .cursorrules file rather than CLAUDE.md, which is worth knowing if you’re porting rather than starting from this template directly. Browse all game development rule sets in the gallery for more examples, or our full CLAUDE.md guide collection for other framework- and engine-specific templates.
FAQ
Does GDScript’s optional typing mean Claude Code will write typed code by default?
No — untyped GDScript compiles and runs identically to typed GDScript, so there’s no compiler signal pushing an agent toward var speed: float = 300.0 over var speed = 300.0. It has to be stated as a required convention in CLAUDE.md, ideally backed by a gdlint hook that actually enforces it.
Is TileMap actually removed in Godot 4.3+?
No, it’s deprecated but still functional — it still compiles, runs, and edits normally. The editor provides a built-in tool to convert a TileMap node into TileMapLayer nodes, and CLAUDE.md should state that new tile-based scenes use TileMapLayer explicitly, since nothing else will stop an agent from reaching for the older, still-working node.
Should global game state always be an autoload singleton? Only state that’s genuinely global and single-instance — game state, save/load, audio bus management, scene transitions. Per-entity or per-level state belongs on the node it describes; routing it through an autoload because it’s reachable from anywhere tends to grow into a single tangled god-object over time.
What’s the difference between using @onready references and calling get_node() repeatedly?
@onready var sprite: Sprite2D = $Sprite2D resolves the reference once, when the node enters the tree, and caches it as a typed variable. Calling get_node() or using a $Path expression repeatedly inside _process() re-resolves the path every frame, which is both slower and a sign the reference should have been cached at the top of the script instead.
Can Claude Code use gdlint and gdformat directly, or do they need to be installed separately?
They’re a separate Python package (gdtoolkit, pip3 install "gdtoolkit==4.*" for Godot 4 projects) — not bundled with the Godot engine itself. Once installed, they’re regular CLI tools that a PostToolUse hook can call after every script edit, the same way a JavaScript project might wire up ESLint.