bulk_delete_assets
Batch-delete Unreal Engine assets with per-item refusal gates for read-only roots, protected paths, open worlds, and zero referencers. Dry run defaults to true for safety.
Instructions
GUARDED batch asset deletion. dry_run DEFAULTS TO TRUE (deliberate: it is the most destructive command in the plugin, bulk_delete_assets(["/Game/VendorPack"], recursive=true) would be a valid unconfirmed call, and agent allowlists may run mutating tools with no prompt). Four independent gates REFUSE per item instead of skipping silently: readonly_roots (default: the project's Read Only Content Roots), protect_paths (wildcards -- the only gate that saves a zero-referencer asset), the currently-open editor world, and the OPT-IN require_zero_referencers gate whose referencer count EXCLUDES referencers that are themselves in this batch (so a self-contained dead subgraph can still be deleted whole). Deletion is a single ObjectTools::ForceDeleteObjects over the whole eligible batch -- resolving each object here via StaticFindObject/StaticLoadObject rather than through UEditorAssetSubsystem::DeleteAsset, whose UE::EditorAssetUtils::LoadAssetFromPath/EnsureAssetsLoaded preamble is a known crash site ("Array index out of bounds", deterministic on corrupted leftovers). deleted_count counts ONLY items that passed BOTH post-delete probes -- the package file is gone (FPackageName::DoesPackageExist) AND the object no longer resolves at its original path (StaticFindObject) -- never the API's own return value. Both are required because for a never-saved, registry-visible asset the disk probe is vacuously true (it was never on disk), so residency is the only real evidence; each failure names which probe still sees the item.
Returns: { requested_count, expanded_count, recursive, dry_run, readonly_roots: [string], protect_paths: [string], require_zero_referencers: bool, dependency_type: string, expanded_folders: [{folder, asset_count}], eligible_count, would_delete: [{path, package_name, external_referencer_count, external_referencers: [string], on_disk}], deleted: [{path, package_name, deleted, still_loaded, error?}], deleted_count, failed_count, force_delete_reported_count, refused_readonly_root: [{path, matched_root}], refused_protected: [{path, matched_pattern}], refused_referenced: [{path, external_referencer_count, external_referencers: [string]}], refused_open_world: [{path, reason}], refused_count, results: [{path, success, error}], note, verification: {...} }
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| dry_run | No | Default TRUE. Nothing is deleted; would_delete reports what WOULD go, with each item's gate verdict. Pass false to actually delete. | |
| recursive | No | If true, expand folder paths to all contained assets. Default false. | |
| asset_paths | Yes | List of asset paths (or folder paths when recursive is true). Required, non-empty. | |
| protect_paths | No | Wildcard patterns (FString::MatchesWildcard, case-insensitive, `*` spans '/') matched against both the package name and the object path. Default [] (gate off). Matches are REFUSED (refused_protected). | |
| readonly_roots | No | Content roots that may never be deleted from. Default: the project's Read Only Content Roots; pass [] to disable the gate entirely. Matches are REFUSED (refused_readonly_root), never silently skipped. (Read via mcp_common::ParseReadonlyRoots, so the type is declared here.) | |
| dependency_type | No | Referencer filter for the count above: "hard"|"soft"|"all" (case-insensitive), default "all". | |
| require_zero_referencers | No | Default FALSE. When true, an asset with at least one referencer OUTSIDE this batch is refused. The per-item external_referencer_count is reported either way. |