Extending the Godot Editor with EditorPlugin (@tool, Custom Docks, Inspector)

Created: 2026-02-08Last updated: 2026-07-07

How to add custom docks, toolbar buttons, and inspector plugins using Godot's @tool annotation and the EditorPlugin class. Covers plugin.cfg setup through best practices.

Overview

Tested on: Godot 4.3+

The Godot editor is highly extensible: with the EditorPlugin class you can add custom docks and inspector UI. But if you don't understand how the @tool annotation behaves and how a plugin's lifecycle works, you'll end up crashing the editor or getting unexpected behavior.

This article walks through the basic structure of a plugin all the way to implementing custom docks and inspector plugins, with steps you can use in real projects.

Attaching your own panel to the Godot editor to extend it

What You'll Learn

  • How @tool runs your script inside the editor, and what to watch out for
  • The basic plugin structure of plugin.cfg plus EditorPlugin
  • How to implement custom docks, toolbar buttons, and inspector extensions
  • Best practices for not crashing the editor

Sponsored

Basics of the @tool Annotation

The first thing to understand about editor extensions in Godot is the @tool annotation. It shines in situations like "I want to draw the Attack Range as a circle in the editor so I can check it."

Adding @tool at the top of a script makes that script run inside the editor too. The important part is separating editor behavior from runtime behavior.

@tool
extends Node2D

func _process(delta):
    if Engine.is_editor_hint():
        # Runs only inside the editor
        queue_redraw()
    else:
        # Runs during gameplay
        move_character(delta)
A normal script doesn't run inside the editor, but adding @tool makes it run there too, letting you draw guide circles in the viewport

Important: without a branch on Engine.is_editor_hint(), your game logic will run inside the editor as well.

Next, let's look at a practical example combining a @tool setter with _draw(). Changing radius in the Inspector updates the circle in the editor in real time.

Main uses for @tool:

UseDescription
Custom drawingShow guide lines and previews in the editor with _draw()
Live property updatesRefresh a preview the moment an @export value changes
EditorPluginRequired on the plugin's main script
@tool
extends Sprite2D

@export var radius: float = 100.0:
    set(value):
        radius = value
        queue_redraw()  # Redraw whenever the value changes

func _draw():
    if Engine.is_editor_hint():
        draw_circle(Vector2.ZERO, radius, Color(0, 1, 0, 0.3))
Sponsored
A practical @tool example. Dragging the Radius slider in the Inspector updates the attack-range circle in the editor instantly

Structuring plugin.cfg and EditorPlugin

With the @tool basics down, let's move on to building an actual plugin. Plugins require a specific directory layout. Godot detects that structure automatically and lists your plugin in the plugin settings screen.

Directory structure

addons/
└── my_plugin/
    ├── plugin.cfg          # Plugin config file
    ├── my_plugin.gd        # EditorPlugin script
    └── dock/
        └── my_dock.tscn    # Custom dock scene (optional)
Plugin structure diagram: place plugin.cfg and plugin.gd under addons/my_plugin/, and enabling the plugin embeds a custom dock into the Godot editor

plugin.cfg

[plugin]

name="My Plugin"
description="A plugin that adds a custom dock"
author="Your Name"
version="1.0.0"
script="my_plugin.gd"

EditorPlugin basics

@tool
extends EditorPlugin

func _enter_tree():
    # Called when the plugin is enabled
    print("Plugin activated")

func _exit_tree():
    # Called when the plugin is disabled
    # Always remove the UI elements you added here
    print("Plugin deactivated")

How to enable a plugin:

  1. Go to Project → Project Settings → Plugins
  2. Find your plugin in the list and tick its checkbox

Adding a Custom Dock

Once the skeleton of your plugin exists, let's actually add something to the editor. The most common use is adding a custom panel (dock). You could build a debug tool that lists every object in the scene, or a texture preview UI.

The code below adds a custom dock to the upper-left panel.

@tool
extends EditorPlugin

var dock: Control

func _enter_tree():
    dock = preload("res://addons/my_plugin/dock/my_dock.tscn").instantiate()
    add_control_to_dock(DOCK_SLOT_LEFT_UL, dock)

func _exit_tree():
    if dock:
        remove_control_from_docks(dock)
        dock.queue_free()

Dock slots

Slot constantPosition
DOCK_SLOT_LEFT_ULUpper-left panel
DOCK_SLOT_LEFT_BLLower-left panel
DOCK_SLOT_RIGHT_ULUpper-right panel
DOCK_SLOT_RIGHT_BLLower-right panel

