project.godot give you the whole
EditorInterface: the resource filesystem, forced reimports, scene saving, autoload
registration, and the built-in editor plugins that do bakes.
If you are automating Summer and have hit “that API is not exposed to scripting”, this
page is the way through. For everything that works without editor context, see
Headless Scripting first — it is cheaper and has no editor to
keep alive.
The minimal plugin
Two files. Copy both exactly. These are the files used to verify everything on this page.addons/summer_demo/plugin.cfg:
addons/summer_demo/plugin.gd:
script= value in plugin.cfg is resolved relative to the plugin.cfg directory,
so script="plugin.gd" means addons/summer_demo/plugin.gd.
Enabling a plugin
Writing the files does nothing on its own. There are three ways to enable one, and they are not interchangeable.Route A — write editor_plugins/enabled into project.godot
Add this section to project.godot:
_enter_tree() fires, and EditorInterface
and get_resource_filesystem() are live.
Route B — the EnableEditorPlugin operation
This is the path Summer’s own AI takes when you ask it to enable a plugin. It takes one
argument, pluginPath. A bare name normalises — summer_demo becomes
res://addons/summer_demo/plugin.cfg — and a full res:// path passes through
unchanged.
It requires a live editor. Without one it returns EditorNode not available (engine not in editor mode?). It is idempotent: an already-enabled plugin returns
{ok: true, alreadyEnabled: true} rather than an error.
Pre-checks run synchronously and return structured errors you can act on:
Route C — EditorInterface.set_plugin_enabled() from GDScript
Called from inside a plugin that is already loaded, this enables a further plugin with
_enable_plugin() firing. Measured: the second plugin’s _enter_tree(),
_enable_plugin() and its add_autoload_singleton() all ran, and project.godot on
disk gained the second entry.
_enable_plugin()
semantics and want the result in the same script where you can check it, this is the
route.
Verification is not optional
None of the enable routes reliably tell you the plugin actually loaded. Route A defers to the next startup, Route B acknowledges before the work runs, and a broken plugin produces an editor that behaves exactly as if nothing was asked for. After enabling, verify with one of these:- Dispatch the sibling operation
IsEditorPluginEnabledwith the samepluginPath. It reports the editor’s live state and returns{ok: true, enabled: <bool>, addonPath: ...}. - Read
project.godotback and confirm your path is ineditor_plugins/enabled. - From GDScript inside the editor, call
EditorInterface.is_plugin_enabled(path).
_enter_tree() also gives you a second,
independent signal in the editor console.
What EditorInterface unlocks
Everything below was measured against the shipped binary.
The bound methods on
EditorFileSystem are scan, reimport_files, update_file,
get_filesystem, get_file_type, is_scanning and get_scanning_progress. Anything
else you have seen in the C++ is not callable from GDScript.
EditorInterface itself exposes 70 bound methods, including save_scene,
save_scene_as, save_all_scenes, open_scene_from_path, reload_scene_from_path,
close_scene, edit_node, edit_resource, set_plugin_enabled and
get_editor_undo_redo.
Recipe: registering a file you just wrote
A file written straight to disk is invisible to the engine until the import database knows about it. The obvious version of this — callscan() and wait — does not reliably
work, because a scan() issued while another scan is running is dropped without a word.
This is the version that works:
update_file() before reimport_files() is the part people leave out. Reimporting a
path the filesystem has never registered fails with Can't find file '...' during file reimport.
Outside the editor there is a separate route for the same problem: run the binary with
--import. See Headless Scripting.
Recipe: baking occluders from code
OccluderInstance3D.bake_single_node() is not exposed to scripting, but the editor
plugin that implements the Bake button is reachable, and calling it works — verified
headless, with no GPU, producing a real 8-vertex ArrayOccluder3D from a cube.
Two things will stall or break this if you skip them.
The traversal that finds a built-in editor plugin, verbatim:
@tool EditorPlugin:
Two frame awaits before the traversal, and one more between
edit_node() and _bake().
The built-in plugins are not in the tree during _enter_tree(), which is why the work is
call_deferred(). Whether the await between edit_node() and _bake() can be dropped
was not tested — keep it.The same traversal finds LightmapGIEditorPlugin, MeshInstance3DEditorPlugin,
MultiMeshEditorPlugin and Node3DEditorPlugin, one match each. There is no
NavigationMeshEditorPlugin under that class name in 4.6.1 — searching for it returns
nothing.This was measured from inside a @tool EditorPlugin under --headless --editor. It was
not tested from a -s script or in the GUI editor.Traps
A broken plugin fails silently, it does not crash
A broken plugin fails silently, it does not crash
Measured across a runtime error, a parse error and a type mismatch: the editor booted
and carried on every time. That is good for stability and bad for feedback.The real failure mode is a silent no-op — the enable returned
ok: true, the editor
looks healthy, and none of your plugin’s code ever ran. If a plugin appears to do
nothing, do not assume it is not enabled and do not re-enable it. Check the editor
console for the load error, then verify with IsEditorPluginEnabled.Engine.has_singleton is not a liveness check
Engine.has_singleton is not a liveness check
Engine.has_singleton("EditorInterface") returns true even outside the editor,
while Engine.get_singleton("EditorInterface") fails with Can't retrieve singleton 'EditorInterface' outside of editor.Guard on Engine.is_editor_hint(), never on has_singleton.scan() while scanning is a silent no-op
scan() while scanning is a silent no-op
A second
scan() issued while one is in flight is dropped with no error and no return
value to check. Code that scans and then waits for resources_reimported will wait
forever.Guard with is_scanning() and always call update_file() + reimport_files()
unconditionally, as in the recipe above.Plugins do not participate in Summer's write lease
Plugins do not participate in Summer's write lease
Summer serialises writes by
res:// path across AI tool operations, editor autosave,
asset import and Git operations. User plugins are outside that mechanism.A plugin that writes files on a timer can race an AI file write to the same path, and
neither side will know. Prefer writing on an explicit user action — a button, a menu
item, a signal you control — over background timers, and keep plugin writes confined to
paths the AI is not working in.The headless editor
Summer --headless --editor --path <project> boots the editor with no window, loads
enabled plugins, and exits 0 on --quit. Engine.is_editor_hint() is true and
EditorInterface resolves. This is how plugin behaviour is verified without a person
clicking anything.
There is also a route with no plugin at all:
tool.gd as extends SceneTree, you get a live EditorNode and full
EditorInterface — no plugin, no plugin.cfg, no project.godot edit. EditorScript
becomes instantiable and EditorInterface.save_scene() persists nodes the script added.
The script’s loop replaces the editor’s SceneTree but is still a SceneTree, so
EditorNode is constructed as usual.
For AI agents
To author and enable a plugin, in order. Step 4 is not optional.1
Confirm the project has been opened at least once
Check that
.godot/ exists in the project root. If it does not, stop — writing
[editor_plugins] into a never-opened project aborts the editor at startup. Have the
project opened once first.2
Write both files
addons/<name>/plugin.cfg and the script it names. The script must start with @tool
and extend EditorPlugin. Use the templates above verbatim.3
Read project.godot before enabling
Collect every path already in
editor_plugins/enabled. The array is replaced, not
merged — anything you omit is silently disabled. Your new path goes in alongside them,
not instead of them.4
Enable, then verify
Enable through whichever route fits (
EnableEditorPlugin against a live editor;
otherwise project.godot plus a restart).Then verify: dispatch IsEditorPluginEnabled, or read project.godot back. An
ok: true from the enable operation only means the request was queued. If the plugin
failed to load, nothing will tell you, and the symptom arrives later as an unrelated
parse error on an autoload name.5
Put load-time work in _enter_tree
_enable_plugin() does not fire on a startup enable, so autoloads registered there
never register. If you need _enable_plugin() semantics, enable from inside an
already-loaded plugin with EditorInterface.set_plugin_enabled().6
Register files you write
update_file() then reimport_files(), unconditionally, guarding only the scan()
with is_scanning(). A dropped scan() reports nothing.ok: truedoes not mean loaded. Verify every enable.- The enabled list is replaced, not merged. Read it first or you will disable the user’s plugins.
--headless --editorrewrites~/.summer/api-portand~/.summer/api-token, and breaks the connection to a running editor. Use plain--headlessunless you truly need editor context.- A broken plugin does not crash anything. It produces an editor that quietly ignores you.
Next steps
Extending Summer
The three extension paths and which one to pick
Headless scripting
Everything that works with no editor at all: baking, importing, exporting
GDExtension
Native libraries, when GDScript is not enough
AI operations
What Summer’s AI can do inside your project
Need help or have questions? Reach out to our founders at founders@summerengine.com or join our community on Discord for fast responses.

