Everything your plugins left behind, layer by layer.
A static analyzer that reads a WordPress plugin's source and reports exactly what it leaves behind in your database.
Uninstalling a WordPress plugin deletes its files and almost nothing else. The options it registered keep autoloading on every request, its custom tables keep bloating your backups, and its cron events keep firing hooks whose code is long gone. Analysis of the plugin directory has found that more than 40% of plugins leave orphaned data behind, and only 28.6% remove it cleanly — a problem that has been documented since 2008 and remains unsolved.
The hard part was never deletion. It is attribution: knowing that a stray smk_last_sync_ts row belongs to a plugin you removed years ago, and is therefore safe to drop. Cleanup tools guess at this by matching name prefixes, and a wrong guess in a tool that deletes data will wreck a site.
Sediment takes a different route. It reads the source. A plugin's own code contains the literal calls that create its data — add_option(), dbDelta(), wp_schedule_event() — so parsing them statically turns attribution from a guess into something you can point at: includes/setup.php:412.
It reads code but never runs it. There is no WordPress install, no database, and nothing executed; a scan works anywhere PHP 8.3 or newer runs. Every finding carries a confidence level, and the report tells you what fraction of write calls it could resolve, so the tool is candid about its own coverage instead of pretending to certainty it doesn't have. And it is real AST analysis built on nikic/php-parser — not a pile of regular expressions.
Download the PHAR from the latest release and run it — one file, no Composer, no dependencies beyond PHP 8.3:
php sediment.phar scan path/to/pluginOr add it to a plugin project as a development dependency:
composer require --dev sediment/analyzerOr work from a clone:
git clone https://github.com/SNO7E-G/Sediment.git
cd Sediment
composer install
php bin/sediment scan path/to/pluginPoint scan at a plugin directory:
php bin/sediment scan path/to/pluginOptions (3)
function key confidence autoload cleaned source
add_option acme_version verified no yes acme.php:31
add_option acme_settings verified yes no acme.php:33
update_option acme_pro_* pattern unknown no pro.php:88
Tables (1)
function key confidence cleaned source
dbDelta {prefix}acme_logs resolved no install.php:12
Resolution rate: 92.0% (23 of 25 write calls resolved to a key).
Cleanup path: uninstall.php — 8 of 12 detected artifacts removed on uninstall.
Grade a plugin, generate the uninstall.php it should have shipped with, emit a machine-readable manifest, or gate your CI on the result:
php bin/sediment grade path/to/plugin
php bin/sediment uninstall path/to/plugin > uninstall.php
php bin/sediment scan path/to/plugin --json > manifest.json
php bin/sediment check path/to/plugin --fail-on=C # exit 1 if worse than CGuard a release against footprint creep, or scan a whole collection at once:
php bin/sediment diff manifest.json path/to/plugin # exit 1 if the footprint got worse
php bin/sediment batch path/to/plugins --out ./manifestsdiff compares against a manifest you committed earlier: a new artifact that isn't cleaned up, one that stopped being cleaned up, or a worse grade fails the build. Adding data you also remove on uninstall doesn't. batch writes one manifest per plugin plus a grade distribution, and is built to survive a run of thousands: each plugin is scanned in its own child process under a wall-clock --timeout and a --memory-limit, --jobs N runs that many children at once, one pathological plugin costs one line in the --report instead of the run, and --resume carries on from the manifests already written. index then turns a directory of manifests into the Index artifacts — a reverse lookup from artifact key to the plugins that create it, aggregate stats, and a QA report that refuses to build a dataset attributing WordPress core data to a plugin.
The manifest carries the grade, the coverage counts, and every artifact with its confidence, cleaned flag, and source lines — it is the contract other tools build on, frozen as a JSON Schema and documented in docs/manifest-schema.md, with the stability rules in docs/stability.md. check is the same analysis wired for CI, so a plugin author can fail a build on their database footprint the way they already fail it on tests.
Options (with the autoload flag), custom tables, cron events, transients, post/user/term/comment metadata, roles and capabilities, custom post types and taxonomies, directories, rewrite rules, Action Scheduler jobs — and the drop-ins and mu-plugins files a plugin installs, which keep executing on every request after the plugin itself is deleted.
Custom post types are the group prefix-matching tools structurally cannot reach. Uninstall an e-commerce plugin and its products stay behind as unreachable rows in wp_posts — often tens of thousands. No prefix reveals that. Reading the source does.
Sediment also reads how a plugin cleans up. A routine gated behind a "delete my data on uninstall" setting is reported as conditional — with the exact option and the value it defaults to — because a plugin that only cleans up for the few users who found that checkbox is clean in its code and dirty on real sites.
grade returns a letter and a weighted-damage score. uninstall writes a teardown that removes only the artifacts Sediment attributed with high confidence — never a WordPress core key, an already-cleaned key, or a guess.
Sediment labels every finding by how certain the attribution is, and it never presents a guess as a fact:
| Level | Meaning |
|---|---|
verified |
The key is a literal string in the source. |
resolved |
The key resolves from a constant, class constant, or literal property. |
pattern |
The key is partly dynamic but exposes a stable prefix, such as _mp_*. |
dynamic |
The key is only knowable at runtime. It is recorded honestly and never acted on. |
A grade reflects what a plugin leaves behind, weighted by real-world cost rather than a raw count — one orphaned autoloaded option matters more than twenty small rows nobody reads:
| Grade | Meaning |
|---|---|
| A | Removes everything it creates, unconditionally, on uninstall. |
| B | Removes everything, but only when the user opts in (conditionally clean) — the grade names the setting. |
| C | Removes some data; leaves a few harmless rows — none autoloaded, no tables or cron. |
| D | Leaves tables, autoloaded options, cron events, or orphaned content behind. |
| F | Ships no uninstall routine at all. |
The rubric is published in full at docs/grading.md so a grade is always something you can explain, not a black box.
Static analysis cannot see a key that is assembled entirely at runtime, so those are reported as dynamic and never acted on — the resolution rate tells you how much of a plugin fell into that bucket.
Sediment follows a write one hop back through a plugin's own settings layer, so update_option($key, ...) inside a helper resolves to the keys its callers actually pass. Across a corpus of ten real plugins the median resolves at 84%, but the largest ones resolve least: Yoast SEO manages 64% and WooCommerce 78%, because much of what they write is keyed at runtime. Sediment publishes those numbers rather than papering over them, and every unresolved write is listed with its source line so a human can settle it. The full corpus and its results are in docs/corpus.md. A handful of narrower edges (cron events scheduled with arguments, an aliased $wpdb handle) are documented plainly in docs/limitations.md. Publishing these is deliberate: an audit tool earns trust by being honest about its own blind spots.
Sediment is built in stages, each useful on its own:
- Analyzer (this repository) — detect what a plugin writes, diff it against the plugin's cleanup routine, grade the result, and generate the
uninstall.phpit should have shipped with. - The Index — shipped. A public, CC0 dataset mapping the 5,000 most popular plugins to the data they create, so a leftover row can be traced back to the plugin that made it: one manifest per plugin plus a reverse lookup, on the
index-databranch, rebuilt on CI from a pinned list anyone can rerun. The findings — among them that only 210 of 4,995 plugins remove everything they create — are indocs/index.md. - Inspector — a read-only WordPress plugin that scans installed plugins on disk and shows what each will leave behind before you click Delete.
The full plan, with what each release has to prove before the next begins, is in ROADMAP.md.
Release candidate. Detection across every artifact type above, static resolution, the cleanup diff (including conditional cleanup), grading, the manifest, the CI check, and the uninstall.php generator all work today, backed by a broad test suite. The Index has shipped: 4,995 of the top 5,000 wordpress.org plugins scanned with zero QA violations, published as open data — see docs/index.md for what it found, and docs/pilot.md for the 500-plugin pilot that earned the pipeline its trust. Per the roadmap, 1.0 follows thirty days after 0.9 with no schema-breaking defect. Releases are cut when a meaningful body of work is ready rather than per change, so each one carries real features — see the changelog and the releases. The manifest schema is frozen at 2.0 and covered by the rules in docs/stability.md; the rest of the tool may still change before 1.0.
composer install
composer test
php bin/sediment scan tests/fixtures/dirty-pluginSee CONTRIBUTING.md for how a change should look — especially the rule that nothing may be reported with more confidence than the source justifies.
Released under GPL-2.0-or-later. The Index dataset is dedicated to the public domain under CC0, so the open data can never be walled off.