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

Post Change Tracker

Logs changes made to posts — core fields, taxonomy terms, and custom fields/meta — to a file.

by Neev Alex · github.com/neevalex/post-change-tracker

0stars
0forks

Install

No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:

wp plugin install https://github.com/neevalex/post-change-tracker/archive/refs/heads/main.zip

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.