Building a dock scene

The contents of a dock can be built as an ordinary scene.

Use a Control node as the root and lay out whatever UI elements you need.

# dock/my_dock.gd
@tool
extends VBoxContainer

@onready var label = $StatusLabel
@onready var button = $RunButton

func _ready():
    button.pressed.connect(_on_run_pressed)

func _on_run_pressed():
    label.text = "Running..."
    # Plugin-specific work goes here
    # EditorInterface is directly accessible inside an EditorPlugin script
    # From a dock script, going through the EditorPlugin is safer
    label.text = "Done"
Sponsored

Adding a Toolbar Button

A dock is a panel that's always visible. If all you need is "run this one thing with a single click," a toolbar button is simpler. It's a good fit for something like a button that validates every node in the scene.

@tool
extends EditorPlugin

var button: Button

func _enter_tree():
    button = Button.new()
    button.text = "My Tool"
    button.pressed.connect(_on_button_pressed)
    add_control_to_container(CONTAINER_TOOLBAR, button)

func _exit_tree():
    if button:
        remove_control_from_container(CONTAINER_TOOLBAR, button)
        button.queue_free()

func _on_button_pressed():
    print("Toolbar button clicked")

Common container constants

ConstantLocation
CONTAINER_TOOLBARMain toolbar
CONTAINER_SPATIAL_EDITOR_MENU3D editor menu
CONTAINER_CANVAS_EDITOR_MENU2D editor menu
CONTAINER_INSPECTOR_BOTTOMBottom of the Inspector

Custom Inspector Plugins

After docks and toolbars, the other powerful extension point is the Inspector. Normally the Inspector just lists a node's properties automatically, but you can also add dedicated UI when a particular node type is selected. For example, you could add a widget that displays velocity values clearly whenever a CharacterBody2D is selected.

First register the inspector plugin from your EditorPlugin, then define the plugin class itself.

# my_plugin.gd
@tool
extends EditorPlugin

var inspector_plugin: MyInspectorPlugin

func _enter_tree():
    inspector_plugin = MyInspectorPlugin.new()
    add_inspector_plugin(inspector_plugin)

func _exit_tree():
    if inspector_plugin:
        remove_inspector_plugin(inspector_plugin)
# my_inspector_plugin.gd
@tool
class_name MyInspectorPlugin
extends EditorInspectorPlugin

func _can_handle(object: Object) -> bool:
    # Decide which objects this plugin handles
    return object is CharacterBody2D

func _parse_begin(object: Object):
    # Add UI elements at the top of the Inspector
    var label = Label.new()
    label.text = "== Character Info =="
    add_custom_control(label)

func _parse_property(object, type, name, hint_type, hint_string, usage_flags, wide):
    # Add a custom control for a specific property
    if name == "speed":
        var label = Label.new()
        label.text = "Speed: %s" % str(object.get(name))
        add_custom_control(label)
        return true  # Override the default display
    return false
Sponsored

Best Practices

Now that you've seen the main features, let's collect the pitfalls that trip people up in plugin development. There are considerations here that don't apply to regular GDScript work, and neglecting resource management in particular will crash the editor or leak memory.

ItemRecommendation
Null checksAlways null-check before removing things in _exit_tree()
Editor/runtime splitBranch editor-only logic on Engine.is_editor_hint()
Resource managementCall queue_free() on every UI element you added, in _exit_tree()
Use preloadLoad scenes and resources ahead of time with preload()
Error handlingNotify the user of problems with push_warning()

A common mistake:

# Bad: forgetting to release UI elements in _exit_tree()
func _exit_tree():
    pass  # Causes memory leaks and editor crashes

# Good: always release them
func _exit_tree():
    if dock:
        remove_control_from_docks(dock)
        dock.queue_free()
    if inspector_plugin:
        remove_inspector_plugin(inspector_plugin)

Summary

  • Adding @tool to the top of a script makes it run inside the editor
  • Use Engine.is_editor_hint() to branch editor-only logic
  • Build a plugin by implementing _enter_tree() / _exit_tree() on an EditorPlugin class
  • Add a custom dock to the left or right panels with add_control_to_dock()
  • An inspector plugin extends the UI for specific nodes by inheriting EditorInspectorPlugin
  • Always release everything you added in _exit_tree() to prevent memory leaks

Further Reading