Skip to content

Change alerts

Arkazzae edited this page Oct 7, 2026 · 1 revision

Change alerts e-mails the alert recipients when something changes that attackers change after they get in: a new administrator, a new or activated plugin, a new theme, a must-use plugin or drop-in, an edited wp-config.php, or the settings for registration, the default role, the admin e-mail and the site address. Every change is also written to the activity log.

Changes made through WordPress to administrators and those settings are reported at once. Everything else is found by an hourly comparison with a stored baseline, which also catches changes made straight in the database or over FTP.

Module ID watch
Default On
Available Always
WP-CLI wp shouse watch (only while the module runs)
Since 0.1.0

How it works

Immediate alerts

These are reported the moment WordPress makes the change:

  • a user's role is set to Administrator: "User promoted to administrator". WordPress sets the role of a new account before it finishes creating it, so a new account created as Administrator is reported with this line too.
  • the Administrator role is added to a user as an extra role: "Administrator role added to a user"
  • a new account gets another role that can manage_options, such as a custom role: "New administrator account"
  • one of the five watched settings changes: users_can_register (anyone can register), default_role, admin_email, siteurl (WordPress address) or home (site address)

Each alert names who did it: the logged-in user's login, or "no logged-in user (code, CLI or cron)". A new administrator is reported like this:

Subject: [SafeHouse] SITE NAME: new administrator

User promoted to administrator: jan (jan@example.com), by admin

If you did not expect this, lock the account and check the site.

A changed setting is reported like this:

Subject: [SafeHouse] SITE NAME: site setting changed

Setting default_role changed from "subscriber" to "administrator", by no logged-in user (code, CLI or cron)

Attackers change these settings to register their own administrator or take over password resets. If you did not expect this, change it back and check the site.

Values in alerts are cut to 200 characters. When admin_email changes, the old address gets the e-mail too, so the owner hears about it even if the attacker changed the address.

An immediate alert also updates the baseline, so the hourly check does not report the same change again.

Hourly check

Once an hour, SafeHouse takes an inventory and compares it with the baseline from the previous check:

What How it is compared Reported as
Administrators Users with the Administrator role, by login "Administrator added: LOGIN", "Administrator removed: LOGIN"
Plugins Names of the top-level entries in the plugins directory, so single-file plugins count too "Plugin directory added: NAME", "Plugin directory removed: NAME"
Active plugins The list of active plugins "Plugin activated: FILE", "Plugin deactivated: FILE"
Themes Names of the top-level entries in the themes directory "Theme added: NAME", "Theme removed: NAME"
Must-use plugins Names of the top-level entries in wp-content/mu-plugins "Must-use plugin added: NAME", "Must-use plugin removed: NAME"
Drop-ins Which of these files exist in wp-content: advanced-cache.php, object-cache.php, db.php, db-error.php, maintenance.php, install.php, sunrise.php, fatal-error-handler.php, php-error.php "Drop-in added: FILE", "Drop-in removed: FILE"
wp-config.php A SHA-256 hash of the file, in the WordPress folder or one level above it "wp-config.php changed"
The five watched settings Read straight from the options table, past any filter and the object cache Setting NAME changed from "OLD" to "NEW"

Directory entries whose names start with a dot, and index.php, are ignored. Reading the settings straight from the table means a value planted in the database is seen even when Hardening hides it from WordPress.

All changes from one check go out in one e-mail:

Subject: [SafeHouse] SITE NAME: changes detected

SafeHouse noticed these changes since the last check (up to an hour ago):

- Plugin directory added: example-plugin
- Plugin activated: example-plugin/example-plugin.php
- wp-config.php changed

If admin_email is among the changes, the old address gets this e-mail too.

Each check replaces the baseline with what it found, so every change is reported once. The first check after the module starts has nothing to compare with: it records the baseline and reports nothing.

Severity in the activity log

The hourly check writes one inventory_changed entry with all changes. It is critical when any change is an administrator added, a must-use plugin added, a drop-in added, a wp-config.php change or a setting change, and a warning otherwise. Immediate alerts are logged as admin_added and option_changed, both critical.

Settings

Change alerts has no settings. While it runs, Details shows what it watches, for example "Watching 2 administrators, 14 plugins, 0 must-use plugins, 2 drop-ins, wp-config.php and 5 site settings." Before the first hourly check it shows "No baseline yet. It is recorded on the first hourly check."

Details also has two buttons:

Button What it does Result
Check now Runs the hourly check at once and e-mails what it finds. "1 change found and reported.", "3 changes found and reported." or "No changes since the last check."
Accept current state Stores the current inventory as the baseline without reporting anything. "The current state is the new baseline."

Accept a deploy

A deploy changes plugin directories and often wp-config.php. Without help, the next hourly check e-mails those changes as if an attacker made them. Accept the result of the deploy as the baseline instead.

At the end of a deploy script, run:

wp shouse watch accept

It prints Success: Baseline updated. and writes inventory_accepted ("Current plugins, admins, files and site settings accepted as the baseline") to the activity log.

accept also takes in, without a report, anything else that changed since the last hourly check. To have those changes reported first, run a check before the deploy starts:

wp shouse watch check

check prints each change, e-mails them like the hourly check, and ends with Success: 2 change(s) reported. or Success: No changes.

A deploy script then looks like this:

wp shouse watch check
git pull --ff-only
wp shouse watch accept

Without WP-CLI, select Accept current state in Details after the deploy.

The wp shouse watch command exists only while the module runs. On a site where Change alerts is off or SafeHouse is in safe mode, WP-CLI reports an unknown command, so guard the line in scripts that run on several sites. For more on deploys, see Deploy with git or CI.

What it stores

  • Option shouse_watch_state (not autoloaded): the baseline, with administrator logins, directory listings, active plugins, drop-ins, the wp-config.php hash and the five settings.
  • Activity log entries: admin_added, option_changed, inventory_changed, inventory_accepted.

Limitations

  • Switching the active theme is not watched. Only theme directories that appear or disappear are reported.
  • File contents are not compared, except for wp-config.php. A changed file inside an existing plugin or theme directory, in wp-content/uploads, or .htaccess is not reported.
  • The hourly check counts only users with the Administrator role. A user given a custom role or another role with administrator capabilities after the account was created is not reported.
  • A change that is undone before the next hourly check, such as a plugin added and removed within the hour, is not seen.
  • The hourly check runs on WP-Cron. If WP-Cron does not run, for example with DISABLE_WP_CRON and no system cron job, only the immediate alerts, Check now and wp shouse watch check work.
  • accept and Accept current state take in any unreported change without a report.
  • Each change is e-mailed once. A change you missed in the e-mail stays in the activity log for 90 days.
  • The e-mails are in English, whatever the site language.

Related pages

Clone this wiki locally