Craft CMS 5 plugin
Nothing is deleted until you have seen it
Craft has no bulk delete worth the name. So a section gets emptied a hundred elements at a time, or with a script nobody reviewed, and the drafts, the revisions and the relations are quietly left behind.
Nuke counts every one of them first — and tells you which surviving entries were pointing at what you are about to remove.
Free Lite edition · Craft CMS 5.3+ · PHP 8.2+ · no runtime dependencies
The four ways a bulk delete goes wrong
None of them announce themselves. Each one is discovered weeks later, by someone who was not in the room.
It took more than you thought
You aimed at a section and it matched a nested entry type you had forgotten about. There was no list, only a number, and the number went past before you could read it.
Something else was pointing at it
Forty entries elsewhere on the site held a relation to what you deleted. Their fields are empty now. Nothing errored, nothing logged, and the templates render as if those fields were never filled in.
The tree rearranged itself
Craft does not delete the children of a structure entry — it moves them up to where their parent was. Delete a top-level page and its whole subtree is promoted to the top level. That is documented in the source and nowhere else.
Nobody can say what happened
Six weeks later somebody asks where the 2019 case studies went. There is no record of what was aimed at, who fired it, when, or whether a backup was taken. There is only an absence.
Every one of these is knowable before the deletion, not after. That is the entire premise of the plugin.
The dry run
Read it before you mean it
The preview and the deletion share one code path and one resolved list of element IDs. What the preview counts is not an estimate of what will go — it is the list that goes.
Blast radius
Entries in Case Studies, of type Case Study, last updated before −2 years
- 9 descendants are not being deleted — Craft moves children up to where their parent was. They will end up at the top of the structure.
- 41 elements elsewhere on the site relate to what you are deleting. Those fields will empty out.
- Files stay on their volumes until Craft’s trash expires. Disk space is not reclaimed yet.
A sample of what matched
- Northwind rebrand — live
- Ridgeline distribution study — live
- Halcyon migration — disabled
- Fairweather rollout — expired
- Kestrel platform audit — live
…and 337 more.
These will lose a reference
- Services — Consulting — Entry, 6 relations
- Homepage — Entry, 4 relations
- Industries — Logistics — Entry, 3 relations
- About the team — Entry, 2 relations
- Careers — Entry, 1 relation
…and 36 more.
A backup is taken first. If it fails, nothing is deleted.
Not a screenshot — this is the real screen’s structure, rebuilt here so it stays legible on a phone and cannot drift out of date. The numbers are illustrative; everything else is exactly what Nuke shows you.
Aiming
Five scopes, then narrow it
A strike names what kind of content it is for, then which sources within it, then the filters that cut it down. Nothing is hidden behind a query language you have to learn.
Other element types can register themselves — a scope is one interface and a base class that already implements every filter below.
- Sources
- Sections, volumes, category groups, tag groups, user groups. Leave them all unticked to mean every one.
- Entry types
- A second axis within the chosen sections.
- Sites
- Narrows what matches. Deleting an element still removes it from all of its sites.
- Status
- Per element type, because asking a query for a status its type does not have returns nothing at all rather than erroring.
- Age
- Last updated or created before a date — absolute, or relative like “−2 years”.
- Search
- Craft’s own search syntax, against the search index.
- Element IDs
- When you already know exactly which ones. Every other filter is ignored.
Guardrails
Configured to hesitate
A plugin that deletes things in bulk should arrive cautious and make you ask for speed. Every one of these is in the free edition — nothing that keeps a deletion safe sits behind the paywall.
A dry run you can read
Every strike is previewed: elements, drafts, revisions, relations, files and bytes, a sample of what matched, and the surviving elements that reference it.
A typed confirmation
You type the handle of the thing you are deleting. The point is not that typing is hard — it is that it cannot happen by accident, by a double-click, or by a browser restoring a form.
A backup, taken first
Named after the run and linked from its record, so “restore it” is a path rather than a hunt. If the backup fails, nothing is deleted.
The trash, by default
Elements are soft-deleted and stay recoverable for as long as Craft’s trash window allows. Deleting permanently is a separate permission.
A ceiling per run
Ten thousand elements out of the box. A mis-scoped strike fails loudly at the preview instead of quietly at two hundred thousand rows.
Protected scopes
Name a section, volume or group in settings and no strike may ever target it, whatever the form says. Checked on the server, not in the browser.
Recorded twice
Every run writes a ledger row holding the target, the preview it was approved against and what actually happened — so the run screen can tell you whether the two agreed. It also writes storage/logs/nuke.log, which survives the database it was deleting from.
Four permissions, not one
Seeing what a deletion would do is a reasonable thing to grant widely. Doing it is not, and deleting permanently is not the same as deleting. So nuke:view deliberately does not imply nuke:strike.
The other half
Twenty-three things nothing else cleans up
Craft’s garbage collector is good at what it does and it does not do most of this. A sweep scans first, exactly like a strike, and every task keeps its own retention window.
Ordered so it works: content first, because deleting an element is what orphans the rows the database tasks then collect; files next; Craft’s own collector last, so one pass picks up everything the rest left behind.
Content
The things that create work for everything below. 5Trashed elements
Empties the trash on your own window rather than Craft’s single global one — and tells you what is in there first.
Abandoned drafts
Unsaved drafts and stale autosaves. Named drafts somebody saved on purpose are never touched.
Excess revisions
Craft applies maxRevisions only when an element is saved, so lowering it never catches up with content nobody edits. This applies it retroactively.
Unreferenced assets
Checks relations and reference tags. Reports by default, and will never do more than move an asset to the trash.
Empty asset folders
Off by default — an empty folder is usually somewhere an editor was told to upload to.
Database
Rows nothing points at any more. 12Dangling relations
Craft has no foreign key on relations.targetId, so anything hard-deleted outside its own delete path leaves rows pointing at nothing.
Stale search index rows
searchindex has no foreign key either. Rows whose element is gone or trashed have nothing to clean them up.
Draft change tracking
changedattributes and changedfields, written on every autosave — the fastest-growing pair of tables on an editorially busy site.
Element activity
The “currently editing” presence rows. Only recent ones are ever displayed.
Deprecation warnings
A development aid nothing prunes. Old entries make the ones that matter harder to see.
Failed queue jobs
Each carries its serialised payload. Waiting and in-progress jobs are never touched.
Stale login sessions
Long-lived “remember me” tokens, on a shorter window than Craft’s if your security review wants one.
Expired tokens
Preview, share and password-reset tokens — swept by expiry, not by age.
Announcements
The “what’s new” notices, stored once per user per notice.
Asset indexing leftovers
One scratch row per file walked. A cancelled index run leaves the lot behind.
Orphaned transform records
Transform index rows whose asset is gone. Rows for live assets are deliberately left alone.
Bulk operation records
Bookkeeping consumed within the request that wrote it.
Files
Under storage/, where the disk actually fills up. 5Old database backups
The single most common cause of a site filling its disk. Always keeps the newest few, however old they are.
Rotated logs
Including the ones Craft’s own rotation does not govern. Today’s log is never touched.
Temporary files
runtime/temp, abandoned uploads, and the local copies Craft makes of remote assets while working on them.
Compiled templates
Twig keeps a copy of every version of every template it has ever compiled, and removes none of them.
Config and Composer backups
Snapshots taken before one change each — not a history you can roll back through.
Craft
Last, on purpose. 1Garbage collection
Craft normally fires this at random on a small percentage of requests. Running it at the end of a sweep means one pass clears everything the other tasks orphaned.
From the console
Dry run is the default
Everywhere in the console, --dry-run is on unless you turn it off. A command that deletes by default is a command that deletes when somebody runs it to see what the arguments are.
The same preview, the same warnings, the same numbers — then an interactive confirmation, or --force for CI. Non-zero exit if anything failed, so a deploy step can stop.
$ php craft nuke/strike/fire entries --sources=caseStudies --updated-before="-2 years"
Entries in Case Studies, last updated before -2 years
elements 342
drafts 118
revisions 907
incoming relations 640
outgoing relations 211
descendants moved up 9
elements left referencing them 41
! 9 descendants are not being deleted — Craft moves children up to
where their parent was.
! 41 elements elsewhere on the site relate to what you are deleting.
Those fields will empty out.
Dry run — nothing was deleted. Pass --dry-run=0 to run it.
$ php craft nuke/sweep/due --dry-run=0
✓ trashed 1,204
✓ revisions 18,340
✓ searchIndex 2,911
✓ backups 11 (4.2 GB)
✓ compiled 14,882 (612 MB)
✓ gc Ran.
4.9 GB of files.
Two commands
No runtime dependencies beyond Craft itself. One database table, for the run ledger — there is not much for a plugin that deletes things to store.
composer require justinholtweb/craft-nuke
php craft plugin/install nuke
Requires Craft CMS 5.3+ and PHP 8.2+. Installs in the free Lite edition.
Pricing
Every guardrail is in the free edition. Pro buys automation, not safety — charging for the thing that stops you making a mistake would be a strange way to run a plugin like this.
Lite
Free
- Strikes against entries, categories, tags, assets and users
- The full blast-radius preview, including collateral
- Typed confirmation, pre-strike backup, soft delete, element ceiling
- Protected scopes and four separate permissions
- All 23 housekeeping sweepers, run by hand
- The run ledger and the nuke.log audit trail
- Every console command
Pro
$99
one-time, then $79/year for updates
- Everything in Lite
- Scheduled sweeps — daily, weekly or monthly, driven by cron or by control panel traffic
- Emailed reports after a sweep, and after a strike, to whoever needs to know
- Send only when something was actually removed, or only when something went wrong
Renewal is optional. Nuke keeps working on the version you bought; the yearly fee buys the next year of updates.
Frequently Asked Questions
Common questions about Nuke.
Craft’s element index lets you select a page of results and delete them, which is fine for twenty entries and unworkable for four thousand. More to the point, it tells you nothing: no count of the drafts and revisions going with them, no list of what elsewhere on the site was pointing at them, no backup, no record. Nuke is the same deletion with the part that lets you check it first.
They go with the element either way — a permanent delete cascades through the drafts and revisions foreign keys, and a soft delete soft-deletes them alongside. Nuke counts them so the numbers in the report are the truth, and optionally removes them outright so they are not left sitting in the trash.
A soft delete puts elements in Craft’s trash, where they stay recoverable until softDeleteDuration elapses — that is the real safety net and it costs nothing to keep, which is why it is the default. Beyond that, the database backup taken before the run is named after it and linked from its record, so restoring is a documented path rather than a hunt through storage/backups.
Elements that are not being deleted but hold a relation to something that is. Those are the fields that silently empty out, and they are the single most common way a bulk deletion turns out to have been a mistake. The preview lists them, worst first, with a link to each.
No — and that is worth knowing, because most people assume otherwise. Craft moves each child up to sit where its parent was, then deletes the now-childless node, so a deleted top-level page leaves its whole subtree promoted to the top level. The preview counts exactly how many elements that applies to and says so in words.
Scan first — that is what the button is for, and it is the same code path as the sweep with the deletion switched off. The defaults are conservative: unreferenced assets only report, empty folders and relations-to-trashed-elements are off, log files written today are never candidates, and the newest few backups are kept whatever their age.
Two sources of truth: the relations table, which is exact for anything in a relation field, and reference tags in element content, which catches rich text and CKEditor. What neither catches is an asset referenced only by a URL typed into raw HTML or hard-coded in a template — so that task reports by default, and the strongest thing it will ever do is move an asset to the trash, where a second and separately configured window has to elapse before it is really gone.
Name its handle under protected scopes in settings and no strike may ever target it, whatever the form posts. The check runs on the server, at preview time, so it also stops a console run and a queued job.
Yes. Every console command defaults to a dry run, takes --force to skip the interactive prompt, and exits non-zero if anything failed — so a deploy step can stop on it. Console strikes can also be switched off entirely if you want the control panel to be the only route.
Scheduled sweeps and emailed reports. That is all, and deliberately so: everything that makes a deletion safe to perform is in the free edition, because putting a safety feature behind a paywall is a strange way to run a plugin like this.
Both. A scope is one interface plus a base class that already implements the shared filters, and a sweeper is one method that handles the scan and the sweep together so the two cannot drift apart. Register either through an event. There is a guide in the docs.
No. No runtime dependencies beyond Craft, one database table for the run ledger, and nothing to configure before it works.
Look before you delete
Install the free edition, aim a strike at whatever has been bothering you, and read the preview. Nothing happens until you type the handle.