WP Manifestindependent plugin directory
manifest / admin / post-change-tracker

Post Change Tracker

★ 0stars

Install

Download source zip No release zip yet

This is the repository archive. It installs, but the folder name will carry the branch suffix and updates will not flow.

Install straight from GitHub with WP-CLI:

wp plugin install https://github.com/neevalex/post-change-tracker/archive/refs/heads/main.zip
No built-in updater. New versions won't appear under Dashboard → Updates. Install new releases yourself, or let Git Updater track the repository.

A tiny WordPress plugin that answers one question: "who or what keeps changing my posts?"

It hooks into WordPress core and logs every change made to a post:

  • Core fields — title, content, excerpt, status, slug, author, dates, parent, menu order, comment/ping status, password (masked), post type
  • Taxonomy terms — categories, tags, and any custom taxonomy
  • Custom fields / post meta — featured image, SEO fields, ACF, or any other post meta (with a filterable ignore-list for noisy internal keys)

For every change it records:

  • which post changed, and the old/new values
  • the source of the request — wp-admin, REST API, XML-RPC, WP-Cron, WP-CLI, AJAX, or frontend
  • the user who was logged in (if any — blank means it wasn't a logged-in human)
  • the IP address, request URI, and user agent
  • a PHP backtrace, so you can see exactly which plugin/theme file triggered the change

Logs are written as JSON Lines (one JSON object per line) to a file in wp-content/uploads/post-change-tracker/change-log.jsonl, and can be browsed from Tools → Post Change Log in wp-admin.

Why a log file instead of the database?

  • No new DB table, no schema migrations.
  • Easy to tail -f, grep, or pipe into jq on the server if you have shell access.
  • Easy to download and share with a developer/host for diagnosis.

Installation

  1. Download post-change-tracker.php.
  2. Copy it into wp-content/plugins/ on your WordPress site (or wp-content/mu-plugins/ if you want it always-on and impossible for another plugin to accidentally deactivate).
  3. If you placed it in wp-content/plugins/, activate Post Change Tracker from the Plugins screen.
  4. Wait for the issue to happen again (or trigger it yourself), then check Tools → Post Change Log.

Reading the log

Go to Tools → Post Change Log in wp-admin. Each row shows:

Column Meaning
When Timestamp of the change
Post The post that was changed, linked to its editor
Type Post field, Taxonomy, or Custom field
Changes Which field(s) changed, old value → new value
Source What kind of request made the change (see list above)
User The logged-in user, or "(none — user_id 0)" if it wasn't a logged-in user
IP IP address of the request
Request URI The URL that was hit
Backtrace Expandable PHP call stack — look here first to identify the responsible plugin/theme/script

You can also click Download raw JSON log to get the .jsonl file directly, or read it from the server:

tail -f wp-content/uploads/post-change-tracker/change-log.jsonl
cat wp-content/uploads/post-change-tracker/change-log.jsonl | jq .

Example log lines

Each line has a type of post_fields, taxonomy, or meta:

{"time":1755000000,"date":"2026-08-13 10:00:00","type":"post_fields","post_id":42,"post_title":"Hello World","changes":[{"field":"post_date","label":"Date","old":"2026-08-01 09:00:00","new":"2026-08-13 10:00:00"}],"source":"REST API","user_id":0,"user_login":"","request_uri":"/wp-json/wp/v2/posts/42","ip":"203.0.113.5","user_agent":"SomeImportPlugin/1.0","backtrace":["wp-content/plugins/some-import-plugin/importer.php:88","wp-includes/rest-api/...","wp-includes/post.php:..."]}
{"time":1755000100,"date":"2026-08-13 10:01:40","type":"meta","post_id":42,"post_title":"Hello World","changes":[{"field":"_thumbnail_id","label":"_thumbnail_id","old":"12","new":"87"}],"source":"wp-admin","user_id":1,"user_login":"admin","request_uri":"/wp-admin/post.php?post=42&action=edit","ip":"203.0.113.9","user_agent":"Mozilla/5.0 ...","backtrace":["wp-content/plugins/some-plugin/hooks.php:40","wp-includes/post.php:..."]}

Here, the first line's backtrace immediately points at some-import-plugin as the culprit for the date change.

Security notes

  • The log directory (wp-content/uploads/post-change-tracker/) is protected with a .htaccess (Require all denied) and an empty index.php, so the raw file can't be fetched directly on Apache/LiteSpeed.
  • Nginx does not read .htaccess files. If your site runs on Nginx, add a rule to block the directory yourself, e.g.:
    location ^~ /wp-content/uploads/post-change-tracker/ {
        deny all;
        return 404;
    }
  • Only users with the manage_options capability (admins) can view or download the log or clear it from wp-admin.
  • The log file can contain IP addresses and user agents; treat it as you would any other access log.

Log rotation

The log file is automatically trimmed once it exceeds 5 MB, keeping the most recent 2000 entries.

Filters

  • pct_watched_post_fields — filter the map of core post fields tracked (field => label). Remove entries you don't care about (e.g. post_content, since page builders can make that noisy) or add your own.
  • pct_ignored_meta_keys — filter the array of post meta keys that are never logged. Defaults to a small list of WordPress-internal noise (_edit_lock, _edit_last, _wp_old_slug, _wp_old_date, _encloseme, _pingme). Add plugin-specific noisy keys (e.g. keys an SEO or page-builder plugin rewrites on every save) here.
add_filter( 'pct_ignored_meta_keys', function ( $keys ) {
    $keys[] = '_some_noisy_plugin_key';
    return $keys;
} );

Common culprits for changing post dates

If the backtrace points at WordPress core rather than a specific plugin, common causes include:

  • SEO plugins (e.g. Yoast, Rank Math) touching "last modified" on save
  • Import/migration tools (WP All Import, WordPress Importer) that don't preserve original dates
  • Scheduled publishing (wp_cron) combined with a timezone misconfiguration
  • Backup/staging-sync plugins that overwrite posts on restore
  • REST API or XML-RPC clients (mobile apps, external integrations, mis-configured app passwords)

License

GPL-2.0-or-later — see LICENSE.