Keel adds a menu of sensible defaults to any WordPress install, each one a switch under
Settings → Site Defaults. Nothing is hidden and nothing is all-or-nothing — you can see exactly what the plugin does to your site and turn any switch on or off.
See and install the security patch for your own WordPress release line. Keel tells you when
WordPress.org flags the installed core version as insecure, names the patched release on the same line instead of pushing you toward a major upgrade, shows the releases WordPress is offering and marks the one core would select. An authorized administrator can deliberately install the same-line patch through WordPress's own upgrader with rollback enabled. The target is checked again on the server; no setting changes, and nothing installs unless you click the button.
Disabling something means it is actually disabled. When you switch comments off, they are off below the presentation layer, not merely hidden by the theme template and the REST route — ask the database directly with
get_comments() and there is nothing to hand back. The same care runs through the rest: closing the REST API also removes the link advertising it, and disabling comments also stops the comment feed from answering requests.
Site Health shows you the whole posture, read-only: every default and its current state on one screen, so you can see what the site is actually doing without clicking through tabs. It also reports when another plugin is controlling the same settings, which otherwise fails silently.
Outgoing email stops at the edge of production. An unsanitized database copied down from production carries real customer addresses and whatever mail service production was using, so a cron run or a bulk action can email real people from a staging site or a laptop. Keel suppresses outgoing mail on any environment that is not production by default. It does nothing on production, and Keel says so in an admin notice so no admin is left wondering why a password reset never arrived.
Keel works the same on a Multisite network. Activated across multisite, Keel seeds every existing site and every site created afterwards, so a later change to a default cannot apply to some sites and not others. A Super Admin can see and change any setting for the whole network under Network Admin → Settings → Network Policy. Sub-sites on the network see those settings as locked, with their own saved values untouched underneath, so lifting a policy returns each site to exactly what it had.
- Copy the plugin folder into
wp-content/plugins/, or upload the built zip through Plugins → Add New → Upload Plugin.
- Activate it. The documented defaults are seeded on activation; nothing is applied before that.
- Visit Settings → Site Defaults and turn off anything you do not want.
Every default is a switch, and the switches are the whole interface. Defaults that can change behaviour or break an integration — requiring authentication for all REST requests, blocking the XML-RPC endpoint, the Classic editor — are off out of the box and opt-in.
There is one exception: Keel sends an
X-Frame-Options header of
SAMEORIGIN, so other sites cannot embed yours in an iframe. If something else is meant to display your site inside a frame — an intranet dashboard, a screenshot or visual-review service, a kiosk or signage screen — set
Frame options to "Leave unchanged" under Security and Attack Surface. A blocked frame usually fails silently, as a blank box, but Keel's rule is to call everything out.
Deactivating stops every default at once; stored settings are kept so reactivating restores the same configuration. Uninstalling removes them.
Versions before 0.5.9 were not published to the directory. The entries below are the development history that led to the first release.
0.6.5
- Fixed: the admin menu width slider stopped previewing the change as you dragged it, on any site that had a width saved. The preview was still there; Keel's own saved rule was overriding it, because that rule is marked important and the preview was not. The preview now outranks it, as it was always meant to.
- Documentation: the FAQ now states a limit that was unstated. Keel reports a setting that is not taking effect by watching WordPress filters. A plugin, theme or host that restyles the admin with CSS registers no filter, so there is nothing to observe - the admin menu width is the usual case, and a managed host styling the admin to its own design is not a conflict Keel can see or should fight.
0.6.4
- Fixed: the admin menu width slider offered "WordPress default (160px)" as its first stop, but that stop set no width at all - it only made Keel stand down. On a site where a theme, a host, or another plugin had widened the menu, it was the stop you would reach for and the one guaranteed to do nothing. There is now an explicit 160px stop that asserts core's width, and the first stop says what it does: "Leave unchanged".
- Fixed: the conflict notice reported settings shared with callbacks it could not trace and sent you to Site Health, where there were no open issues. The finding was there, but filed under a passing test - green, collapsed, and headed "No attributable policy overlap was found". Untraceable overlaps are now reported as a recommendation, so the notice and Site Health describe the same site.
- Changed: the conflict notice names Keel, drops a sentence the link beneath it already made, and says settings "may be contested" rather than asserting a contest the Site Health test itself declines to assert.
0.6.3
- Added: the security release on your own version line is now offered on the Updates screen, where WordPress sends you to update. WordPress builds that screen with
get_core_updates(), which discards every offer flagged for automatic installation — and a same-line security patch is only ever offered that way. So the screen has always listed the newest release and never mentioned the patch. Keel adds it back, directly below the automatic-update settings that decide which release the site would take.
- Fixed: an install started from the Updates screen finished, then sent you to Site Health to find out whether it had worked. Worse, it usually said nothing when you got there: a successful install leaves the site secure, so the panel carrying the result correctly stops rendering. The result now appears on the screen the button was pressed on, once.
- Fixed: the Updates screen offer named the newest release from the WordPress.org stable check, and called it "the update offered above". That check and the update list WordPress renders refresh on their own schedules, so the two could disagree — or the screen could be offering nothing at all. It now reads the same list the screen does, and says nothing rather than inventing an update above.
- Fixed: the Site Health panel told you the Updates screen would not offer the patch, on sites where Keel had just added it there.
- Changed: the ladder markers are one symbol and one label per rung rather than several, and the plainer wording drops an explanation of release numbering nobody reading two version numbers needs.
- Screenshots retaken against the current wording.
0.6.2
- Fixed: the patch-status panel could promise a scheduled install the ladder directly beneath it contradicted. "Minor updates are permitted and the updater works" does not establish what WordPress would install: a site that also accepts major updates gets the highest release on offer, not the nearest, so the panel could say a patch was scheduled above a ladder marking a different release as the one WordPress would take. The claim is now made only when core's own selection is that patch, and names the release core would take instead when it is not.
- Fixed: the persistent admin notice repeated the same promise with no ladder beneath it to correct it. It renders on every admin screen, so it cannot afford to ask WordPress which release it would install — it now says automatic updating appears available and sends you to Site Health, which can answer.
- Fixed: the panel offered a deliberate install in states where Keel refuses one, and then, once corrected, claimed no install was possible at all. Both were wrong. Keel refuses a blocked install; a deployment workflow or WP-CLI may still manage it, and the wording now says which of those it is speaking for.
- Fixed: an unexpected
null from WordPress's upgrader was reported as a successful install. Core documents a version string on success, so null establishes nothing.
- Added: two live matrix rows that leave the automatic updater operable — one where WordPress would take the same-line patch, one where major updates are permitted and it steps over. Every previous row switched the updater off, so no row had ever rendered the panel in the state these fixes are about.
- Screenshots retaken against a release WordPress.org flags, so the listing images show the current wording.
0.6.1
- Fixed:
/?author=N still disclosed the author nicename, and attachment pages still rendered, on sites with those defaults enabled. Both redirects registered on template_redirect at the default priority, where core has already registered redirect_canonical during load — so they lost the tie on registration order every time. Both now run at priority 9.
- Fixed: a site with comments disabled still answered
/wp/v2/comments/123. The filter covering every comment listing does not cover the single-item REST route, which reads its row without building a query.
- Fixed: the REST password policy resolved any user ID in the request before the route checked authorization, so the policy ran against another account's login, email and nicename and returned the answer in the validation error. It now resolves only the caller's own user, or one they may edit.
- Fixed: an empty or over-long password reached the breach-screening network call before anything cheap rejected it.
- Added:
tests/hook-precedence.php and tests/route-coverage.php, which check that a registration wins its hook and covers its routes rather than merely existing. The first two fixes above were invisible to the previous suite.
- Thanks to Austin Ginder of Anchor Hosting for the security review these fixes come from.
0.6.0
- New: Site Health reports whether the installed version of WordPress has publicly known vulnerabilities. That is a different question from whether an update is available, and nothing in wp-admin answered it. WordPress.org publishes the answer at its core stable-check API, which core itself never queries.
- Where a patched release exists on your own release line, Keel names that release rather than the newest one. A 6.9.5 site is told about 6.9.7, not 7.1 — only the third number changes, so nothing is deprecated.
- New: the ladder of releases WordPress.org is currently offering this site, and which one WordPress would actually install. It takes the highest release your settings permit rather than the nearest, so a site accepting major updates skips the patch and jumps to the newest release.
- Fixed: the panel offered "Install this now from the Updates screen" when the patch appeared only as an automatic-update offer.
get_core_updates() omits those offers, so following that button installed the newest release instead. Keel now asks core what that screen is showing, and distinguishes listed, hidden behind Show hidden updates, absent, and not yet known.
- Blockers name their own cause — the constant and the file it usually lives in, or the filter a plugin is using — instead of describing the situation in the abstract. Each one needs a different fix, and they now carry stable codes rather than being told apart by their translated text.
- New: install the patch from the Site Health panel, using WordPress's own upgrader with rollback enabled. The target is recomputed on the server and can only ever be the patched tip of your own release line, so it cannot cross a release line or move a site backwards. It refuses when files are not writable, when the site is a version-control checkout, or when the release's PHP or MySQL requirements are not met — that last check runs before anything is downloaded, because core otherwise reads those requirements out of the new version's own files and only finds out after unpacking it.
- Being blocked from automatic updates does not block a deliberate install. A disabled updater, a filter, or an earlier failed update are all reasons a patch will not arrive by itself, and installing it deliberately is the remedy for each.
- The other remediation switches minor auto-updates back on, and only where the stored option is genuinely what decides.
0.5.10
- The network settings screen is now Network Policy rather than "Keel Defaults". It decides settings for every site on a network and locks them, which is the opposite of what the per-site screen does, and the old name did not say so. The screen, its address and its behaviour are otherwise unchanged.
- Two links in Site Health pointed at "Settings → Keel", a menu that has not existed since that screen was renamed to Site Defaults. They now name the screen you will actually find. The same correction has been made to the multisite password help text and to this readme.
0.5.9
- Breach-cache entries can no longer be reached again after the plugin is removed and reinstalled. On a site with Redis or Memcached, WordPress keeps transients in the object cache rather than the database, where the uninstaller's queries cannot follow them. Each installation now has its own cache namespace, so anything left behind belongs to an installation that no longer exists.
- Rewrote the Help menu's Environments and Overlapping plugins tabs, and corrected how the environment type is described: WordPress reads the
WP_ENVIRONMENT_TYPE constant and wp_get_environment_type() reports the result, not the other way round.
0.5.8
- Header names, constants and file names in the Help menu are shown as code again rather than as running text. Three paragraphs across two tabs had lost the formatting the rest of the help uses.
0.5.7
- Breach-screening reports are now based on evidence rather than the absence of it. A successful lookup clears an earlier failure, which it did not before — so a site that recovered kept being told it had a problem, against the notice's own promise. Site Health also distinguishes "the last lookup completed" from "nothing has ever run here", and notices when the lookup has been switched off with the filter rather than only the constant.
- Grouped settings that depend on another setting now show and hide as you change it. The three XML-RPC method controls were the only settings rendered inside a group, and the script that reveals dependent settings only looked at ungrouped ones — so those three stayed as they were until the page reloaded, and never announced the relationship to a screen reader.
- Network-locked sliders and role checkboxes now behave as locked. They said they were locked and then accepted edits anyway. The server always refused the value; now the screen agrees with it.
- Deleting the plugin clears breach-cache data held in a persistent object cache. On a site with an external cache the cached hash prefixes were not database rows at all, so they outlived the plugin by up to twelve hours.
- Tightened the check that removes the settings option from before the rename. It deleted any stored array carrying one of five fairly ordinary keys, which could have taken an unrelated option with it.
0.5.6
- Breach screening now says when it is not working. If the Have I Been Pwned lookup cannot be completed — the service is unreachable, rate-limiting, or something else answered in its place — Site Health reports it instead of the check being skipped in silence. Passwords are still never blocked by an outage, and the rest of the password policy is unaffected; the difference is that a site whose screening stopped working weeks ago can now find out.
- Added help for environments and for overlapping plugins. The Help menu on the settings screen explains why email stops on staging, what the environment indicator is for, and what the two kinds of overlap report actually mean — including why some overlaps name a plugin and some cannot.
0.5.5
- Fixed a confusing conflict report. Where a callback could not be traced back to a plugin, Site Health said "callbacks from Unattributed callback" — which reads as the name of a plugin, and several such lines read as several plugins, none of them the one the notice had actually named. It now says a callback could not be traced to a plugin, and the notice accounts for those settings too, so the two screens agree.
0.5.4
- A setting that is not taking effect now says so where you will see it. Keel could already detect that something else on the site was overriding one of its settings, but it reported that only under Site Health — which nobody opens until something has already gone wrong. It now appears on the dashboard and the plugins screen, which is where the overlap warnings already were. This is the case you most need telling about, because a plugin that switches a feature off using one of WordPress's own helper functions leaves nothing to name it by.
- Fixed a comments heading appearing on posts with comments switched off. Keel reported the count as the number zero where WordPress reports it as the text "0", and core's Comments Title block compares the two exactly — so it did not take its early return. Affected block themes, which is the default.
- Pingbacks are now watched for the same override as comments. Both are decided by the same setting and were registered together, but only comments was checked.
0.5.3
- Fixed the login screen's logo link. Removing, unlinking or replacing the logo is supposed to point that link at your site's home page; it pointed at your home page with
https://wordpress.org/ appended, which goes nowhere. Affected every site with any of those three behaviours set.
- Keel can now see another plugin that switches the same setting off in the same way it does. WordPress stores one entry per callback, so when two plugins register the identical callback at the identical priority the second replaces the first and only one remains — which meant a plugin doing exactly what Keel does could be reported as Keel's own registration, or hide Keel's. Keel now uses callbacks that are its alone, so both are always visible and the report can tell them apart.
- Every stylesheet and script Keel adds now goes through the WordPress asset API instead of being written straight into the page. Nothing changes on screen. It means a site can dequeue, override, or defer any of it by handle, and that caching and asset-optimizing plugins can see it — none of which was possible while the markup was printed directly.
- The settings screen's CSS and JavaScript are now static files rather than markup rebuilt on every page load, so a browser caches them. The admin-menu-width slider carries its labels and widths as data attributes, which also makes its script work for any number of sliders rather than being re-emitted once per field.
- The network policy screen and the per-site settings screen now share one copy of the script that refuses changes to a locked control. There were two, and they had already drifted apart.
- Deleting the plugin now also removes the settings option from before the rename, which a site that had run both the older plugin and this one kept as an orphaned autoloaded row. Deleting from the Plugins screen left it behind, because nothing in the current code refers to that name any more.
- Dropped the compiled Canadian English translation from the plugin package. Translations for every locale are generated and delivered by translate.wordpress.org; shipping a catalog alongside that only means two sources for the same strings.
0.5.2
- Fixed a setting being switched off by saving a different one. On WordPress 6.4 to 6.9 the AI Connectors control is not shown, because those versions have no AI connectors to turn off — but saving any other setting still read the missing checkbox as "off" and rewrote the stored value from on to off. Silently, and against a note in 0.4.0 saying the stored value was left alone. A site that had chosen to block connectors would have reached WordPress 7.0 with them enabled.
- A setting the screen does not show is no longer changed by saving the screen. This is the same protection settings locked by
wp-config.php already had, for the same reason: the form has no business speaking for a control it did not draw.
- Network Admin no longer offers a network-wide policy for a feature the running WordPress does not have, and an existing policy for one survives a save rather than being read as switched off.
- The Network Admin role list shows role names again rather than their internal slugs, and no longer emits a PHP notice for each one.
- A plugin that only removes entries from the block inserter is no longer reported as competing with Keel. Two plugins restricting which blocks are available both get their way, so reporting a collision there suggested deactivating a plugin that works alongside this one.
0.5.1
- Removed effect probes from policy-overlap detection. Diagnostics no longer execute another plugin's callbacks with synthetic or real user/post context, so reporting an overlap cannot send mail, write data, terminate the request, mutate hooks, or trigger other callback side effects.
- Restored structural detection on authoritative hooks: Keel must be registered on the hook and the other callback must be attributable to an active plugin. The report confirms shared ownership only, tells administrators to compare settings, and does not recommend deactivation from presence alone.
- Memoized the overlap report for each request and added adversarial coverage for mutating, throwing, and terminating callbacks, plus guards against hook-registry mutation and overstated UI copy.
0.5.0
- Added post-revision retention: new activations keep 10 revisions, existing sites preserve their previous unlimited behavior on upgrade,
-1 means unlimited, and 0 disables future revisions. Numeric or false WP_POST_REVISIONS policy locks both site and network controls.
- Author feeds now return an explicit 404 when author archives are disabled. They were already closed by the archive's broad 301 because WordPress sets both query flags; the corrected test now proves that routing fact against the real request.
- Rebuilt overlapping-policy detection around confirmed, compatible, and unconfirmed effects. Callback presence alone no longer generates deactivation advice, and the incorrect claim that mail/comment-query callbacks stop after the first non-null value is gone.
0.4.1
- Removed the unconfirmed half of the overlapping-settings check. It reported a plugin when something untraceable was registered on a setting Keel also sets and that plugin's source mentioned the same filter — but WordPress itself, and Keel itself, both register through the same untraceable helper functions, so the first of those two conditions was true on nearly every setting. That left one weak signal doing the work of two, and it named plugins that were not doing anything: Clearfy and WP Master Toolkit were both reported on five settings between them while registering nothing at all. Confirmed detection is unchanged and unaffected.
- The check now says what it cannot see, on the settings screen and in Site Health. A plugin that turns something off by handing one of WordPress's own helper functions to a filter cannot be traced back from that filter, so a clear result means nothing traceable was found rather than nothing competing.
0.4.0
- The plugin folder is now
keel-defaults rather than keel, and the text domain moved with it. WordPress.org serves translations as {slug}-{locale}.mo, so a text domain that is not the slug means no translation ever loads — silently, with nothing to search for.
- Keel now tells you when another plugin is setting the same things it is. Session length, comment behaviour, the editor and a dozen other settings are applied through WordPress filters that return a single value: when more than one plugin uses the same filter, only one of them takes effect, there is no error, and the ones that lost go on showing the values they set. The check names the plugins and the settings — on the Plugins screen, on Keel's own screen, and in full under Site Health.
- What it cannot see is stated where it is reported. A plugin that turns a feature off by handing one of WordPress's own helper functions to a filter leaves nothing to trace back to it, so a clear result means nothing traceable was found rather than nothing competing.
- A conflict needs Keel to be on the hook too. Turning a default off takes Keel out of the contest and the report follows, instead of warning about a setting Keel has stopped touching.
- Capability conflicts are judged by the capability rather than by the filter. Nearly every plugin that adds a custom role uses the same filter Keel uses to take
unfiltered_html away, and almost none of them touch that capability; only the ones that do are reported.
- AI Connectors no longer appears on WordPress versions that have no AI connectors. The setting is gated on the core function rather than a version number, and its stored value is left alone, so a site that upgrades to 7.0 finds the default already there.
- Tested against WordPress 7.1, behaviourally rather than by reading the release notes.
0.3.0
- Multisite: a Super Admin can decide any setting for the whole network, under Network Admin → Settings → Keel Defaults. Sites see those settings as locked. Policy applies when a value is read rather than being written into each site, so a site's own saved settings are untouched and lifting the policy returns every site to exactly what it had.
- A locked setting now stays locked when the form is saved, not only when it is drawn. A wp-config constant or a network policy was enforced in the rendered control and nowhere else, so a submission could still write the value it protected. It never took effect, but the stored setting drifted from what the screen showed.
- Locked controls can be reached by keyboard and screen reader, and say why they are locked. They were disabled, which removes them from the tab order — so the explanation attached to them was announced on a focus that never happened.
- Settings that hide when another choice makes them irrelevant now tell assistive technology which control governs them, and whether they are showing.
- The staging environment indicator failed WCAG AA contrast at 2.41:1 against the 4.5:1 minimum for text that size. Every environment colour is now checked by the test suite.
- The admin menu width slider announces its setting as a word rather than a position, and no longer repeats itself on every keypress.
- Site Health → Info groups the defaults by category instead of repeating the group name on every row, and the section is named "Keel Defaults" rather than "Keel".
- Number settings report their unit, so Site Health says "14 days" rather than "14".
- X-Frame-Options is left alone inside the Customizer preview, which sets that header itself so the preview can load.
- Translation catalogs rebuilt: 68 strings in the code were missing from the template, and the en_CA catalog translated nothing because every string it named had been reworded.
- An XML-RPC help tab covering the whole family — what it is, why four switches rather than one, the Jetpack constraint, and why system.multicall's reputation is out of date.
- A "try it live" Playground link that follows each stable release.
0.2.0
- First stable release. The initial feature set was frozen; what changed since the scaffold is listed below.
- Comment teardown now reaches past the rendered page: comment queries are answered empty, comment blocks stop rendering in block themes, comment feeds return a real 404 instead of a redirect loop, and the comment count reports zero.
- A closed REST API stops advertising itself — the
<link rel>, the Link: header and the RSD entry all go — and oEmbed stays reachable through the gate so other sites embedding yours do not silently degrade to a bare link.
- Author identity no longer leaks past a hidden author archive. oEmbed responses drop
author_name and author_url, and the users sitemap provider is removed.
- Uninstall leaves nothing behind: settings, the last-login user meta and the breach-screening transients are all removed, network-wide on multisite.
- Activation seeds every existing site on a network, and a subsite created afterwards is seeded too, so a later schema change cannot move some sites and not others.
- Site Health reports every default and its state under Info, flags only what warrants attention under Status, and names other active plugins setting the same defaults.
- Outgoing mail is suppressed outside production, and the settings screen says so on screen rather than only in a notice.
- The session-length filter stands down when it has nothing to say, so it does not overrule a host or another plugin that has already decided.
- Environment detection no longer overrides a site that declares
WP_ENVIRONMENT_TYPE through an environment variable rather than the constant.
0.1.0-dev
- Removed the reserved-usernames default. It refused to create accounts named
admin, support, info and 70 others, which is a reasonable policy for a managed fleet and a presumptuous one for a general-purpose defaults plugin — the list is long, opinionated, and includes names an ordinary site legitimately uses (manager, marketing, sales, office, client). Existing accounts were never affected and still are not. A stored setting is ignored and drops out of the option on the next save; no migration is needed. To keep the behaviour, WordPress's own filter does it in one call: add_filter( 'illegal_user_logins', function ( $logins ) { return array_merge( $logins, array( 'admin', 'administrator', 'root' ) ); } );
- Initial scaffold: base imported from Better by Default (WPYEG, GPL-3.0-or-later) and re-identified as Keel. Work in progress.
- Licence is now GPL-2.0-or-later, matching WordPress core and the upstream 10up Experience code some defaults descend from. Relicensed by the sole author of the carried-over work; nothing is withdrawn, since "or later" still permits GPL-3 terms.
- Breach screening can be switched off with the KEEL_DISABLE_HIBP constant or the keel_disable_hibp filter, and a truncated or malformed range response is now rejected instead of parsed and cached.