WordPress plugin
Lets CheckRoost report WordPress, PHP, plugin and theme update status — without holding a credential that can change the site.
What it is
A single file that adds one read-only endpoint to your site. CheckRoost requests it on a schedule and reports what comes back.
The usual way to read this data is a WordPress Application Password, and we deliberately do not use one. The core route for listing plugins requires the activate_plugins capability, which is effectively administrator — so the secret a monitoring service stored for every site it watched could install plugins, create users and edit content on all of them. That is an enormous amount of power to buy a count of pending updates.
This inverts it, the same way the CheckRoost server agent does for a Linux machine: the site publishes a document about itself behind its own token, and the monitor reads it. Losing that token discloses version numbers. It does not lose the site.
What it publishes
One JSON document at /wp-json/checkroost/v1/health, and nothing else:
{ "wp": { "version": "6.7.1", "updateAvailable": false, "multisite": false, "debug": false }, "php": { "version": "8.3.2" }, "plugins": { "total": 24, "active": 22, "inactive": 2, "updates": 3, "checkedAt": "2026-10-05T04:11:00+00:00" }, "themes": { "total": 3, "active": "Twenty Twenty-Four", "updates": 1, "checkedAt": "2026-10-05T04:11:00+00:00" }, "cron": { "overdue": 0, "disabled": false }, "users": { "count": 2, "atLimit": false }, "hardening": { "fileEditDisabled": true, "xmlrpcEnabled": false, "forceSslAdmin": true, "fileModsDisabled": false }, "cache": { "objectCache": true, "pageCache": "WP Rocket" }, "database": { "bytes": 524288000, "autoloadBytes": 412000, "transients": 42, "revisions": 1200 }, "integrity": { "version": "6.7.1", "modified": 0, "missing": 0, "files": [], "checkedAt": "2026-10-05T03:00:00+00:00" }, "pluginVersions": { "woocommerce": "10.2", "yoast-seo": "22.0" }, "themeVersions": { "twentytwentyfour": "1.2" }}No post content, no user data, no settings beyond those. The counts come from WordPress’s own update transients, which it refreshes twice a day through wp-cron — so checkedAt is reported beside every one of them. A monitor that cannot tell “no updates” from “nobody has looked since March” is exactly the confidently wrong answer this product exists to avoid.
users.count is a count and never a name. An administrator account appearing is among the strongest signs a site has been taken over, and it is one nothing outside the site can see — DNS, uptime, the certificate and the page title all look identical the morning after. A list of administrator logins, by contrast, is a list of accounts worth attacking, which is exactly what this plugin exists not to publish.
integrity compares WordPress core files against the official checksums for your version. It is the one reading here that can detect a backdoor rather than infer one, and it is far too slow to run on every request — so it runs once a day on a scheduled event and the endpoint serves the cached verdict. The request path itself writes nothing. If WordPress.org cannot be reached the field is null, never zero: not checked is not the same as verified.
exposure and the exposedFile… maps report files that should never be in a site’s folders: database dumps, archives and backups, copies of wp-config.php, .env files, logs, a .git repository, and tools like phpinfo and Adminer. A host migration that leaves a full database export in the web root is how a site loses every credential it holds, and nothing else would notice it. Once a day the plugin walks the site’s folders, three levels deep and skipping WordPress core, plugins and themes, and records each file’s name, size and date. It never opens one. Then it asks the site for each file it found, once, with a HEAD request: a file the site will serve to anybody is critical; one the server refuses is a warning to tidy up. From 1.3.7, every file it found is looked for again each time CheckRoost asks, so one you delete drops off at the next check rather than the next morning.
From 1.3.6 it also looks for PHP files in wp-content/uploads, where only media belongs, and at the top of wp-content/cache. That is where attacks like wp2shell leave a webshell to come back through. Those files are never requested, because requesting one would run it. Instead the plugin asks for a made-up PHP file in the same folder: if the server refuses (401 or 403) it will not run PHP there and the file is a warning; any other answer means it would, and the file is critical.
Some plugins keep their own PHP there on purpose. CheckRoost recognises these by their folder and their naming, groups them into one row, and never calls them critical; the folder is still a warning while PHP can run in it, because that is what an attacker would use. A file in the same folder that is not named the vendor’s way — or, where the vendor’s files are empty placeholders, is not empty — is treated like any other. Recognised so far:
- Sucuri Security —
wp-content/uploads/sucuri/. Sucuri Security’s own logs and settings, stored as PHP that stops before printing anything, so a browser cannot read them. - WP All Import —
wp-content/uploads/wpallimport/. WP All Import’s empty placeholder files, which stop its folders being listed. Only empty files are recognised: one with anything in it is treated like any other.
What it will not do
There is no write route, and there never will be. The moment this can change something, the argument above stops being true and it becomes the credential it was built to avoid. Updating plugins is a job for your host, for WP-CLI, or for a person — not for the thing whose only job is to report honestly on what it finds.
Apart from WordPress’s own checks for updates to this plugin, those HEAD requests are the only ones it makes, and they go to the site itself.
The endpoint is authenticated even though it exposes only version numbers, because a list of installed plugins and their versions is a shopping list for anyone hunting a known vulnerability. Publishing it unauthenticated would leave the site less safe than before the plugin was installed.
Download
Version 1.3.9 — what changed.
The zip is for Plugins → Add New → Upload, or for ManageWP. checkroost.php is the same file that is inside it, to read before installing.
Install
Either route works. The upload is the one most people want.
# 1. Plugins -> Add New -> Upload Plugin -> choose the zip -> Activate# or, over SFTP:# unzip it and put the checkroost folder into wp-content/plugins/# then activate it from the Plugins screenKeep the checkroost folder. The bare file dropped into wp-content/plugins/ works, but it cannot update itself: an update unpacks into the folder and would leave the bare file’s install switched off, so that install is never offered one.
The first activation opens Settings → CheckRoost for you; after that it is the Settings link on the plugin’s row. It shows the endpoint and the token that was generated on activation, and a button that connects it to CheckRoost (below).
Updates
From 1.3.0 a new version appears where every other update does: the Plugins screen, Dashboard → Updates and ManageWP. It is installed the same way, by a person or a tool they chose, and nothing installs itself. The plugin asks https://app.checkroost.com/wordpress/info.json rather than WordPress.org, at most every six hours and only when WordPress itself is checking for updates. That request is the only one the plugin makes, and it never happens on a visit to the site.
Every update is signed. Before WordPress installs one, the plugin downloads it with its .sig and checks the Ed25519 signature against the public key built into the plugin. A zip that does not match is deleted rather than installed. The key that signs releases is not on this server, so somebody who took it over could stop updates arriving but could not use them to put code on your site.
Sites on 1.2.1 or earlier have no updater, so 1.3.0 goes on by hand, once: upload the zip over the installed plugin (WordPress offers Replace current with uploaded), or push it to every site at once with ManageWP. The token is kept, so nothing in CheckRoost needs changing.
Point a check at it
The quick way: on the WordPress site, open Settings → CheckRoost and click Connect to CheckRoost, or click Connect WordPress on the monitor’s Overview in CheckRoost, which opens that page for you. CheckRoost asks which monitor the site belongs to and sets up the check. The token travels in the part of the link after the #, which a browser never sends to a server, and it is saved only when you confirm.
By hand, which is also the way when the token is set in wp-config.php and there is no button: add an Application health check to the monitor for that site.
URL https://example.com/wp-json/checkroost/v1/healthHeader X-CheckRoost-Token: <the token from Settings -> CheckRoost>Assertions (all optional, all editable later): plugins.updates lt 5 wp.updateAvailable eq false cron.overdue eq 0Assertions are plain data rather than code, so they can be changed from the settings tab without a deploy. Start with one and add more once you have seen what a healthy site actually reports.
Moving the token out of the database
By default the token is generated on activation and stored as a site option. Defining it in wp-config.php instead is better: it is then absent from a database dump and cannot be read back from wp-admin by a compromised administrator session. The constant takes precedence, and the plugin stops using the stored value.
define( 'CHECKROOST_TOKEN', 'a-long-random-string' );Checksums
Generated from the same bytes this page serves, in the same build. A checksum served from the host that serves the file proves the download was not corrupted — it does not prove the host is honest. The signature does: checkroost-1.3.9.zip.sig is an Ed25519 signature over the zip’s SHA-384, made with a key that is not on this server, and it is what the plugin checks before installing an update. The plugin trusts two public keys, both also in checkroost.php: the one releases are signed with, and a backup kept offline so that losing the first never strands a site.
3AgWRlB/SGOa3jemBOKGBWJ6scvey0llNR/S+Kvt31Q= primary1c3LXYFEMXZMoSm+a70o2fS/34Fy984F322f+rOZ3TI= backup| File | SHA-256 |
|---|---|
| checkroost.php | 48f7e089531cf8635bd2238f2333e768d6a1674984bd648505c280a16d0ecb37 |
| README.md | b9488854c5a61b7e9d5965605cdd54f57451b9448ca567a6d436948a9f90be68 |
| CHANGELOG.md | 8dc047abd2468f15d431cb4742d37e1718f5a8b3d58ac35847515b54973484ed |
| checkroost-1.3.9.zip | f2e088b22552ba0795d2d579dfa72597307819f388d4e0eb3711d30351848c70 |
| checkroost.zip | f2e088b22552ba0795d2d579dfa72597307819f388d4e0eb3711d30351848c70 |
| checkroost-1.3.9.zip.sig | 7b6d5d0eb7b8204d122113b4f9b97682536831d51bb05b4c45093136606d591e |
Removing it
Deactivate and delete it from the Plugins screen, or delete the checkroost folder. The endpoint disappears with it. It leaves two small options behind, checkroost_token (absent if you used the constant) and checkroost_slow_readings, the cached daily readings. Both are inert once the plugin is gone.
Changelog
Every released version, newest first. The download above is always the newest; CheckRoost reports which version each site is actually running, so an install left behind is visible rather than assumed.
1.3.9
currentChecks every folder in uploads for PHP files, however many images the site has.
- The daily check for PHP files in wp-content/uploads used to stop after three seconds, so on a site with years of images it never reached some folders. It now reads the folders other plugins leave behind first, does not slow down for image files, and picks up the next day where it stopped, so every folder is checked within a few days at no more work a day than before.
- Until a full pass has finished, what the last full pass found stays listed for the folders not yet checked again, and CheckRoost shows how far the check has got.
1.3.8
The update screen and the update itself trust the server less.
- The “requires” fields the update server sends are version numbers or nothing: WordPress prints one of them into “View details” unescaped, so a compromised server could no longer put anything but digits there.
- An update is installed only when the version inside the signed zip is the one it was offered as, and newer than what is running — a signed older release served under a new number is refused.
1.3.7
A deleted backup or log drops off CheckRoost within minutes, not the next day.
- Files the daily check found are looked for again every time CheckRoost asks, so one you delete disappears from the list, and the alert, at the next check. It only checks that the file still exists; it never opens it.
1.3.6
Reports PHP files in uploads and cache, which is where a hacked site hides its way back in.
- The daily check of the site's folders now also looks for PHP files in wp-content/uploads, where only media belongs, and at the top of wp-content/cache. That is where attacks like wp2shell leave a webshell. It records the name, size and date, and never opens or requests the file.
- To rate one, it asks the site for a made-up PHP file in the same folder. If the server refuses to run PHP there it is a warning; if it would run it, critical.
1.3.5
Opens its settings page after you first activate it, so Connect is one click away.
- The first time the plugin is activated, WordPress goes straight to Settings → CheckRoost and its Connect button. Only for the person who pressed Activate, and never for a bulk, network, WP-CLI or ManageWP activation. Turning it off and on again stays on the Plugins screen.
- A Settings link on the plugin's row in the Plugins list.
1.3.4
Also trusts CheckRoost's offline backup signing key, so updates survive losing the main one.
- Updates are accepted when signed by either of two CheckRoost keys. The second is kept offline, so if the main key is ever lost or stolen, a release signed with the backup can replace it — without reinstalling the plugin by hand on every site.
1.3.3
Finds database dumps, backups and config copies a browser could download.
- Checks the site's folders once a day for files that should never be there: database dumps, archives and backups, copies of wp-config.php, .env files, logs, a .git repository, and tools like phpinfo and Adminer. It reports each file's name, size and date and never opens one.
- Asks the site for each file it finds, once, to learn whether a browser could download it. A file anybody can download is critical; one the server refuses is a warning to tidy up.
- CheckRoost can alert on a critical file, and Settings → CheckRoost lists what was found for the site's own administrators.
1.3.2
A proper View details page, with the CheckRoost icon in the update lists.
- View details now has a description, installation steps, an FAQ and every release's changes, rather than one line.
- Shows the CheckRoost icon and banner instead of WordPress's grey plug.
- Says which WordPress version it is tested with, so the Updates screen no longer reads "Compatibility: Not tested".
1.3.1
Connects to CheckRoost with one click instead of copying the token.
- Settings → CheckRoost has a Connect to CheckRoost button. It opens CheckRoost, where you choose which monitor reads this site, and sets up the check there. Nothing is copied by hand.
- The token travels in the part of the link after the #, which a browser never sends to a server, so it does not appear in any access log. CheckRoost saves it only once you confirm.
- No button when the token is set in wp-config.php, because that setup exists to keep the token out of wp-admin.
1.3.0
Updates itself from now on. Install this one by hand, once.
- New versions now appear under Dashboard → Updates, on the Plugins screen and in ManageWP, like any other plugin, and are installed the same way. They come from app.checkroost.com, not WordPress.org.
- Every update is checked against CheckRoost's signing key before WordPress installs it, and one that does not match is deleted rather than installed. The key that signs releases is not on the server, so taking over the server would not be enough to change these sites.
- An install that is the bare checkroost.php file rather than the checkroost folder is not offered updates, because one would leave the plugin switched off. Settings → CheckRoost says so.
1.2.1
Core integrity now says why it could not verify, instead of looking unfinished.
- Distinguishes "the daily check has not run yet" from "it ran and WordPress.org published no checksums for this version". Both used to report nothing at all, so a site that would never be verifiable looked exactly like one you only had to wait for.
- Reports when verification was last attempted, separately from what it found — so an unverifiable reading cannot be read as a recent clean one.
1.2.0
Names each plugin the way WordPress does, and says what an update would install.
- Publishes the display name WordPress shows for each plugin and theme, so the dashboard can read "WooCommerce" rather than "woocommerce". Diffing still keys on the slug, so a plugin that rebrands itself is not reported as one removed and another installed.
- Publishes the version each pending update would install. WordPress already knows it — it is what the Plugins screen renders as "update now to 11.0.1" — and publishing only the count meant the dashboard could say "13 pending" without ever saying what any of them were.
1.1.1
Fixes autoloaded option size, which read as a byte on WordPress 6.6 and later.
- Measures autoloaded options the way WordPress itself does. 6.6 replaced the plain yes/no autoload column with a set — on, auto, auto-on — and the old query matched almost nothing on a modern install, reporting one byte where a real site has hundreds of kilobytes.
1.1.0
Names which plugin changed, and can see an administrator appearing.
- Publishes plugin and theme versions by slug, so an update is reported as "woocommerce 10.1 → 10.2" rather than a count that moved.
- Publishes the number of administrator accounts — a count, never a name. An administrator appearing is among the strongest signs a site has been taken over, and nothing outside the site can see it.
- Verifies WordPress core files against the official checksums, on a daily schedule. The request path still writes nothing.
- Publishes autoloaded option bytes, caching in use, hardening constants and database size.
1.0.0
- First release. Publishes a read-only health document behind its own token: WordPress and PHP versions, plugin and theme update counts, WP_DEBUG, and the wp-cron backlog.
- No write route, and no capability granted. Losing the token discloses version numbers; it does not lose the site.
From 1.3.0, updates arrive in Dashboard → Updates (how). The token survives every update, and deactivating is not uninstalling: a site toggled off and on again does not need re-pasting into CheckRoost.