Incident-response tools for a hacked site, contributed by @Mrshahidali420 from a real reinfection case (#160): spot the signs of a compromise, pull a suspicious file out safely, close the usual doors, find the request that brought a hack back, and stop a process that keeps rewriting files. Plus a switch to keep new tools on after updates, binary file writes, OAuth on subdirectory multisites, backups without
wp-config.php, corrected ChatGPT and Codex connection steps, and five fixes from user reports: atomic text that rendered empty on Elementor 4.3, styles writes that dropped props, writes that an open editor's Force Save threw away, a wrong row id frominsert-row, and SVG icon uploads History could not undo.
- New: scan-security looks for an active compromise (#161). A fifth area,
compromise, checks for folders named like their parent that hold their own.htaccess(the hiding place of the back door in #160), PHP files written recently but dated much older, a homepage that shows Googlebot a different page than visitors, backup archives anyone can download (EMCP's own backup folder included), must-use plugins named like regular plugins, and scheduled tasks with re-infection names and no callback. Three malware rules were added: superglobal names split into pieces, a known injector marker, and includes from compressed or archive streams. The audit follows the scan'smax_seconds, and a walk that runs out of time says which checks it only partly covered instead of reporting them clean. Run it alone withchecks: ["compromise"]. - New: Quarantine a suspicious file instead of deleting it (#162).
quarantine-filemoves a file or folder out of the site, keeps a record of its path, size, hash and dates, andrestore-quarantined-file(or Undo in History) puts it back. Stored files are renamed so no web server can run them, the folder is denied to the web for Apache and IIS, and it goes above WordPress only when that folder is not itself public. Core files,wp-admin,wp-includesand EMCP itself are refused.list-quarantinelists the entries. - New: Harden the site from your AI agent (#163).
harden-siteswitches off XML-RPC, hides the WordPress version from page source and asset links (a per-site token keeps cache busting after updates), blocks user enumeration through the REST API and?author=N(author archives keep working), and hides PHP errors from visitors. Each change can be undone from History.get-hardening-statuschecks each one the way a visitor sees the site. Turning XML-RPC off stops Jetpack and the mobile apps, and the tool says so. - New: search-access-log (#164) searches the web server access logs the site can read, plain and
.gz, by text, IP, method, status, path and time, to find the request behind a hack. It reads only from a fixed list of log folders, removes query strings from results by default, and stops at its time, size and result limits. - New: list-processes and kill-process (#165) show the processes running as the site's own user on Linux, flag watcher scripts and other suspicious ones, and stop one with confirmation. Web server, PHP worker and database processes are never stopped, because on shared hosting they also serve the account's other sites. A stopped process cannot be restarted from History.
- New: Turn on new tools after updates. A switch on the Tools screen, off by default. Updates add some new tools switched off (file, database, plugin and user writes, and Pro batches); with the switch on they arrive switched on, so a site you manage gets them without a visit. Tools you turned off stay off, a new install still starts with the safe defaults, and the setting travels with Cloud settings sync. Requested by a customer managing many client sites.
- New:
write-filewrites binary files (#173). Passencoding: "base64"to write images, fonts or ZIP files byte for byte. The content is checked strictly (whitespace and missing padding are fine, anything else is refused with nothing written), the 5 MB limit applies to the decoded size, and the bytes are decoded by a stream filter as they are written. Requested by a customer; contributed by @amiriqbalmcs. Files over 5 MB are not supported yet. - The new tools that change the site, read server logs or list processes ship switched off: enable them on the Tools screen when you need them. The compromise audit,
list-quarantineandget-hardening-statusare on. - Fixed: Atomic headings, paragraphs and buttons rendered empty on Elementor 4.3 (#166). Elementor 4.3 stores their text as
escaped-html, butadd-atomic-heading,add-atomic-paragraphandadd-atomic-buttonwrote the olderhtml-v3format, so the element showed no text until Elementor happened to convert it. The tools now write whichever format the installed Elementor declares (escaped-htmlon 4.3,html-v3on 4.2), and any write to a page converts text already stored in the wrong format, in either direction. Translations through Polylang and TranslatePress now include 4.3 headings and paragraphs too. - Fixed: A
styleswrite replaced the whole variant (#169).update-elementandbatch-updatereplaced a style variant's props wholesale, so sending onlycolordeletedfont-size. Variants are now matched by breakpoint and state and merge prop by prop: a prop you send replaces the stored one, a prop set tonullis removed, props you leave out are kept, and a new breakpoint or state is added. A class set tonullis removed. - Fixed: Writes to a page open in your own Elementor editor were lost (#168). The 3.18.0 guard refused a write only while another user had the page open with unsaved changes. When the same administrator ran both the AI connection and the editor, the write went through and the editor's Force Save then threw it away. Elementor writes are now refused whenever the page is open in the editor with unsaved changes, by any user, yours included, and the answer says what to do. If the editor was closed without saving, retry with
ignore_open_editor: trueinstead of waiting a few minutes for the editor's lock to expire. An editor that is open with nothing unsaved does not block writes. - Fixed:
insert-rowreturned the wrong id (#167). It reported the id of its own History entry instead of the new row's, so a follow-up update by that id changed nothing, or on a small table could change an unrelated row. It now returns the new row's id. Anupdate-rowsordelete-rowsthat matches no rows now says so (undoable: false, emptychange_id) and adds nothing to History. - Fixed: OAuth sign-in failed on every subsite of a subdirectory multisite (#171). A subsite's issuer is
https://host/<path>, so AI clients ask the network root for/.well-known/oauth-authorization-server/<path>, which WordPress sends to the main site, and that answered 404. The main site now answers for the subsite when the subsite is live, has EMCP active with OAuth on, and its issuer matches, even when the main site's own OAuth is off. EMCP must be active on the main site for this; otherwise the nginx rewrite from the support docs still works. Contributed by @amiriqbalmcs. - Fixed: The Connection wizard sent ChatGPT users to a menu that no longer exists (#172). Codex joined the ChatGPT desktop app and its MCP settings moved: the steps now read Settings, MCP servers, Add server, Streamable HTTP, Save, Restart, Authenticate. Contributed by @amiriqbalmcs.
- Fixed: The Codex terminal command in the Connection wizard failed.
codex mcp addhas no--transportor--headerflag, so the command was refused. It is nowcodex mcp add <name> --url <endpoint>, followed by thehttp_headersline to add under the server in~/.codex/config.toml(checked with codex-cli 0.163). - Security: Full backups no longer include
wp-config.php. The rootwp-config.phpholds the database password and the auth keys and salts, and restore never writes it (the destination keeps its own), so it only put those secrets in every backup. Only the root file is left out:wp-config-sample.phpand files of the same name inside plugins are still archived. Reported by a customer. - Fixed:
upload-svg-iconwas not recorded in History (#170). It now returns achange_idlikeupload-media, and undo removes the attachment and its file. - Changed: EMCP Tools uses the official MCP Adapter plugin when it is active. The MCP Adapter became a standalone WordPress.org plugin (
mcp-adapter) with 0.7, and its migration guide asks plugins to stop bundling it. EMCP bundled 0.6.1 and its loader forced that copy over the standalone plugin, so plugins that need 0.7 broke and WordPress reported a duplicate copy. Now the official plugin, when active, serves the whole adapter, and EMCP's own copy (updated to 0.7.1) serves when it is not. The built-in copy stays as a permanent fallback, kept up to date with each adapter release, so nothing changes for sites without the official plugin. The Connection screen offers the official plugin as an option, with one button that installs it from WordPress.org and activates it; on hosts where WordPress needs FTP details, the button opens WordPress's own installer. A notice and a Dashboard item appear only when something needs you: no adapter loads, the official plugin is too old, or another plugin's copy is in use (Not now dismisses it). AI tools can no longer deactivate or delete the MCP Adapter plugin, since that would cut their own connection. Reported by a customer. - Fixed: Cloud Safe Updates refused or could not fully restore many real sites. Reported by a customer. Readiness now checks the database the way a restore will need it (table storage engine, triggers, generated columns, row and packet sizes, and the database user's rights to recreate every table) and the free disk space for the file and database snapshot together, and names what blocks a site. Database snapshots are saved in small checked pieces instead of stopping at 20 MB or 50,000 rows per table, and a restore checks every piece before it drops a table, so a large site can be restored in short steps even on hosts with low memory or time limits. Values that used to come back slightly changed now restore exactly: BIT, FLOAT, ENUM and SET columns and MySQL 8's invisible primary keys. A rollback also removes tables and triggers that the failed update added. WP Rocket and 10Web Booster page caches are supported: their cached pages are cleared before the update and again after it, so visitors never get a page cached from a broken update, and other caching drop-ins are still refused. MySQL older than 5.7.8 and MariaDB older than 10.1 are refused, because they cannot limit how long the checks run. Jobs that were already prepared keep the version they started with.
- Fixed: WebP copies were no smaller than the JPEGs they replaced. Image Optimization encoded each
.webpfrom the JPEG it had just compressed, and WordPress reset the quality to its own WebP default (86) whenever the format changed, so the Quality setting never reached WebP. WebP is now encoded from the untouched original (theemcp-originalsbackup, or the upload before it is compressed) at its own quality: a new WebP quality setting, automatic by default at 15 below Quality. On sample photos WebP is now 15% smaller than the compressed JPEG and keeps more detail, where before it was larger. A WebP copy that is still not smaller is not kept, and the bulk optimizer replaces copies left by earlier versions. The size cap andresize-mediaalso saved at WordPress's default quality instead of yours; they now use the setting. - Fixed: Cached pages served JPEG or WebP to everyone depending on who came first. WebP was chosen per request from the browser's
Acceptheader, but Elementor caches rendered widgets and writes background image URLs into its CSS files, so the first visitor decided for everyone until the cache was rebuilt, and a crawler could pin JPEG for all. With Serve WebP on the site on, every visitor now gets WebP where a copy exists. Elementor's element cache and CSS are flushed when the module is turned on or off, when either WebP setting changes, when a bulk run finishes and after a restore.