A Godot 4 editor plugin for browsing and selectively deleting your project's user:// directory without leaving the editor.
Requires Godot 4.3+ · MIT License
- Install
- Usage
- Features
- API Reference
- Working with Save Files
- Handling Edge Cases
- Running Tests
- What's in user://
- Contributing
- Credits
- Open your project → AssetLib → search "Manage User Data"
- Download → Install
- Project → Project Settings → Plugins → enable Manage User Data
- Copy
addons/manage_user_data/into your project:your_project/ └── addons/ └── manage_user_data/ ├── plugin.cfg └── plugin.gd - Project → Project Settings → Plugins → enable Manage User Data
Click the User Data button in the editor toolbar to open the dialog.
| Action | How |
|---|---|
| Delete everything | Leave all items checked → Delete Selected |
| Delete specific files | Uncheck Select All → tick what you want → Delete Selected |
| Find a file | Type in the Search bar |
| Filter by type | Use the All Types dropdown (Files, Folders, .json, .cache) |
| Clear filters | Click × next to the dropdown |
| Refresh the list | Click the ⟳ icon |
| Open in file manager | Click Open Folder |
| Copy engine logs | Click Copy Logs (de-duplicates automatically) |
The status bar shows a live count and total size of selected items before you confirm. Deletion is permanent and cannot be undone.
- File Tree — Full recursive view of
user://with per-item checkboxes - Real-time Search — Instantly filters the tree as you type; parent folders auto-expand
- Type Filters — Narrow to Files Only, Folders Only,
.json, or.cache - Bulk Selection — Select All / Deselect All with indeterminate state support
- File Sizes — Inline sizes on every entry; status bar totals the selection
- Open in OS — Launch
user://in your native file manager - Copy Logs — One-click copy of de-duplicated
godot.logto clipboard - Refresh — Rescan
user://without reopening the plugin - Version Check — Warns on Godot versions below 4.3
- Error Resilience — Gracefully handles unreadable files, locked directories, and permission errors
The plugin extends EditorPlugin and provides utility methods for managing user:// contents. While primarily a UI tool, the following public methods are available.
Opens the main plugin dialog. Scans user:// and builds the file tree.
# Typically called via toolbar button, but can be invoked programmatically:
var plugin = EditorInterface.get_editor_plugin("Manage User Data")
plugin.show_confirmation_dialog()Recursively scans a directory and populates the Tree control with items. Each item stores its full path in metadata slot 1.
- Handles unreadable directories with
push_warning() - Displays file sizes inline (or "unreadable" for locked files)
- Assigns icons based on file extension
Deletes all checked items in the tree. Sorts paths deepest-first to avoid parent-before-child conflicts.
Returns: {"deleted": int, "failed": Array[String]}
var result = delete_selected_items()
print("Deleted %d items" % result.deleted)
if not result.failed.is_empty():
push_warning("Failed to delete: %s" % str(result.failed))Recursively deletes all files and subdirectories inside path without removing the directory itself. Logs warnings for items that fail to delete.
Returns the total byte size of all files under path, recursively. Skips unreadable files.
Applies the current search text and type filters to the tree. Matching parents auto-expand.
Recursively filters tree items. Returns true if the item or any descendant matches.
Filter type IDs:
| ID | Filter |
|---|---|
| 1 | Files Only |
| 2 | Folders Only |
| 3 | .json files |
| 4 | .cache files |
Formats byte counts for display.
format_file_size(0) # "0 B"
format_file_size(1024) # "1.00 KB"
format_file_size(1572864) # "1.50 MB"Returns a human-readable type label based on file extension.
get_file_type_label("save.json") # "JSON Data"
get_file_type_label("icon.png") # "PNG Image"
get_file_type_label("player.gd") # "GDScript"
get_file_type_label("Makefile") # "File"Returns an appropriate editor theme icon for the file type.
Supported extensions: .json, .cfg, .ini, .toml, .png, .jpg, .jpeg, .webp, .bmp, .svg, .wav, .ogg, .mp3, .tscn, .tres, .gd, .cache, .save, .dat
Recursively applies a checked/unchecked state to all children of the given tree item.
Collects full paths of all checked items into the result array.
collect_checked_items_with_stats(item, result, total_size_ref, file_count_ref, folder_count_ref) → void
Like collect_checked_items, but also accumulates total size, file count, and folder count via reference arrays.
Updates the status bar with the current selection count, size, and warning text.
This plugin helps you manage any files your game writes to user://. Here are common patterns used by Godot games and how this plugin helps during development.
# Saving
var save_data := {"version": 2, "player_name": "Hero", "level": 5}
var file := FileAccess.open("user://save.json", FileAccess.WRITE)
file.store_string(JSON.stringify(save_data, "\t"))
file.close()
# Loading with error handling
var file := FileAccess.open("user://save.json", FileAccess.READ)
if file == null:
push_error("Save file not found")
return
var json := JSON.new()
var err := json.parse(file.get_as_text())
file.close()
if err != OK:
push_error("Corrupted save file: %s" % json.get_error_message())
return
var data: Dictionary = json.data# Saving
var cfg := ConfigFile.new()
cfg.set_value("meta", "version", 2)
cfg.set_value("player", "name", "Hero")
cfg.set_value("player", "score", 1500)
cfg.save("user://settings.cfg")
# Loading
var cfg := ConfigFile.new()
if cfg.load("user://settings.cfg") != OK:
push_error("Failed to load settings")
return
var version := cfg.get_value("meta", "version", 1)
var name := cfg.get_value("player", "name", "Unknown")# Saving
var file := FileAccess.open("user://world.dat", FileAccess.WRITE)
file.store_32(0x53415645) # Magic number "SAVE"
file.store_32(2) # Version
file.store_float(player.position.x)
file.store_float(player.position.y)
file.store_pascal_string(player.name)
file.close()
# Loading with validation
var file := FileAccess.open("user://world.dat", FileAccess.READ)
if file == null:
return
var magic := file.get_32()
if magic != 0x53415645:
push_error("Not a valid save file")
file.close()
return
var version := file.get_32()
if version > 2:
push_error("Save file from newer game version (v%d)" % version)
file.close()
returnAlways validate before trusting file contents:
func load_save_safe(path: String) -> Variant:
if not FileAccess.file_exists(path):
return null
var file := FileAccess.open(path, FileAccess.READ)
if file == null:
push_warning("Cannot open: %s (error %d)" % [path, FileAccess.get_open_error()])
return null
var content := file.get_as_text()
file.close()
if content.strip_edges().is_empty():
push_warning("Empty save file: %s" % path)
return null
var json := JSON.new()
if json.parse(content) != OK:
push_error("Corrupted save: %s — %s" % [path, json.get_error_message()])
return null
return json.dataVersion your save format and handle migrations:
const CURRENT_SAVE_VERSION := 3
func load_and_migrate(path: String) -> Dictionary:
var data := load_save_safe(path)
if data == null or not data is Dictionary:
return {}
var version := int(data.get("version", 1))
if version > CURRENT_SAVE_VERSION:
push_error("Save from future version %d (current: %d)" % [version, CURRENT_SAVE_VERSION])
return {}
# Apply migrations sequentially
if version < 2:
# v1 → v2: rename "name" to "player_name"
if data.has("name"):
data["player_name"] = data["name"]
data.erase("name")
data["version"] = 2
if version < 3:
# v2 → v3: add inventory array
if not data.has("inventory"):
data["inventory"] = []
data["version"] = 3
return datafunc migrate_cfg(path: String) -> ConfigFile:
var cfg := ConfigFile.new()
if cfg.load(path) != OK:
return cfg
var version := int(cfg.get_value("meta", "version", 1))
if version < 2:
# Rename section key
var old_val = cfg.get_value("player", "name", "")
cfg.set_value("player", "display_name", old_val)
cfg.erase_section_key("player", "name")
cfg.set_value("meta", "version", 2)
cfg.save(path)
return cfgThe test suite validates the plugin's core logic — formatting, filtering, file operations, and migration patterns.
- Godot 4.3+ in your
PATH(or setGODOT_BINenvironment variable)
bash tests/run_all.shgodot --headless --path . --script tests/test_format_file_size.gd
godot --headless --path . --script tests/test_file_type_label.gd
godot --headless --path . --script tests/test_filter_logic.gd
godot --headless --path . --script tests/test_file_operations.gd| Test File | What It Covers |
|---|---|
test_format_file_size.gd |
Byte/KB/MB formatting, boundary values |
test_file_type_label.gd |
Extension → label mapping, case insensitivity, unknown extensions |
test_filter_logic.gd |
Search matching, type filters, combined filters, depth sorting, propagation |
test_file_operations.gd |
JSON/CFG/binary save+load, corrupted files, empty files, recursive delete, version migration, folder size calculation |
Add to your GitHub Actions workflow:
- name: Run plugin tests
uses: chickensoft-games/setup-godot@v2
with:
version: 4.4.1
use-dotnet: false
- run: godot --headless --path . --script tests/test_format_file_size.gd
- run: godot --headless --path . --script tests/test_file_type_label.gd
- run: godot --headless --path . --script tests/test_filter_logic.gd
- run: godot --headless --path . --script tests/test_file_operations.gdGodot writes to user:// when your game uses paths like "user://save.json". Common files to clean during development:
| Extension | Source |
|---|---|
.cfg / .json |
Save data, settings |
.log |
Engine/game log files |
.cache |
Engine/game caches |
.save / .dat |
Binary save files |
On-disk location:
| Platform | Path |
|---|---|
| Windows | %APPDATA%\Godot\app_userdata\<project>\ |
| macOS | ~/Library/Application Support/Godot/app_userdata/<project>/ |
| Linux | ~/.local/share/godot/app_userdata/<project>/ |
Contributions are welcome! Here's how to get started:
- Fork the repository
- Create a feature branch:
git checkout -b feature/my-change - Run the tests:
bash tests/run_all.sh - Submit a pull request
git clone https://github.com/Lost-Rabbit-Digital/manage_user_data_plugin.git
cd manage_user_data_plugin
# Open in Godot 4.3+ and enable the pluginMade by Lost Rabbit Digital · Discord
MIT — see LICENSE




