Patchmark is a WordPress plugin for the moment just before you replace a plugin or theme with a new version. When you upload a plugin or theme that is already installed, WordPress shows a five-row table and a Replace current with uploaded button. Patchmark adds what that table leaves out: every file that changed, line by line; anything in the new code that deserves a second look; the changelog between the two versions; and the functions, hooks and routes the upload adds or removes.
It is built for anyone who updates plugins by hand: premium plugins delivered as zips, client sites where a vendor emails "the fixed version", your own plugins between releases, or a zip from a forum that you would rather read before you run.
The same review also covers:
- Updates from WordPress.org: a Review the changes first link in every update row on the Plugins screen downloads the update and compares it with what you have, with Update in the top bar.
- Integrity: Tools › Patchmark checks installed plugins against WordPress.org's checksums and shows what changed since installation, file by file.
- WP-CLI:
wp patchmark diff | update | check, with table, JSON or Markdown output and a failing exit status for CI.
Reviews export as an HTML report, Markdown or a .patch, and there's a dark theme. To see it without a zip, Tools › Patchmark › See a sample review builds a review of a made-up plugin's update (it's also the landing page of the Playground blueprint behind the Live Preview button on WordPress.org).
Requires WordPress 6.6+ and PHP 7.4+. Free and GPL. No settings and nothing stored; WordPress.org is contacted only when you ask for an update review or an integrity check.
- What it shows
- Updates, integrity and WP-CLI
- How it works
- Limits
- Install
- For developers
- Development
- Brand
- Credits
- License
Patchmark takes over the "already installed" screen. Its top bar carries the brand, the zip you are reviewing, and WordPress's own Cancel and go back and Replace current with uploaded buttons. When the review has serious findings, Replace turns red and takes a second click. If WordPress refuses the replacement (PHP or WordPress too old), its reasons are shown at the top and Replace is disabled. Below the bar, a header gives the plugin, the version change (upgrade, downgrade or same version), a verdict and how many files changed. Four tabs hold the rest.
WordPress's heading, progress lines, comparison table and buttons are hidden, not removed: the script switches the layout on, so without JavaScript the regular screen (with the extra header rows) still works. Notices from other plugins stay visible.
Findings, most serious first, followed by the full plugin header side by side (everything WordPress's table showed, plus description, URIs, Update URI, text domain and required plugins, with changed fields marked) and WordPress's reminder to back up first. Each finding says what was found, why it matters, and links to the file and line, which opens in the file browser with the line highlighted.
Header and environment
| Finding | Level |
|---|---|
| The upload sets a new Update URI: future updates would come from that address instead of WordPress.org | Serious |
| The plugin name is different (possibly a different plugin using the same folder) | Serious |
| The main file is renamed while the plugin is active (WordPress would deactivate it) | Serious |
| Needs a PHP or WordPress version this site doesn't have | Serious |
| Older version than the one installed | Check |
| Same version number, different files | Check |
| Different author | Check |
| Requires Plugins lists plugins that aren't active | Check |
| Becomes network-only | Check |
| Minimum PHP raised, text domain changed, Plugin or Author URI moved to another host, readme Stable tag doesn't match Version | Note |
Files
| Finding | Level |
|---|---|
Adds or changes .htaccess, .user.ini, php.ini or web.config |
Serious |
PHP with unusual extensions (.phtml, .phar, .php7) or double extensions (logo.jpg.php) |
Serious |
| PHP in folders meant for images, fonts, styles, uploads or languages | Check |
Executables and scripts (.exe, .so, .sh, .bat, .ps1, .jar) |
Check |
| Almost nothing shared with the installed copy (a rewrite, or a different plugin) | Check |
| Bundled archives, new hidden files | Note |
Code that is new in the upload
| Finding | Level |
|---|---|
eval(), and the decode-and-run combination with base64_decode() and friends |
Serious |
Shell commands: exec(), shell_exec(), system(), passthru(), proc_open(), popen(), backticks |
Serious |
include / require of a path taken from $_GET, $_POST, $_REQUEST or $_COOKIE |
Serious |
unserialize() of request data |
Serious |
| Long base64 strings | Serious |
create_function(), assert() |
Check |
| Creating users, adding roles, setting login cookies | Check |
Decoding: base64_decode(), gzinflate(), str_rot13(), hex2bin() |
Check |
Names spelled in \x65\x76… escapes or chr(101).chr(118)… chains |
Serious |
preg_replace() with the /e modifier |
Serious |
| Minified PHP (thousands of characters and dozens of statements on one line) | Check |
add_cap(): roles get new capabilities |
Check |
navigator.sendBeacon() in JavaScript |
Check |
| New web addresses in the code | Check or note |
| New outgoing requests, new file writes | Note |
| New scheduled events, database tables and admin screens | Note |
Themes
| Finding | Level |
|---|---|
The new version names a parent theme (Template) that isn't installed |
Serious |
| The parent theme changes | Check |
| It's the active theme, or the parent of the active theme | Note |
- Folder tree of every added, removed, modified, moved and whitespace-only file, with line counts. Single-child folders are compacted (
vendor/composer). - Split or unified diffs with syntax highlighting and word-level highlights on top of it, unchanged runs folded behind "Show 12 unchanged lines", and an Ignore whitespace switch. Your layout choice is remembered.
- Moves are detected: a removed file whose exact contents appear under a new name is shown once, as moved.
- Whitespace-only changes (line endings, trailing spaces) are told apart from real ones.
- Images are shown side by side, installed and uploaded, on a checkerboard.
- Filter by file name or status, or search inside the changed files.
jandkstep through files. - Download .patch: a git-style patch of the text changes.
git applyturns the installed copy into the upload, line endings and missing final newlines included. Binary files are listed at the top of the patch.
The entries between the installed and the uploaded version, read from the upload's readme.txt (= 1.2.0 = headings) or CHANGELOG.md (## [1.2.0] - 2026-10-20, with ### Added groups). The Upgrade Notice for the new version is shown on top. For a downgrade, the entries come from the installed copy: the changes you would give up. code, bold and links are rendered; nothing else from the readme is.
What the upload adds and removes, read from the PHP with the tokenizer:
- functions, classes, interfaces, traits and enums (with namespaces)
- public methods (private and protected ones are left out; methods of a removed class are covered by the class)
- hooks the plugin fires (
do_action,apply_filtersand their variants; dynamic names are shown as written,save_{$post_type}) - REST routes, shortcodes, post types and blocks
- scheduled events (
wp_schedule_event,wp_schedule_single_event), admin screens (add_menu_pageand friends, by slug), database tables (CREATE TABLE, with the$wpdb->prefixpart left out), options (add_option,update_option) and capabilities (add_cap) - Composer packages in
vendor/composer/installed.jsonorcomposer.lock: added, removed and updated, with versions
Removed names are the ones that break a theme, a snippet or another plugin that relies on them.
Update reviews. On the Plugins screen, every plugin with an update gets Review the changes first next to "update now". It opens Tools › Patchmark with the plugin named in the address; the page asks the server (POST patchmark/v1/review) to download the package WordPress already found, unpack it to a temporary folder, build the review against the installed copy and delete the folder. The top bar then has Update to x.y (WordPress's own update link) and Back to Plugins. Premium plugins whose update has no package (usually no license) get no link.
Integrity check. Tools › Patchmark lists installed plugins. Check all plugins asks for one at a time (POST patchmark/v1/integrity): the server downloads downloads.wordpress.org/plugin-checksums/<slug>/<version>.json and compares every file's MD5 (or SHA-256) with it. Files that differ, files WordPress.org's copy doesn't have, and files that are missing are listed. See what changed downloads WordPress.org's zip of the same version and opens the regular review with WordPress.org on the left and your copy on the right. Plugins that aren't on WordPress.org, or at a version it no longer has, show as "can't check".
WP-CLI.
# Each side is a folder, a zip, or an installed plugin or theme folder name.
wp patchmark diff woocommerce ~/woocommerce.9.4.0.zip
wp patchmark diff ./release/1.4 ./release/1.5.zip --format=markdown > review.md
# The update WordPress found for a plugin.
wp patchmark update akismet --fail-on=warn
# Checksums, and a Markdown review with a unified diff for each changed plugin.
wp patchmark check --all --diff--format is table (default), json (the full report, without file contents) or markdown. diff and update exit with status 1 when there are serious findings (--fail-on=warn includes "check" findings, --fail-on=none never fails); check exits with 1 when a plugin was changed.
WordPress unpacks an uploaded zip into wp-content/upgrade/, runs upgrader_source_selection, notices that the plugin folder already exists, deletes the unpacked files, and then shows the comparison screen. So there is one short window where both copies are on disk.
Patchmark hooks upgrader_source_selection at priority 999 (after core's own package check and after plugins that rename GitHub-style folders) and does all its work there:
- List both folders and hash every file with
md5_file(), without loading it. Version-control and editor clutter (.git,node_modules,.DS_Store,__MACOSX) is skipped. - Compare by hash. Only files whose hash differs are read: to tell whitespace-only changes from real ones, to scan the code, and to send the contents to the viewer.
- Scan both versions of each changed file. Findings are reported only when a signal appears more often in the upload than in the installed copy of the same file, so a plugin that has always used
base64_decode()isn't flagged for it. - Hand over the review through
install_plugin_overwrite_comparison. The data travels as JSON with the script, which prints in the footer; nothing is written to the database.
The viewer is a small React app (@wordpress/element, already loaded in wp-admin). Diffs are computed in the browser with Myers' algorithm on lines, then on words for paired lines, so the page carries the two texts once instead of pre-rendered HTML for every file.
If anything in the review throws, the upload screen shows a one-line notice and WordPress carries on as usual. The review never stands between you and the upload.
Theme uploads work the same way, through install_theme_overwrite_comparison.
Nothing runs on the front end. The stylesheet loads only on the upload screens and Tools › Patchmark, and the script only when there is something to show. The light or dark choice is the only thing saved, as the patchmark_theme user meta.
| Files per package | 20,000 (the rest are not compared, and the review says so) |
| File contents shown in the viewer | up to 512 KB per file, 8 MB of text in total, modified files first |
| Image previews | up to 256 KB per image |
| Code scanned | up to 2 MB per file, 40 MB in total per side |
| Changelog entries shown | 25 |
Files over a limit are still compared by hash and listed. Formidable Forms (1,841 files, 29 MB) with 46 changed files is compared and scanned in about 0.4 seconds.
Single-file plugins (a lone .php in wp-content/plugins) aren't covered. Update reviews and the integrity check are for plugins; themes are reviewed when you upload a zip.
The code checks point at changes worth reading. They are not a malware scanner, they can't prove a plugin is safe, and some findings are normal for some plugins (a membership plugin creates users). Only install code from sources you trust.
From a release zip: Plugins › Add New › Upload Plugin, choose patchmark-<version>.zip, activate.
From source: see Development.
Then upload any plugin or theme you already have installed, or click Review the changes first next to a plugin update. The integrity check is under Tools › Patchmark.
Filters
patchmark_ignore changes which file and folder names are left out of the comparison.
add_filter( 'patchmark_ignore', function ( $names ) {
$names[] = 'tests';
return $names;
} );REST routes (all under patchmark/v1, for users who can update_plugins, except preferences):
| Route | Does |
|---|---|
POST /review {plugin, source: update|original} |
Downloads, compares and returns a review |
POST /integrity {plugin} |
Checks one plugin against WordPress.org's checksums |
POST /preferences {theme: light|dark} |
Saves the viewer theme for the current user |
Translations. languages/patchmark.pot is generated with wp i18n make-pot . languages/patchmark.pot --exclude=node_modules,app,tests,scripts,languages (the unminified build/index.js carries the viewer's strings). Translations are delivered by translate.wordpress.org.
Engine checks
tests/check.php builds an installed and an uploaded copy of a small plugin in a temporary folder (the upload with planted problems), runs the review and checks 42 results: counts, moves, whitespace, every finding type, API changes, changelog ranges, downgrades, identical copies, themes, the integrity labels and checksum comparison, and the Markdown and unified diff WP-CLI prints. The suspicious code is written at test time, so it never sits in the repository as PHP.
wp eval-file tests/check.phpThe admin app is TypeScript and SCSS, built with @wordpress/scripts, the same setup as Routemark and Unified Blocklist. The build is not minified, so build/ is readable as it ships.
npm install
npm start # watch build while developing
npm run typecheck
npm run lint:js
npm run zip # build + dist/patchmark-<version>.zipPHP follows the WordPress Coding Standards; phpcs.xml.dist is included.
| Folder | What's in it |
|---|---|
src/ |
PHP: capture and screen (Plugin), file listing (Package), comparison (Comparison), findings and API changes (Review), tokenizer scan (CodeScan), changelog (Changelog) |
app/ |
The viewer: React + TypeScript, with the diff and patch code in app/lib |
build/ |
Compiled viewer (generated, not committed) |
tests/ |
Engine checks |
licenses/ |
Font licenses (Bricolage Grotesque, Atkinson Hyperlegible Next and Mono, OFL) |
.wordpress-org/ |
Icon, banners and screenshots for the plugin directory |
The GitHub workflow typechecks, lints, runs PHPCS, builds, and attaches the plugin zip to every push.
Patchmark is a CC Software plugin, a sibling of Routemark and Unified Blocklist, and uses the same look. The name is one word with a capital P. In the wordmark, "Patch" is in the text color and "mark" is in blue, as in Routemark.
The mark is the CC Software hex well with a diff inside it: a removed line in red, an added line in green that ends in Routemark's marker, and an unchanged line in light blue. The well is always graphite, so the same mark works on light and dark backgrounds.
Blue #3B82F6 |
Outer hex |
Graphite #16181B |
Hex well, banner background |
Dark blue #1E40AF |
"mark" on light backgrounds, marker center |
Light blue #93C5FD |
Unchanged line, "mark" on dark backgrounds |
Red #F87171 |
Removed line |
Green #4ADE80 |
Added line |
The review sits inside a core WordPress screen, so, like Routemark's light theme, it has no palette of its own: wp-admin's grays and the user's admin color scheme (--wp-admin-theme-color), with the usual green and red for additions and removals. A brand bar with the mark, wordmark and version tops the review, as Routemark's top bar does.
The wordmark is set in Bricolage Grotesque. Everything else uses Atkinson Hyperlegible Next, and code uses Atkinson Hyperlegible Mono, which keeps l, I, 1, O and 0 apart; that matters when a change is one character. All three are bundled under the OFL.
Patchmark was called Upload Diff during development. Same plugin, new name.
Bricolage Grotesque (Copyright 2022 The Bricolage Grotesque Project Authors) and Atkinson Hyperlegible Next and Mono (Copyright 2020-2024 The Atkinson Hyperlegible Next / Mono Project Authors), all under the SIL Open Font License 1.1. Bundled in build/fonts, licenses in licenses/. Served from the plugin folder, never from Google.
Syntax highlighting is Prism (MIT), bundled. Each version of a file is highlighted as a whole, so comments and strings that span several lines are colored correctly, and the colors are then merged with the word-level change marks. Files over 400 KB, or with lines over 3,000 characters (minified bundles), are shown without colors.
The line diff is Eugene W. Myers' O(ND) algorithm ("An O(ND) Difference Algorithm and Its Variations", 1986).
GPL-2.0-or-later. See LICENSE.
Made by CC Software.




