Post Change Tracker
Logs changes made to posts — core fields, taxonomy terms, and custom fields/meta — to a file.
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.zipA 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 intojqon the server if you have shell access. - Easy to download and share with a developer/host for diagnosis.
Installation
- Download
post-change-tracker.php. - Copy it into
wp-content/plugins/on your WordPress site (orwp-content/mu-plugins/if you want it always-on and impossible for another plugin to accidentally deactivate). - If you placed it in
wp-content/plugins/, activate Post Change Tracker from the Plugins screen. - 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 emptyindex.php, so the raw file can't be fetched directly on Apache/LiteSpeed. - Nginx does not read
.htaccessfiles. 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_optionscapability (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.