<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Susumu Takahashi</title>
    <description>The latest articles on DEV Community by Susumu Takahashi (@susumun).</description>
    <link>https://dev.to/susumun</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3961116%2F87a59747-8eb8-43eb-9db6-c160d3592934.JPG</url>
      <title>DEV Community: Susumu Takahashi</title>
      <link>https://dev.to/susumun</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/susumun"/>
    <language>en</language>
    <item>
      <title>"not a valid OPENSSH private key file" — building a compat layer for seven SSH private-key formats</title>
      <dc:creator>Susumu Takahashi</dc:creator>
      <pubDate>Sat, 20 Jun 2026 02:29:48 +0000</pubDate>
      <link>https://dev.to/susumun/not-a-valid-openssh-private-key-file-building-a-compat-layer-for-seven-ssh-private-key-formats-fl8</link>
      <guid>https://dev.to/susumun/not-a-valid-openssh-private-key-file-building-a-compat-layer-for-seven-ssh-private-key-formats-fl8</guid>
      <description>&lt;p&gt;If you wire SSH into WordPress maintenance automation, you'll meet this error sooner or later:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SSHException: not a valid OPENSSH private key file
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"I configured the key file — why?" is the usual reaction. Tracing several real SSH-connection-test failures, the root issue becomes clear: &lt;strong&gt;&lt;code&gt;paramiko&lt;/code&gt; alone can't read SSH private keys outside the OpenSSH format&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In production, hosting providers and key-generation tools produce different formats, and paramiko's standard loader rejects many of them. Here's the design of a "seven-format compat loader" we built to handle this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Far more SSH key formats than you'd guess
&lt;/h2&gt;

&lt;p&gt;"SSH key" usually conjures up &lt;code&gt;-----BEGIN OPENSSH PRIVATE KEY-----&lt;/code&gt;, but in practice the keys you receive fall into:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;OpenSSH new format&lt;/strong&gt; (&lt;code&gt;-----BEGIN OPENSSH PRIVATE KEY-----&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PKCS#1 RSA&lt;/strong&gt; (&lt;code&gt;-----BEGIN RSA PRIVATE KEY-----&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SEC 1 EC&lt;/strong&gt; (&lt;code&gt;-----BEGIN EC PRIVATE KEY-----&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PKCS#8 plain&lt;/strong&gt; (&lt;code&gt;-----BEGIN PRIVATE KEY-----&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PKCS#8 encrypted&lt;/strong&gt; (&lt;code&gt;-----BEGIN ENCRYPTED PRIVATE KEY-----&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Legacy PEM encrypted&lt;/strong&gt; (&lt;code&gt;-----BEGIN RSA PRIVATE KEY-----&lt;/code&gt; + &lt;code&gt;Proc-Type: 4,ENCRYPTED&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PuTTY .ppk&lt;/strong&gt; (v2 / v3 — common from Windows users)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;paramiko.from_private_key_file()&lt;/code&gt; handles OpenSSH and PKCS#1, but &lt;strong&gt;PKCS#8 and .ppk are out of scope&lt;/strong&gt;. For instance, Sakura Internet's control-panel option to "generate and register a key pair" currently produces ECDSA + PKCS#8 — and paramiko's regex rejects it outright.&lt;/p&gt;

&lt;h2&gt;
  
  
  The approach — let &lt;code&gt;cryptography&lt;/code&gt; pre-read it, then re-serialize as OpenSSH
&lt;/h2&gt;

&lt;p&gt;Extending paramiko directly is hard, so the compat loader takes a &lt;strong&gt;detect → normalize → hand off to paramiko&lt;/strong&gt; approach. We added &lt;code&gt;core/ssh_key_loader.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;load_any_ssh_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;passphrase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;paramiko&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Load one of seven SSH private-key formats and normalize to paramiko.PKey.
    Supported: OpenSSH / PKCS#1 RSA / SEC 1 EC / PKCS#8 plain /
               PKCS#8 encrypted / legacy PEM encrypted / PuTTY .ppk (v2 / v3)
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;fmt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_detect_format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;pem_openssh&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_to_openssh_pem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;passphrase&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;paramiko&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RSAKey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_private_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;BytesIO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pem_openssh&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;_detect_format()&lt;/code&gt; looks at the first bytes, PEM headers, and the PuTTY-specific &lt;code&gt;PuTTY-User-Key-File-2/3:&lt;/code&gt; line to identify which of the seven formats it is. After detection, the &lt;strong&gt;&lt;code&gt;cryptography&lt;/code&gt; library reads the key object and re-serializes it as an OpenSSH-compatible PEM&lt;/strong&gt;, which then gets handed to paramiko. From paramiko's perspective, it's always "the OpenSSH format I know."&lt;/p&gt;

&lt;h2&gt;
  
  
  PuTTY .ppk — a hand-rolled parser, no extra dependency
&lt;/h2&gt;

&lt;p&gt;PuTTY's &lt;code&gt;.ppk&lt;/code&gt; is supported by neither paramiko nor cryptography, so it gets a &lt;strong&gt;dependency-free, hand-rolled parser&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;v2&lt;/strong&gt;: SHA1 + HMAC-SHA1 authentication; base64-encoded public key plus an encrypted private-key section that needs decryption&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;v3&lt;/strong&gt;: Argon2id + HMAC-SHA256 (a newer KDF); different passphrase handling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;.ppk&lt;/code&gt; parser is about 200 lines on its own, but pulling in something like &lt;code&gt;pyppk&lt;/code&gt; would add binary size and a separate compatibility surface to maintain. A hand-rolled parser turned out lighter operationally.&lt;/p&gt;

&lt;h2&gt;
  
  
  Designing the "unknown format" error
&lt;/h2&gt;

&lt;p&gt;When &lt;code&gt;_detect_format()&lt;/code&gt; matches none of the seven, the original error was a vague "format unknown" — leaving the user with no idea what to do.&lt;/p&gt;

&lt;p&gt;Alongside the seven-format support, the error message itself was rewritten:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Could not identify the format of the key file.
Accepted formats: OpenSSH / PKCS#1 RSA / SEC 1 EC /
                  PKCS#8 / PuTTY .ppk
First bytes detected: &amp;lt;hex dump&amp;gt;
Recommended next step: regenerate an OpenSSH-format key with
  `ssh-keygen -t rsa -f new_key`, then re-register the public key
  on the server.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A consistent pattern: &lt;strong&gt;what arrived, what is accepted, and what to do next&lt;/strong&gt; — three pieces in every failure case. Error messages that only say "something happened" are the biggest source of support tickets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway — library constraints can be absorbed in a layer just in front
&lt;/h2&gt;

&lt;p&gt;paramiko's narrow format support was solved by inserting a classic &lt;strong&gt;detect-and-normalize layer in front&lt;/strong&gt; of it. Seven formats now look like "the same old OpenSSH key" from the app's side. The change is smaller and the regression tests are simpler than patching paramiko itself would have been.&lt;/p&gt;

&lt;p&gt;The error-message pattern — "what arrived, what's accepted, what to do next" — is something we want to spread well beyond SSH. &lt;strong&gt;A library's constraints disappear from the user's view if you slip a single absorbing layer in front of it.&lt;/strong&gt; That's the lesson worth recording from this round.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>php</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Renaming a site without losing its data — separating display name from a stable identifier</title>
      <dc:creator>Susumu Takahashi</dc:creator>
      <pubDate>Thu, 18 Jun 2026 23:08:47 +0000</pubDate>
      <link>https://dev.to/susumun/renaming-a-site-without-losing-its-data-separating-display-name-from-a-stable-identifier-gpb</link>
      <guid>https://dev.to/susumun/renaming-a-site-without-losing-its-data-separating-display-name-from-a-stable-identifier-gpb</guid>
      <description>&lt;p&gt;A client asks you to rename a site from &lt;code&gt;acme-staging&lt;/code&gt; to the production name &lt;code&gt;acme&lt;/code&gt;. The moment you rename it in the app, &lt;strong&gt;the DB backups, screenshots, and thumbnails you had been collecting all appear to disappear&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The files are still on disk, but the new directory is empty. &lt;strong&gt;The data hasn't carried over as "the same site."&lt;/strong&gt; It's a trap you can fall into on day one, and we did — with our original design.&lt;/p&gt;

&lt;p&gt;Here's how we redesigned things so renames don't orphan data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the data appears to disappear — the site name was the key
&lt;/h2&gt;

&lt;p&gt;The original design of WP Maintenance Manager decided file locations &lt;strong&gt;based on the site name&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;backups/
  acme-staging/        ← DB backups for site "acme-staging"
    backup_20260101_120000.sql

screenshots/
  acme-staging/        ← Screenshots for the same site
    home_pre.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After renaming &lt;code&gt;acme-staging&lt;/code&gt; → &lt;code&gt;acme&lt;/code&gt;, &lt;strong&gt;a new empty directory &lt;code&gt;backups/acme/&lt;/code&gt; gets created and starts from zero&lt;/strong&gt;. The old directory is still there, but the app treats it as "some other site's stale data" and doesn't surface it.&lt;/p&gt;

&lt;p&gt;Site names are natural candidates for labels, but &lt;strong&gt;in practice they get renamed all the time&lt;/strong&gt;. Cleaning up client-name typos, promoting staging to production, renaming on a re-org — the reasons to rename are endless.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix — give every site an immutable &lt;code&gt;site_id&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Every site now carries a &lt;code&gt;_id&lt;/code&gt; in the form &lt;strong&gt;&lt;code&gt;site_xxxxxxxxxxxx&lt;/code&gt;&lt;/strong&gt; (a UUID, 12 hex chars), and every file location now keys off that &lt;code&gt;_id&lt;/code&gt; instead of the site name.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# core/site_id_utils.py
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generate_site_id&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;site_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nb"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;_id&lt;/code&gt; is &lt;strong&gt;assigned once and never changes&lt;/strong&gt;. Even if the site name is renamed, the file location stays the same &lt;code&gt;backups/site_a1b2c3d4e5f6/&lt;/code&gt; directory — and the existing contents are still in use.&lt;/p&gt;

&lt;p&gt;It's a classic two-layer design: the display name (site name) is separate from the internal identifier (&lt;code&gt;_id&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  A migration that doesn't break existing data
&lt;/h2&gt;

&lt;p&gt;The hardest part was handling &lt;strong&gt;existing users whose data was already keyed by site name&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ensure_site_ids()&lt;/code&gt; is an idempotent migration:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Auto-generates and assigns &lt;code&gt;_id&lt;/code&gt; only to sites that don't have one&lt;/li&gt;
&lt;li&gt;Leaves sites that already have a &lt;code&gt;_id&lt;/code&gt; untouched&lt;/li&gt;
&lt;li&gt;Uses &lt;code&gt;FileLock&lt;/code&gt; + tempfile + &lt;code&gt;os.replace()&lt;/code&gt; for atomic writes, so a crash mid-write won't corrupt anything&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It runs at app startup and at the entry points of site-related APIs (three paths in total). &lt;strong&gt;The user doesn't have to do anything&lt;/strong&gt; — IDs are silently assigned in the background.&lt;/p&gt;

&lt;p&gt;The file-side migration follows the same pattern. On first launch, if &lt;code&gt;backups/&amp;lt;site_name&amp;gt;/&lt;/code&gt; exists, rename it to &lt;code&gt;backups/&amp;lt;site_id&amp;gt;/&lt;/code&gt; (but if the new-format directory already exists, leave both alone). Idempotent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tying logs to sites — strict + compat hybrid matching
&lt;/h2&gt;

&lt;p&gt;Log entries also carry a &lt;code&gt;site_id&lt;/code&gt; now. But &lt;strong&gt;existing log entries don't have one&lt;/strong&gt; — they were written before the rename.&lt;/p&gt;

&lt;p&gt;The UI scoping feature (filter logs for a specific site) is implemented as a hybrid:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;New logs (with &lt;code&gt;site_id&lt;/code&gt;) → match by &lt;strong&gt;strict equality&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Old logs (without &lt;code&gt;site_id&lt;/code&gt;) → fall back to &lt;strong&gt;&lt;code&gt;site_name&lt;/code&gt; compat matching&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result: logs from before and after a rename appear together in the same scope. The user never feels like "past history disappeared."&lt;/p&gt;

&lt;h2&gt;
  
  
  A post-release blunder
&lt;/h2&gt;

&lt;p&gt;For honesty: shortly after release, we shipped a bug. The &lt;code&gt;is_valid_site_id&lt;/code&gt; validation function had a regex that &lt;strong&gt;only matched the new-generation format&lt;/strong&gt;, and rejected some legitimate existing IDs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# the broken version
&lt;/span&gt;&lt;span class="n"&gt;SITE_ID_RE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;^site_[0-9a-f]{12}$&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# exactly 12 hex
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few longer ID formats — leftovers from the migration's earlier iterations — got rejected outright, and the symptom was "every site has disappeared." The lesson is mundane but real: &lt;strong&gt;fully audit existing data formats before tightening validation&lt;/strong&gt;. Adding validation after the fact is exactly where these regressions hide.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway — separating stable identifier from display name
&lt;/h2&gt;

&lt;p&gt;Separating "the name displayed to humans" from "the immutable identifier" is a classic software-design pattern, but &lt;strong&gt;introducing it after the product is already in production is expensive&lt;/strong&gt;. The idempotent migration, the edit-vs-duplicate ownership split, the backward-compatible validation — drop any one of these and existing user data evaporates.&lt;/p&gt;

&lt;p&gt;Since separating site name (display) from &lt;code&gt;site_id&lt;/code&gt; (immutable), clients can have their site names corrected, staging promoted to production, or org-rename refactoring done — all while &lt;strong&gt;keeping every byte of historical data tied to the same site&lt;/strong&gt;. Designing your file locations to trust the display name 100% on day one closes that door before you even reach for it. That's the retrospective on this one.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>php</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Beyond "Update All" — selecting plugins across sites while keeping every safety mechanism on</title>
      <dc:creator>Susumu Takahashi</dc:creator>
      <pubDate>Thu, 18 Jun 2026 01:04:25 +0000</pubDate>
      <link>https://dev.to/susumun/beyond-update-all-selecting-plugins-across-sites-while-keeping-every-safety-mechanism-on-8a</link>
      <guid>https://dev.to/susumun/beyond-update-all-selecting-plugins-across-sites-while-keeping-every-safety-mechanism-on-8a</guid>
      <description>&lt;p&gt;"A vulnerability in Elementor was just disclosed. Several of the sites I maintain run Elementor. &lt;strong&gt;I need to update only Elementor across those sites today.&lt;/strong&gt;"&lt;/p&gt;

&lt;p&gt;If you maintain WordPress sites for multiple clients, this scenario happens several times a month. Core updates and other plugin updates can wait for the regular maintenance run, but &lt;strong&gt;a specific plugin needs to land urgently&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The trouble is that mainstream maintenance tools don't really offer a UI for "&lt;strong&gt;a targeted update across multiple sites, with safety mechanisms intact.&lt;/strong&gt;" Here's how that design problem can be solved.&lt;/p&gt;

&lt;h2&gt;
  
  
  The limits of an industry-standard "Update All"
&lt;/h2&gt;

&lt;p&gt;Most WordPress maintenance tools have an "Update All" button on each site's dashboard.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Press it, and every plugin update fires at once&lt;/li&gt;
&lt;li&gt;If any one of them breaks the site, the industry-standard "Safe Updates" / "Atomic Updates" feature &lt;strong&gt;rolls everything back&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;There's no granularity for "update just Elementor" or "across only this subset of sites"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The structural reasons the industry converged on this design are covered in &lt;a href="https://en.wpmm.jp/blog/wordpress-maintenance-industry-gaps/" rel="noopener noreferrer"&gt;three gaps the WordPress maintenance industry still hasn't solved&lt;/a&gt; — the Worker-plugin-over-HTTP-API architecture constrains how fine-grained the operations can be.&lt;/p&gt;

&lt;p&gt;The practical result: an agency task like "update only Elementor across these sites" tends to fall back to &lt;strong&gt;logging into each site one by one and updating manually&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What was needed was two-axis granularity (site × plugin)
&lt;/h2&gt;

&lt;p&gt;WP Maintenance Manager v1.6.2 solves this with &lt;strong&gt;two axes of granularity&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Site axis&lt;/strong&gt;: see, across every managed site, which plugins are due for update&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plugin axis&lt;/strong&gt;: pick the (site × plugin) cells with checkboxes — only those combinations are updated&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of an "Update All" button, you get a "&lt;strong&gt;update only the combinations you picked&lt;/strong&gt;" button.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cross-site dashboard design
&lt;/h2&gt;

&lt;p&gt;Opening "Plugin Updates" from the toolbar triggers an SSH-parallel scan across every managed site, surfacing all plugins that are due for update.&lt;/p&gt;

&lt;p&gt;Results are grouped by plugin name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Elementor — needs update on several sites (4.0.0 → 4.0.8)
  ☐ siteA.com
  ☐ siteB.com
  ☐ siteC.com

Yoast SEO — needs update on several sites (22.0 → 22.1)
  ☐ ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At a glance, you can see &lt;strong&gt;which plugins are out of date on which sites&lt;/strong&gt;. Looking at the full list of sites, you can make decisions like "update Elementor everywhere, defer Yoast for another window."&lt;/p&gt;

&lt;p&gt;Results are cached in the browser and surface immediately even after restarting the app. The parallel SSH scan completes quickly even with a large fleet, so it's lightweight enough to check casually.&lt;/p&gt;

&lt;h2&gt;
  
  
  A selective update runs with the same safety mechanisms as regular maintenance
&lt;/h2&gt;

&lt;p&gt;This is the key point. When you press "&lt;strong&gt;Update selected plugins&lt;/strong&gt;," the underlying execution uses &lt;strong&gt;exactly the same safety mechanisms as a regular maintenance run&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Database backup (taken before any update)&lt;/li&gt;
&lt;li&gt;One-plugin-at-a-time updates with HTTP checks + automatic pinpoint rollback if something breaks (&lt;a href="https://en.wpmm.jp/blog/wp-cli-pinpoint-rollback/" rel="noopener noreferrer"&gt;implementation deep dive&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Visual check and summary email&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The only difference from a regular maintenance run is that &lt;strong&gt;Core / theme / translation updates are skipped&lt;/strong&gt;. Only the plugin combinations you picked actually run.&lt;/p&gt;

&lt;p&gt;The log history and report carry a "selective update" tag, and the names of the targeted plugins are recorded. You can trace back later: "when did we push Elementor across multiple sites last month, and to which sites?" The audit trail is preserved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway — granularity changes how operations feel
&lt;/h2&gt;

&lt;p&gt;The simplicity of "Update All" is convenient, but it doesn't fit the realistic need to &lt;strong&gt;move only a specific plugin across multiple sites simultaneously&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The combination of &lt;em&gt;cross-site dashboard&lt;/em&gt; + &lt;em&gt;selective update&lt;/em&gt; + &lt;em&gt;safety mechanisms preserved&lt;/em&gt; is what makes "a targeted, safe, multi-site update" actually work. When a single plugin's vulnerability gets disclosed, you can update just the affected sites in one action — safely.&lt;/p&gt;

&lt;p&gt;What used to be a binary choice between "update everything" and "do nothing" becomes &lt;strong&gt;surgical movement with all the safety nets still on&lt;/strong&gt;. That shifts where the line falls between emergency response and routine maintenance.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>php</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Distributing a Python desktop app on Windows and Mac — the full release pipeline</title>
      <dc:creator>Susumu Takahashi</dc:creator>
      <pubDate>Wed, 17 Jun 2026 00:27:06 +0000</pubDate>
      <link>https://dev.to/susumun/distributing-a-python-desktop-app-on-windows-and-mac-the-full-release-pipeline-1n4</link>
      <guid>https://dev.to/susumun/distributing-a-python-desktop-app-on-windows-and-mac-the-full-release-pipeline-1n4</guid>
      <description>&lt;p&gt;WP Maintenance Manager ships from a single Python codebase to both Windows and macOS. "Python is cross-platform — write once, run anywhere," the saying goes. The reality is that &lt;strong&gt;the distribution pipeline is completely separate per OS&lt;/strong&gt;, each with its own pitfalls.&lt;/p&gt;

&lt;p&gt;PyInstaller / Inno Setup / Apple Notarization / eSigner — the release cycle is a combination of OS-specific toolchains. Here's the full picture, plus what to watch out for at each step. (The choice of internal architecture, Flask + browser UI, is covered separately in &lt;a href="https://en.wpmm.jp/blog/local-flask-desktop-architecture/" rel="noopener noreferrer"&gt;why we built a desktop app on local Flask + browser UI&lt;/a&gt;; this post is about distributing that architecture across two operating systems.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The per-OS pipeline at a glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Mac&lt;/th&gt;
&lt;th&gt;Windows&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Build&lt;/td&gt;
&lt;td&gt;PyInstaller (&lt;code&gt;--target-arch x86_64&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;PyInstaller&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Distribution format&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.app&lt;/code&gt; bundle → &lt;code&gt;.dmg&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;folder → &lt;code&gt;.exe&lt;/code&gt; installer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Installer creation&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;hdiutil&lt;/code&gt; / &lt;code&gt;create_dmg.sh&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Inno Setup (&lt;code&gt;.iss&lt;/code&gt; script)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code signing&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;codesign&lt;/code&gt; + Developer ID certificate&lt;/td&gt;
&lt;td&gt;eSigner CSC (cloud signing)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pre-distribution validation&lt;/td&gt;
&lt;td&gt;Apple Notarization&lt;/td&gt;
&lt;td&gt;SmartScreen reputation buildup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Final artifact&lt;/td&gt;
&lt;td&gt;&lt;code&gt;WP_Maintenance_Pro_X.X.X.dmg&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;WP_Maintenance_Pro_Setup_X.X.X.exe&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both OSes share PyInstaller, but the path diverges from there. Mac sits inside Apple's review process; Windows runs through Microsoft's reputation system. They're fundamentally different ecosystems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mac — PyInstaller → sign → Notarization → DMG
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Intel / Apple Silicon trap
&lt;/h3&gt;

&lt;p&gt;The first trap in Mac PyInstaller builds is &lt;strong&gt;architecture&lt;/strong&gt;. Running &lt;code&gt;pip install&lt;/code&gt; + &lt;code&gt;python build_app.py&lt;/code&gt; on an Apple Silicon Mac without thinking produces native binaries (like &lt;code&gt;cffi&lt;/code&gt;) for &lt;code&gt;arm64&lt;/code&gt; only — which then don't run on Intel Macs at all.&lt;/p&gt;

&lt;p&gt;The fix is to run the entire build through &lt;code&gt;arch -x86_64&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;arch&lt;/span&gt; &lt;span class="nt"&gt;-x86_64&lt;/span&gt; pip3 &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;span class="nb"&gt;arch&lt;/span&gt; &lt;span class="nt"&gt;-x86_64&lt;/span&gt; python3 build_app.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That produces an &lt;code&gt;.app&lt;/code&gt; containing only &lt;code&gt;x86_64&lt;/code&gt; binaries, which runs natively on Intel Macs and through Rosetta 2 on Apple Silicon — a unified distribution.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sign inside-out
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;.app&lt;/code&gt; PyInstaller produces contains many Mach-O binaries internally (&lt;code&gt;_cffi_backend.so&lt;/code&gt; from &lt;code&gt;cryptography&lt;/code&gt;, etc.). The straightforward &lt;code&gt;codesign --deep&lt;/code&gt; approach has known compatibility issues with Hardened Runtime, so we detect Mach-O binaries with the &lt;code&gt;file&lt;/code&gt; command and &lt;strong&gt;sign them individually from the inside out&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;find &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;APP_BUNDLE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-type&lt;/span&gt; f &lt;span class="nt"&gt;-exec&lt;/span&gt; file &lt;span class="o"&gt;{}&lt;/span&gt; &lt;span class="se"&gt;\;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"Mach-O"&lt;/span&gt; | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;: &lt;span class="nt"&gt;-f1&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;read &lt;/span&gt;bin&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    &lt;/span&gt;codesign &lt;span class="nt"&gt;--force&lt;/span&gt; &lt;span class="nt"&gt;--options&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;runtime &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nt"&gt;--entitlements&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ENTITLEMENTS&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nt"&gt;--sign&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;APP_CERT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;bin&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;done&lt;/span&gt;

&lt;span class="c"&gt;# Finally, sign the whole .app&lt;/span&gt;
codesign &lt;span class="nt"&gt;--force&lt;/span&gt; &lt;span class="nt"&gt;--options&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;runtime &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--entitlements&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ENTITLEMENTS&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--sign&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;APP_CERT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;APP_BUNDLE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Notarize via ZIP, then build the DMG
&lt;/h3&gt;

&lt;p&gt;There's an empirical rule that &lt;strong&gt;submitting a ZIP for notarization is faster than submitting a DMG directly&lt;/strong&gt;. Apple's backend goes through a more complex DMG analysis path; ZIP rides a simpler scan path.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ditto &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="nt"&gt;-k&lt;/span&gt; &lt;span class="nt"&gt;--keepParent&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;APP_BUNDLE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ZIP_PATH&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
xcrun notarytool submit &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ZIP_PATH&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--keychain-profile&lt;/span&gt; &lt;span class="s2"&gt;"wpmm-notary"&lt;/span&gt; &lt;span class="nt"&gt;--wait&lt;/span&gt;
xcrun stapler staple &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;APP_BUNDLE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="c"&gt;# Then build the DMG from the stapled .app&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notarization sometimes stalls at &lt;code&gt;In Progress&lt;/code&gt; for days. The cause is often &lt;strong&gt;incomplete Apple Developer Program setup&lt;/strong&gt; — missing Tax Forms or banking information — rather than anything technical. Contacting Apple support occasionally results in a batch of stuck submissions all flipping to Accepted at once. Surprisingly often, the blocker is contractual rather than technical.&lt;/p&gt;

&lt;h2&gt;
  
  
  Windows — PyInstaller → Inno Setup → eSigner CSC
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Inno Setup script
&lt;/h3&gt;

&lt;p&gt;On Windows, &lt;code&gt;WP_Maintenance_Pro.iss&lt;/code&gt; is the Inno Setup script that assembles the installer. User-mode installation (no admin required) into &lt;code&gt;%APPDATA%&lt;/code&gt;, shortcut creation, residual-file cleanup on uninstall — all defined here:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Setup]&lt;/span&gt;
&lt;span class="py"&gt;AppId&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;{{B3A7F2C1-4E8D-4A9F-B2C3-D5E6F7A8B9C0}&lt;/span&gt;
&lt;span class="py"&gt;DefaultDirName&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;{userappdata}&lt;/span&gt;&lt;span class="se"&gt;\{&lt;/span&gt;&lt;span class="c"&gt;#MyAppName}
&lt;/span&gt;&lt;span class="s"&gt;PrivilegesRequired=lowest&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;PrivilegesRequired=lowest&lt;/code&gt; installs in user mode so people without admin rights — common in corporate environments — can still install the app. The visible drop in support tickets just from avoiding the UAC dialog is noticeable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cloud signing with eSigner CSC
&lt;/h3&gt;

&lt;p&gt;Windows code signing traditionally requires a physical USB token holding the certificate. &lt;strong&gt;eSigner CSC&lt;/strong&gt; (SSL.com's cloud signing service) lets you sign from automation scripts without any token plugged in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"C:\esigner\CodeSignTool.bat"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sign&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;-input_file_path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"WP_Maintenance_Pro_Setup.exe"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;-output_dir_path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"signed/"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;-credential_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;${CRED_ID}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;-username&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;${USERNAME}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-password&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;${PASSWORD}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;-totp_secret&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;${TOTP_SECRET}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;OV/EV certificate grade differences, and SmartScreen reputation buildup (you get "Unknown publisher" warnings until enough installs accumulate) are each topics of their own — but for "just getting it signed and shipping," eSigner CSC automation is the practical path.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cross-cutting challenge — version synchronization and reproducibility
&lt;/h2&gt;

&lt;p&gt;The recurring headache on every release is &lt;strong&gt;version number synchronization&lt;/strong&gt;. &lt;code&gt;version.py&lt;/code&gt;, &lt;code&gt;MyAppVersion&lt;/code&gt; in &lt;code&gt;WP_Maintenance_Pro.iss&lt;/code&gt;, &lt;code&gt;server/wpmm-web/version.json&lt;/code&gt;, the LP download links — four or more files all need to match per release. If one drifts, you get bugs like "the new version is live, but the in-app update check doesn't notice."&lt;/p&gt;

&lt;p&gt;&lt;code&gt;release.py&lt;/code&gt; exists as a single-shot updater, but as files get added over time the script needs to be kept in sync. A release checklist remains essential.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reflection — "the same Python," distributed completely differently
&lt;/h2&gt;

&lt;p&gt;"Cross-platform" sounds simple, but distribution-side work splits cleanly per OS. Binary building can be unified through PyInstaller, but &lt;strong&gt;installers, code signing, and OS-side validation&lt;/strong&gt; all live in separate ecosystems — you have to follow each one's conventions.&lt;/p&gt;

&lt;p&gt;That said, once the pipeline is built, new releases come out in about 30 minutes — &lt;code&gt;python build_app.py&lt;/code&gt; + &lt;code&gt;bash sign_and_notarize.sh&lt;/code&gt; + Inno Setup F9 and you're done. &lt;strong&gt;High initial setup cost, but easy to spin afterward&lt;/strong&gt; — that's the lived experience of two-OS distribution.&lt;/p&gt;

</description>
      <category>python</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Why we built a desktop app on local Flask + browser UI instead of PyQt or Electron</title>
      <dc:creator>Susumu Takahashi</dc:creator>
      <pubDate>Tue, 16 Jun 2026 00:39:16 +0000</pubDate>
      <link>https://dev.to/susumun/why-we-built-a-desktop-app-on-local-flask-browser-ui-instead-of-pyqt-or-electron-33pk</link>
      <guid>https://dev.to/susumun/why-we-built-a-desktop-app-on-local-flask-browser-ui-instead-of-pyqt-or-electron-33pk</guid>
      <description>&lt;p&gt;When you double-click WP Maintenance Manager, it opens a browser tab — and the entire UI lives inside that tab. &lt;strong&gt;No native window is created.&lt;/strong&gt; It's an unusual structure for a first-time user, and the natural question is: "why a browser?"&lt;/p&gt;

&lt;p&gt;That choice was an intentional design decision when building a Python desktop application. Here's the comparison that led to it, and the side effects of the choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four realistic options
&lt;/h2&gt;

&lt;p&gt;For a WordPress maintenance automation tool, four implementation styles were practical:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;UI&lt;/th&gt;
&lt;th&gt;Distribution size&lt;/th&gt;
&lt;th&gt;Dev cost&lt;/th&gt;
&lt;th&gt;Per-OS extra work&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Native (Swift / WPF)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;OS-native windows&lt;/td&gt;
&lt;td&gt;Small–medium&lt;/td&gt;
&lt;td&gt;High (separate impl per OS)&lt;/td&gt;
&lt;td&gt;Heavy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PyQt / PySide&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Qt widgets&lt;/td&gt;
&lt;td&gt;Medium (~80 MB)&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Light&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Electron&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Chromium-embedded web UI&lt;/td&gt;
&lt;td&gt;Large (~150 MB+)&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Light&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Local Flask + system browser&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;System browser tab&lt;/td&gt;
&lt;td&gt;Small (~50 MB)&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Light&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;PyQt&lt;/strong&gt; was a serious early candidate. A Python-only stack is appealing, but widget styling drifts subtly between OSes, Qt's layout system demands constant attention, and resolving Qt plugins under PyInstaller is fiddly. Dev velocity was not where it needed to be.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Electron&lt;/strong&gt; is the industry-standard choice for cross-platform UI, with the big benefit that HTML/CSS-based UIs are quick to write. But the distribution is well over 100 MB, and memory consumption is heavy. For a tool that often runs in the background, that overhead is too much to justify.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why local Flask + browser won
&lt;/h2&gt;

&lt;p&gt;The final structure was &lt;strong&gt;Flask (Python's lightweight web framework) + the system browser&lt;/strong&gt; for UI. The decision rested on three axes:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The backend had to be Python anyway
&lt;/h3&gt;

&lt;p&gt;SSH connections via &lt;code&gt;fabric&lt;/code&gt; / &lt;code&gt;paramiko&lt;/code&gt;, browser automation via &lt;code&gt;playwright&lt;/code&gt;, encryption via &lt;code&gt;cryptography&lt;/code&gt; — every library at the core of WordPress maintenance lives in the Python ecosystem. Writing the backend in another language wasn't really an option. If Python is already required on the backend, putting the UI in Python too keeps distribution simple.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. HTML/CSS/JS makes UI iteration fast
&lt;/h3&gt;

&lt;p&gt;Flask renders &lt;code&gt;templates/index.html&lt;/code&gt;, and the UI is built with Tailwind CSS and vanilla JS. Anyone with web-development experience can ship features quickly. Learning a new native widget vocabulary every time slows iteration far more than this approach does.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Distribution is about 1/3 the size of Electron
&lt;/h3&gt;

&lt;p&gt;By not bundling Chromium, the PyInstaller artifact lands around 50 MB. The same Python codebase and the same &lt;code&gt;templates/&lt;/code&gt; directory power both the macOS &lt;code&gt;.app&lt;/code&gt; and the Windows &lt;code&gt;.exe&lt;/code&gt;. Almost no per-OS extra work — that was the biggest practical win.&lt;/p&gt;

&lt;h2&gt;
  
  
  The side effects of using a browser
&lt;/h2&gt;

&lt;p&gt;This structure comes with a tax. &lt;strong&gt;The browser tab is the UI.&lt;/strong&gt; If the user closes that tab, the app is still running, but there's no way to reach it. Double-clicking the app again to reopen it doesn't help, because macOS LaunchServices sees "this app is already running" and just refocuses it, without opening a new browser tab.&lt;/p&gt;

&lt;p&gt;Fixing this required a heartbeat-based liveness check combined with a self-clobbering lockfile. (Details in &lt;a href="https://en.wpmm.jp/blog/macos-launchservices-restart-fix/" rel="noopener noreferrer"&gt;when a macOS desktop app refuses to restart&lt;/a&gt;.)&lt;/p&gt;

&lt;p&gt;There are other side effects too: a fixed port is occupied (so port-collision detection is needed), browser private mode breaks the login session, and so on. None of these would have existed with a native window.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reflection — structure choice is requirement-dependent
&lt;/h2&gt;

&lt;p&gt;"Local Flask + browser UI" is not a universal best choice. For apps that lean heavily on native UI components (notification center, menu-bar residency, keychain integration), or where startup happens frequently in offline-only contexts, PyQt or Native make more sense.&lt;/p&gt;

&lt;p&gt;But under the constraints WordPress maintenance automation actually had — &lt;strong&gt;backend-heavy, dashboard-style UI, small distribution, mandatory two-OS support&lt;/strong&gt; — Flask + browser was the right balance. We optimized for dev velocity and distribution size, accepting other trade-offs.&lt;/p&gt;

&lt;p&gt;The side effects need separate, careful handling. Even so, the structure has more than paid for itself across the lifetime of the project.&lt;/p&gt;

</description>
      <category>python</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Pinpoint rollback — building per-plugin revert with WP-CLI</title>
      <dc:creator>Susumu Takahashi</dc:creator>
      <pubDate>Sun, 14 Jun 2026 23:35:48 +0000</pubDate>
      <link>https://dev.to/susumun/pinpoint-rollback-building-per-plugin-revert-with-wp-cli-5go5</link>
      <guid>https://dev.to/susumun/pinpoint-rollback-building-per-plugin-revert-with-wp-cli-5go5</guid>
      <description>&lt;p&gt;You batch-update 20 plugins, and one breaks the site. Most WordPress maintenance tools play it safe and &lt;strong&gt;roll back all 20 updates&lt;/strong&gt; (variations on "Safe Updates" or "Atomic Updates"). It's a reasonable default.&lt;/p&gt;

&lt;p&gt;But running in production, you start running into cases where you want &lt;strong&gt;only the broken one reverted, and the other 19 to keep their updates&lt;/strong&gt;. Here's how that design is built on top of WP-CLI.&lt;/p&gt;

&lt;h2&gt;
  
  
  The command that makes it possible — &lt;code&gt;wp plugin install --version=X --force&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;WP-CLI has a powerful command for "install a plugin at a specific version, overwriting whatever's currently there":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wp plugin &lt;span class="nb"&gt;install&lt;/span&gt; &amp;lt;plugin-slug&amp;gt; &lt;span class="nt"&gt;--version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1.2.3 &lt;span class="nt"&gt;--force&lt;/span&gt; &lt;span class="nt"&gt;--skip-plugins&lt;/span&gt; &lt;span class="nt"&gt;--skip-themes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What each flag does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;--version=1.2.3&lt;/code&gt; — Specifies the older version to install (any version published on WP.org works)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--force&lt;/code&gt; — Overwrites if the plugin is already installed&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--skip-plugins --skip-themes&lt;/code&gt; — Skips plugin/theme loading at WP-CLI startup, so WP-CLI itself doesn't crash even when a broken plugin is present (&lt;a href="https://en.wpmm.jp/blog/wp-cli-skip-plugins-rescue/" rel="noopener noreferrer"&gt;covered in a separate post&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That one line is enough to roll back a single plugin to a known-good version. The remaining design question is &lt;strong&gt;when and under what conditions&lt;/strong&gt; to invoke it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Combining with HTTP checks
&lt;/h2&gt;

&lt;p&gt;Step-by-step updates with HTTP status checks between each:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Pre-update:    GET / → 200
↓
wp plugin update &amp;lt;plugin-slug&amp;gt; --skip-plugins --skip-themes
↓
Post-update:   GET / → 500   ← broken!
↓
wp plugin install &amp;lt;plugin-slug&amp;gt; --version=&amp;lt;previous&amp;gt; --force --skip-plugins --skip-themes
↓
Post-rollback: GET / → 200   ← recovered
↓
proceed to the next plugin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By switching the update granularity from "everything at once" to &lt;strong&gt;"one at a time + HTTP check"&lt;/strong&gt;, you get clear visibility into which plugin caused the breakage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Identifying the rollback target
&lt;/h2&gt;

&lt;p&gt;The plugin you just updated is the prime suspect — by construction. This isn't possible with bulk updates: if multiple plugins are updated together and HTTP returns 500, you can't tell which one is responsible.&lt;/p&gt;

&lt;p&gt;The advantage of one-at-a-time updates is that &lt;strong&gt;the moment the HTTP status changes, the plugin updated immediately before becomes the confirmed rollback target&lt;/strong&gt;. No risk of accidentally reverting an unrelated plugin.&lt;/p&gt;

&lt;p&gt;The previous version comes from a pre-update snapshot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Capture before updates start&lt;/span&gt;
wp plugin list &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;json &lt;span class="nt"&gt;--skip-plugins&lt;/span&gt; &lt;span class="nt"&gt;--skip-themes&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/plugins_before.json

&lt;span class="c"&gt;# Look up the previous version of the failed plugin&lt;/span&gt;
&lt;span class="nv"&gt;prev_version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.[] | select(.name=="&amp;lt;slug&amp;gt;") | .version'&lt;/span&gt; /tmp/plugins_before.json&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives you the version string to feed back into &lt;code&gt;wp plugin install --version=&amp;lt;prev&amp;gt;&lt;/code&gt;. Roll back, verify, proceed to the next plugin.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verifying recovery — always re-check HTTP after a rollback
&lt;/h2&gt;

&lt;p&gt;After rolling back, take the HTTP status again:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Back to 200 → skip the update for this plugin, continue to the next&lt;/li&gt;
&lt;li&gt;❌ Still 500 → the rollback didn't recover the site (the broken state is in the DB schema or other files, not just the plugin's PHP) → escalate to heavier recovery (full DB rollback, cache flush, theme fallback, etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The lesson: &lt;strong&gt;don't assume rolling back the plugin always fixes things&lt;/strong&gt;. &lt;code&gt;--force&lt;/code&gt; overwriting only replaces files; DB-side inconsistencies need separate detection.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway — "roll back all" vs "roll back one"
&lt;/h2&gt;

&lt;p&gt;The industry-mainstream "roll back everything" design is simpler to implement and errs on the safe side — and there are good reasons it's the default. On the other hand, the "roll back only the one that broke" design has these operational advantages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The culprit is recorded directly in the log&lt;/li&gt;
&lt;li&gt;Successful updates to other plugins are preserved&lt;/li&gt;
&lt;li&gt;At the next maintenance run, only the rolled-back plugin needs retrying&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A trade-off: more implementation complexity, in exchange for higher operational transparency and continuity.&lt;/p&gt;

&lt;p&gt;The small WP-CLI command &lt;code&gt;--version=X --force&lt;/code&gt; is what makes this possible. For anyone considering maintenance automation on shared hosts where SSH + WP-CLI is available, this flag combo is worth committing to memory.&lt;/p&gt;

&lt;p&gt;For the structural reasons the industry as a whole hasn't adopted per-plugin rollback, see also &lt;a href="https://en.wpmm.jp/blog/wordpress-maintenance-industry-gaps/" rel="noopener noreferrer"&gt;three gaps the WordPress maintenance industry still hasn't solved&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>php</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Three gaps the WordPress maintenance industry still hasn't solved — from a survey of four major tools</title>
      <dc:creator>Susumu Takahashi</dc:creator>
      <pubDate>Sat, 13 Jun 2026 03:45:43 +0000</pubDate>
      <link>https://dev.to/susumun/three-gaps-the-wordpress-maintenance-industry-still-hasnt-solved-from-a-survey-of-four-major-11ej</link>
      <guid>https://dev.to/susumun/three-gaps-the-wordpress-maintenance-industry-still-hasnt-solved-from-a-survey-of-four-major-11ej</guid>
      <description>&lt;p&gt;WordPress maintenance automation has a long-running market, especially outside Japan. ManageWP, MainWP, WP Umbrella, InfiniteWP — each has more than a decade of history behind it.&lt;/p&gt;

&lt;p&gt;While building our comparison pages, we surveyed all four side by side. An interesting pattern emerged: &lt;strong&gt;three things none of the four tools offer&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Each is a gap the industry has long treated as "not feasible," and there are structural reasons why. Here's a look at those three unsolved areas — and why they remain unsolved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gap 1 — Per-plugin updates with HTTP checks between each one
&lt;/h2&gt;

&lt;p&gt;In most maintenance tools, plugin updates run in &lt;strong&gt;bulk&lt;/strong&gt;. After the batch, the tool takes a sitewide screenshot diff or HTTP status check, and if anything is broken, "Safe Updates" or "Atomic Updates" features &lt;strong&gt;roll everything back at once&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Why isn't "one at a time with an HTTP check between each" the standard? The main reason is &lt;strong&gt;API design constraints&lt;/strong&gt;. WordPress's built-in &lt;code&gt;wp_ajax_update-plugin&lt;/code&gt; and Worker-plugin APIs (like ManageWP Worker) are designed around batch processing. Doing an HTTP probe from an external host after every single update would add significant per-update overhead. The industry has settled on "bulk update → bulk check" as the natural granularity.&lt;/p&gt;

&lt;p&gt;The side effect: &lt;strong&gt;identifying which plugin caused the breakage&lt;/strong&gt; often falls to the operator's manual investigation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gap 2 — Pinpoint rollback (only the one that broke)
&lt;/h2&gt;

&lt;p&gt;The industry-standard "Safe Updates" feature is fundamentally a &lt;strong&gt;"roll back everything"&lt;/strong&gt; design. If 20 plugins are batched together and one breaks the site, all 20 updates revert. It's a safety-first choice — but operationally, it means the 19 that finished cleanly are also lost.&lt;/p&gt;

&lt;p&gt;Why isn't pinpoint rollback (revert only the one that broke) the standard? The root cause is &lt;strong&gt;state-management complexity&lt;/strong&gt;. To pinpoint rollback, you need to keep the pre-update files of each plugin individually. Storage, transfer cost, and dependency consistency checks become impractical over a Worker-plugin HTTP API. "One whole-site snapshot, restore once" is far simpler to implement and operate, which is where the industry converged.&lt;/p&gt;

&lt;p&gt;WP-CLI-based approaches change the math here — but as the next section explains, &lt;strong&gt;WP-CLI itself isn't the industry mainstream&lt;/strong&gt;, which limits how widely this alternative spreads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gap 3 — Reaching client sites without installing an extra plugin
&lt;/h2&gt;

&lt;p&gt;This is the most structural gap. All four tools require a &lt;strong&gt;Worker / Child plugin&lt;/strong&gt; installed on each client site. ManageWP Worker, MainWP Child, Umbrella, InfiniteWP Client — the names differ, but every tool ships a "gateway plugin" that lives on each managed site.&lt;/p&gt;

&lt;p&gt;Why has the industry adopted this? It's about &lt;strong&gt;connectivity and compatibility guarantees&lt;/strong&gt;. Shared hosts are extremely varied — SSH availability, WP-CLI presence, and firewall configuration all differ. A &lt;strong&gt;WordPress-internal gateway plugin&lt;/strong&gt; lets the tool reach every site through a uniform HTTP API. It's the pragmatic industry solution.&lt;/p&gt;

&lt;p&gt;But it comes with side effects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The client site permanently hosts a "maintenance-vendor management plugin"&lt;/li&gt;
&lt;li&gt;If that plugin develops a vulnerability, every client is affected&lt;/li&gt;
&lt;li&gt;Clients eventually ask "what is this plugin?" — which needs an explanation&lt;/li&gt;
&lt;li&gt;If the plugin gets deleted (e.g., on contract termination), the site disappears from the tool's management view&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Whether to accept these trade-offs, or to choose a different connection model entirely, ends up being a quietly important axis when selecting a maintenance tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway — should the industry's premises be questioned?
&lt;/h2&gt;

&lt;p&gt;What these three unsolved gaps share is that they're areas &lt;strong&gt;the industry has long agreed are "good enough as is."&lt;/strong&gt; The combination of "bulk update + whole-site rollback + Worker plugin" is technically and commercially stable. The fact that four major tools have run on this same structure for over a decade speaks to the design's maturity.&lt;/p&gt;

&lt;p&gt;That said, alternative approaches that &lt;strong&gt;question those industry premises&lt;/strong&gt; do exist. Using SSH + WP-CLI sidesteps the API overhead, making step-by-step updates and pinpoint rollback practical, and removing the need for a Worker plugin. The trade-off: the target narrows to shared hosts where SSH is available, and the operator needs some familiarity with SSH fundamentals.&lt;/p&gt;

&lt;p&gt;It's not a question of which is "right" — it's that &lt;strong&gt;the operating style and constraint mix lead to different fits&lt;/strong&gt;. Whether you pick the industry-mainstream "Worker + bulk + whole-site rollback" model or step outside it, awareness of these three unsolved areas gives you a clearer frame for asking what actually matters to your own maintenance operation.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>php</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>After a core rollback, halt the rest — a safety design we arrived at the hard way</title>
      <dc:creator>Susumu Takahashi</dc:creator>
      <pubDate>Fri, 12 Jun 2026 00:23:25 +0000</pubDate>
      <link>https://dev.to/susumun/after-a-core-rollback-halt-the-rest-a-safety-design-we-arrived-at-the-hard-way-6c</link>
      <guid>https://dev.to/susumun/after-a-core-rollback-halt-the-rest-a-safety-design-we-arrived-at-the-hard-way-6c</guid>
      <description>&lt;p&gt;In WordPress maintenance automation, you inevitably run into points where you have to decide: &lt;strong&gt;keep going, or stop right here?&lt;/strong&gt; One that took us a long time to get right was this: when a WordPress core update goes wrong and gets rolled back, should the remaining plugin updates continue, or stop?&lt;/p&gt;

&lt;p&gt;We eventually switched to the "stop" design, but we started with "keep going" — and several traps surfaced only after running it in production. Here's how the redesign happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three cases to separate
&lt;/h2&gt;

&lt;p&gt;The outcome of a core update, viewed through a rollback lens, falls into three patterns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Case 1: Core rollback succeeded, site recovered&lt;/strong&gt; — the site is healthy again after the RB&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Case 2: Core rollback succeeded, but site did not recover&lt;/strong&gt; — the RB ran, but the site is still broken&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Case 3: Core rollback itself failed&lt;/strong&gt; — the RB couldn't even complete&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Case 1 is clearly "keep going," and Cases 2/3 are clearly "abnormal." But &lt;strong&gt;what to do next&lt;/strong&gt; isn't as simple as that framing suggests.&lt;/p&gt;

&lt;h2&gt;
  
  
  The old design — disable the HTTP check and continue
&lt;/h2&gt;

&lt;p&gt;The original design kept maintenance running through Cases 2 and 3:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;_skip_http_check&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;   &lt;span class="c1"&gt;# disable HTTP check after a core anomaly, keep going
# remaining plugin / theme / translation updates still run
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reasoning was: "Once core is broken, of course a plugin update will return 5xx — so disable the HTTP check, and we won't mistakenly roll back unrelated plugins."&lt;/p&gt;

&lt;p&gt;In practice, this did reduce false-positive rollbacks. But as the tool ran in real environments, two problems emerged.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two problems that surfaced
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Problem (a): broken state plus piled updates = untraceable
&lt;/h3&gt;

&lt;p&gt;If 20 plugins are updated while core is still broken, the log records "20 updates succeeded." The site is still broken, but the log reads as healthy.&lt;/p&gt;

&lt;p&gt;The next day, when the agency tries to trace "where did it break?" — there's no way to tell whether &lt;strong&gt;core was the cause, one of the later updates was, or some combination of them&lt;/strong&gt;. A safety mechanism intended to reduce noise was actually inflating investigation cost.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem (b): genuine plugin failures became invisible
&lt;/h3&gt;

&lt;p&gt;Setting &lt;code&gt;_skip_http_check = True&lt;/code&gt; disables the HTTP check uniformly — including for plugin-side bugs that have nothing to do with core (memory leaks, dependency conflicts, PHP version incompatibility).&lt;/p&gt;

&lt;p&gt;What was supposed to be "skip the HTTP check while core is broken" was actually "&lt;strong&gt;make all anomalies in this window invisible&lt;/strong&gt;." That's equivalent to intentionally disabling a safety device.&lt;/p&gt;

&lt;h2&gt;
  
  
  The new design — halt in Cases 2 and 3
&lt;/h2&gt;

&lt;p&gt;Based on these problems, Cases 2 and 3 now &lt;strong&gt;stop all subsequent plugin / theme / translation updates&lt;/strong&gt; entirely.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;_halt_remaining&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# set to True in Case 2 / 3
&lt;/span&gt;    &lt;span class="c1"&gt;# record step_rollbacks first, then early return
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key is that this isn't just "stop and walk away":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;step_rollbacks&lt;/code&gt; records are kept&lt;/strong&gt; — the full record of what happened stays in the log&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The outer &lt;code&gt;visual_check&lt;/code&gt; / &lt;code&gt;browser_automation&lt;/code&gt; / email notification still run&lt;/strong&gt; — final HTTP confirmation and the alert to the agency are still guaranteed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reports are still generated after the early return&lt;/strong&gt; — the message "stopped after core rollback" appears in the client-facing report too&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Case 1 (RB succeeded + recovery confirmed) continues as before. The site is healthy again, so the precondition for safely running the remaining plugin updates with HTTP checks is intact.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trade-off we accepted
&lt;/h2&gt;

&lt;p&gt;This change means "if core breaks today, all subsequent updates for the day stop." A scheduled batch of 20 plugin updates &lt;strong&gt;gets deferred to the next maintenance run&lt;/strong&gt; if a single core rollback happens. In the short term, that feels inconvenient.&lt;/p&gt;

&lt;p&gt;But in real operation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The agency receives a &lt;strong&gt;clear signal: "fix core before retrying"&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;The next maintenance job resumes from a healthy state&lt;/li&gt;
&lt;li&gt;The log carries an unambiguous "halted after core rollback" trace&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These three together — at the cost of skipping that day's plugin updates — give &lt;strong&gt;operationally far more traceable behavior&lt;/strong&gt; than the silently-continues-while-broken alternative.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway — safety devices need to be left on
&lt;/h2&gt;

&lt;p&gt;Designs that "disable the check during abnormal conditions" can look clever but tend to &lt;strong&gt;make anomalies invisible&lt;/strong&gt;. Stopping the moment something abnormal is detected, and handing the decision back to the agency, generally gives more predictable behavior across the workflow.&lt;/p&gt;

&lt;p&gt;When a maintenance-automation design choice is hard to settle, a useful heuristic is: &lt;strong&gt;don't try to fix it automatically — communicate it clearly to a human.&lt;/strong&gt; Surprisingly often, that's what saves the operation.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>php</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>InfiniteWP's Strengths and Who It Fits — An Honest Review from a Competing Tool Builder</title>
      <dc:creator>Susumu Takahashi</dc:creator>
      <pubDate>Thu, 11 Jun 2026 00:22:25 +0000</pubDate>
      <link>https://dev.to/susumun/infinitewps-strengths-and-who-it-fits-an-honest-review-from-a-competing-tool-builder-276k</link>
      <guid>https://dev.to/susumun/infinitewps-strengths-and-who-it-fits-an-honest-review-from-a-competing-tool-builder-276k</guid>
      <description>&lt;p&gt;Among WordPress maintenance tools, &lt;strong&gt;InfiniteWP&lt;/strong&gt; is one of the most established names. Released by Revmakx in 2011, the tool has been operated continuously for over a decade. It enjoys deep loyalty from agencies that have &lt;strong&gt;invested years building operational know-how around it&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;We at WP Maintenance Manager take a different approach, and our comparison pages outline where the two diverge. But before talking about differences, &lt;strong&gt;the strengths of InfiniteWP deserve to be stated honestly&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here are the five points where InfiniteWP fits an agency particularly well.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Over a decade of operational track record
&lt;/h2&gt;

&lt;p&gt;InfiniteWP's biggest structural advantage is &lt;strong&gt;trust built across more than a decade of continuous operation&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Released in 2011 — one of the oldest tools in the space&lt;/li&gt;
&lt;li&gt;A large base of long-time English-speaking users with shared operational patterns&lt;/li&gt;
&lt;li&gt;Well-defined upgrade paths from older versions&lt;/li&gt;
&lt;li&gt;Backward compatibility with existing workflows and scripts has been maintained for years&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For agencies already invested in InfiniteWP, switching tools means more than "migration work" — it means &lt;strong&gt;rebuilding the operational know-how accumulated over years&lt;/strong&gt;. Continuing to use a tool with proven track record is, in itself, a strength that long-running platforms have.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;temporal depth&lt;/strong&gt; that newer tools simply cannot replicate is a meaningful selection reason for conservative industries — those reluctant to substantially change established workflows.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Self-hosted — full control of the dashboard
&lt;/h2&gt;

&lt;p&gt;InfiniteWP is &lt;strong&gt;self-hosted by default&lt;/strong&gt;, letting you place the dashboard on your own server (a cloud-hosted version is available separately).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Host on infrastructure you own&lt;/li&gt;
&lt;li&gt;Complete data ownership&lt;/li&gt;
&lt;li&gt;No dependency on external SaaS&lt;/li&gt;
&lt;li&gt;Arbitrary customization possible&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When the constraint is "client data must not sit in a third-party SaaS" or "our security policy doesn't permit SaaS," InfiniteWP's self-hosted architecture is a direct answer. If your team has experience operating PHP / WordPress infrastructure, the operational overhead is manageable.&lt;/p&gt;

&lt;p&gt;The cloud-hosted version is also available, so &lt;strong&gt;"start in the cloud, move to self-hosted as you grow"&lt;/strong&gt; is achievable within the same tool — a flexibility most competitors don't match.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Modular — pick only the add-ons you need
&lt;/h2&gt;

&lt;p&gt;InfiniteWP uses a &lt;strong&gt;modular add-on model&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Core features (updates, backups, basic management) are free&lt;/li&gt;
&lt;li&gt;Add only the extensions your operation actually needs&lt;/li&gt;
&lt;li&gt;Per-add-on licensing&lt;/li&gt;
&lt;li&gt;You don't pay for features you don't use&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of "bundle me everything because I want feature X," you can &lt;strong&gt;selectively pick only what you actually use&lt;/strong&gt;. This precision aligns well with operating philosophies that aim to keep tools minimal and waste-free.&lt;/p&gt;

&lt;p&gt;For large-scale operations managing many sites, the per-add-on model makes it easier to &lt;strong&gt;optimize total tool cost&lt;/strong&gt; by including only what's necessary.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Browser-based access despite self-hosting
&lt;/h2&gt;

&lt;p&gt;Even though it's self-hosted, the dashboard is a WordPress plugin-based UI, so it remains &lt;strong&gt;accessible through any browser&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Same operation from phone, tablet, or multiple PCs&lt;/li&gt;
&lt;li&gt;Access from client sites or during travel&lt;/li&gt;
&lt;li&gt;Shared use across multiple developers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The "I don't want to be tied to a specific PC" advantage of cloud tools — while staying on a self-hosted architecture. A reasonable middle ground for teams that want both data sovereignty and access flexibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Annual licensing — predictable budgeting
&lt;/h2&gt;

&lt;p&gt;InfiniteWP's add-on pricing uses &lt;strong&gt;annual licensing&lt;/strong&gt; (starting at $147/year, cloud-hosted version at $597/year).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Single-fiscal-year expenditure&lt;/li&gt;
&lt;li&gt;Easy to classify as "annual operational cost" in accounting&lt;/li&gt;
&lt;li&gt;No monthly subscription micromanagement&lt;/li&gt;
&lt;li&gt;High predictability for budget planning&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For organizations suffering from "subscription fatigue" — or operating under &lt;strong&gt;strict annual budgeting cultures&lt;/strong&gt; — the annual licensing model is itself a procurement-friendly trait.&lt;/p&gt;

&lt;p&gt;In cases where parent-company accounting rules require "contracts that complete within a fiscal year" (such as financial subsidiaries or public-sector affiliated entities), annual licensing can become a hard selection criterion.&lt;/p&gt;

&lt;h2&gt;
  
  
  What kind of agency InfiniteWP fits
&lt;/h2&gt;

&lt;p&gt;Putting it together, InfiniteWP fits a team with these traits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Has used InfiniteWP for years (continuity of operational know-how)&lt;/li&gt;
&lt;li&gt;Wants to control the dashboard via self-hosting&lt;/li&gt;
&lt;li&gt;Wants to pick only the features (add-ons) actually needed&lt;/li&gt;
&lt;li&gt;Annual licensing fits the accounting / budgeting culture&lt;/li&gt;
&lt;li&gt;Wants browser-based access flexibility&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If three or more of these apply, InfiniteWP belongs on your shortlist.&lt;/p&gt;

&lt;h2&gt;
  
  
  How we're different
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;WP Maintenance Manager&lt;/strong&gt; takes a different shape.&lt;/p&gt;

&lt;p&gt;Where InfiniteWP is a &lt;strong&gt;self-hosted dashboard built as a WordPress plugin&lt;/strong&gt;, WP Maintenance Manager is a &lt;strong&gt;desktop application (Mac &amp;amp; Windows) using SSH + WP-CLI&lt;/strong&gt; to reach client sites.&lt;/p&gt;

&lt;p&gt;That trade-off gives up InfiniteWP's "access from anywhere" and "incremental expansion via add-ons" flexibility. In exchange:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No dashboard server to operate&lt;/strong&gt; (it runs on your local PC — no separate WordPress install to maintain)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No Client plugin on client sites&lt;/strong&gt; (SSH-native, so no Worker / Client plugin is needed)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pinpoint rollback&lt;/strong&gt; — if a single plugin update breaks the site, only that one is reverted while the rest of the maintenance continues (standard, not an add-on)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pricing models also differ. InfiniteWP uses &lt;strong&gt;annual licensing&lt;/strong&gt;; WP Maintenance Manager uses a &lt;strong&gt;monthly model&lt;/strong&gt;. Annual licensing favors long-term budget planning; monthly pricing favors short-term trials and easy scaling.&lt;/p&gt;

&lt;p&gt;This isn't "which is better." It's &lt;strong&gt;which operating style each fits&lt;/strong&gt;. Self-hosted, modular, annual-license operations → InfiniteWP. SSH-based, PC-centric, dashboard-overhead-averse operations → WP Maintenance Manager. That's the natural split.&lt;/p&gt;

&lt;h2&gt;
  
  
  For a more detailed comparison
&lt;/h2&gt;

&lt;p&gt;A 12-item objective spec table, pricing model breakdown, parallel-trial steps, and decision-frame analysis are on our comparison page:&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://en.wpmm.jp/vs-infinitewp.php" rel="noopener noreferrer"&gt;WP Maintenance Manager vs InfiniteWP — Objective Spec Comparison&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Use it not as a single-tool review, but as input for deciding which fits your operating style.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;InfiniteWP is a remarkably strong tool for agencies that prioritize &lt;strong&gt;long-running operations, self-hosting, and annual licensing&lt;/strong&gt;. Over a decade of stable operation, a flexible self-hosted architecture, and a modular add-on catalog give it qualities newer tools simply cannot replicate.&lt;/p&gt;

&lt;p&gt;Every maintenance tool has the operating style it fits. InfiniteWP — established, self-hosted, annually licensed — deserves to top the shortlist for teams that value operational continuity and budget predictability. WP Maintenance Manager's desktop + SSH approach fits a different operating style.&lt;/p&gt;

&lt;p&gt;The best path is to evaluate several candidates against the operating style of your own team.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>php</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>WP Umbrella's Strengths and Who It Fits — An Honest Review from a Competing Tool Builder</title>
      <dc:creator>Susumu Takahashi</dc:creator>
      <pubDate>Tue, 09 Jun 2026 13:38:36 +0000</pubDate>
      <link>https://dev.to/susumun/wp-umbrellas-strengths-and-who-it-fits-an-honest-review-from-a-competing-tool-builder-400</link>
      <guid>https://dev.to/susumun/wp-umbrellas-strengths-and-who-it-fits-an-honest-review-from-a-competing-tool-builder-400</guid>
      <description>&lt;p&gt;Among WordPress maintenance tools, &lt;strong&gt;WP Umbrella&lt;/strong&gt; has built strong traction with EU-based agencies. Launched in 2020 from France, this relatively young SaaS tool has rapidly grown its user base by anchoring its identity around three pillars: strict GDPR operations, EU data residency, and continuous uptime monitoring — all wrapped in a notably modern interface.&lt;/p&gt;

&lt;p&gt;We at WP Maintenance Manager take a different approach, and our comparison pages outline where the two diverge. But before talking about differences, &lt;strong&gt;the strengths of WP Umbrella deserve to be stated honestly&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here are the five points where WP Umbrella fits an agency particularly well.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. EU data residency and strict GDPR compliance
&lt;/h2&gt;

&lt;p&gt;WP Umbrella's biggest structural advantage is that &lt;strong&gt;the servers, the data, and the operating company are all within the EU&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Operating company: based in France&lt;/li&gt;
&lt;li&gt;Data centers: within the EU&lt;/li&gt;
&lt;li&gt;Legal framework: full GDPR compliance&lt;/li&gt;
&lt;li&gt;Architected to be outside the reach of the U.S. CLOUD Act&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For European companies, public-sector organizations, and educational institutions, "we cannot place client data with a U.S.-based SaaS" is not a rare constraint. Beyond simply offering GDPR-compliant settings, WP Umbrella is &lt;strong&gt;structurally built so EU data residency is guaranteed&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For organizations operating under EU regulations, this isn't a preference — it's a &lt;strong&gt;compliance prerequisite&lt;/strong&gt; that maps directly onto procurement decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Continuous uptime monitoring as a core feature
&lt;/h2&gt;

&lt;p&gt;WP Umbrella &lt;strong&gt;positions uptime monitoring as a central piece of value&lt;/strong&gt;, not as an add-on. It comes standard with every plan.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Site checks at 1-minute intervals&lt;/li&gt;
&lt;li&gt;Immediate email and Slack notifications on downtime&lt;/li&gt;
&lt;li&gt;Continuous performance (response time) tracking&lt;/li&gt;
&lt;li&gt;HTTPS certificate expiration monitoring&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This stands in contrast to the "check during maintenance" approach common in other tools. With WP Umbrella, &lt;strong&gt;24×7×365 continuous monitoring is the default assumption&lt;/strong&gt;. For agencies running e-commerce sites, booking systems, or contact forms — anything where downtime equates to lost transactions — that continuous coverage is direct value.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Daily file-level backups
&lt;/h2&gt;

&lt;p&gt;WP Umbrella performs &lt;strong&gt;daily backups of both database and files&lt;/strong&gt;. Many maintenance tools default to "daily DB, weekly files." Standard daily full backups (DB + files) are notable in this space.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Daily full (DB + files) backups across all plans&lt;/li&gt;
&lt;li&gt;30 days of retention&lt;/li&gt;
&lt;li&gt;Incremental diffs to reduce transfer overhead&lt;/li&gt;
&lt;li&gt;One-click restore&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Knowing — at daily granularity — when each file was uploaded and by which client is particularly valuable for media-heavy sites or content-driven operations.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Modern, anywhere-accessible SaaS UI
&lt;/h2&gt;

&lt;p&gt;Being SaaS, WP Umbrella is browser-based and &lt;strong&gt;accessible from anywhere on any device&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Same operational feel on phone, tablet, or laptop&lt;/li&gt;
&lt;li&gt;Built for shared editing and collaborative operations&lt;/li&gt;
&lt;li&gt;No dashboard startup cost (just log in)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The UI is widely praised as one of the most polished in the maintenance-tool category. The &lt;strong&gt;modern, Linear- or Notion-inspired interface&lt;/strong&gt; resonates particularly well with creative-leaning agencies that care about tool aesthetics.&lt;/p&gt;

&lt;p&gt;If the dashboard is going to be part of daily work, UI quality translates directly into productivity.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Simple, predictable pricing
&lt;/h2&gt;

&lt;p&gt;WP Umbrella uses &lt;strong&gt;flat per-site pricing&lt;/strong&gt;, starting at €1.99 per site per month.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All features included; no separate add-on charges&lt;/li&gt;
&lt;li&gt;Pricing scales by site count (not by team members)&lt;/li&gt;
&lt;li&gt;No granular per-feature billing&lt;/li&gt;
&lt;li&gt;Easy to trial and scale up or down&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For teams that want to &lt;strong&gt;avoid the complexity of "X add-on for feature Y, plan upgrade for team size Z,"&lt;/strong&gt; the simplicity of WP Umbrella's pricing is decision-making clarity in itself.&lt;/p&gt;

&lt;p&gt;For small-to-mid agencies managing roughly 30–100 sites, the monthly rate also falls into a reasonable range.&lt;/p&gt;

&lt;h2&gt;
  
  
  What kind of agency WP Umbrella fits
&lt;/h2&gt;

&lt;p&gt;Putting it together, WP Umbrella fits a team with these traits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Handles client data inside the EU (GDPR-strict operations required)&lt;/li&gt;
&lt;li&gt;24×7 uptime monitoring is part of the SLA&lt;/li&gt;
&lt;li&gt;Daily file-level backups are required&lt;/li&gt;
&lt;li&gt;Prefers a SaaS model accessible from anywhere&lt;/li&gt;
&lt;li&gt;Values a polished, modern UI&lt;/li&gt;
&lt;li&gt;Prefers a simple per-site pricing model&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If three or more of these apply, WP Umbrella belongs on your shortlist.&lt;/p&gt;

&lt;h2&gt;
  
  
  How we're different
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;WP Maintenance Manager&lt;/strong&gt; takes a different shape.&lt;/p&gt;

&lt;p&gt;Where WP Umbrella is an &lt;strong&gt;EU-based cloud SaaS&lt;/strong&gt;, WP Maintenance Manager is a &lt;strong&gt;desktop application (Mac &amp;amp; Windows) using SSH + WP-CLI&lt;/strong&gt; to reach client sites.&lt;/p&gt;

&lt;p&gt;That trade-off gives up WP Umbrella's "continuous uptime monitoring" and "access from anywhere" advantages. In exchange:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No additional plugin installed on client sites&lt;/strong&gt; (SSH-native, so no Worker / Child plugin is needed)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client data stays encrypted on your own PC&lt;/strong&gt; (no client information passes through the cloud)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pinpoint rollback&lt;/strong&gt; — if a single plugin update breaks the site, only that one is reverted while the rest of the maintenance continues&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This isn't "which is better." It's &lt;strong&gt;which operating style each fits&lt;/strong&gt;. EU data residency, 24×7 monitoring, SaaS access → WP Umbrella. SSH-based, PC-centric, self-held data → WP Maintenance Manager. That's the natural split.&lt;/p&gt;

&lt;h2&gt;
  
  
  For a more detailed comparison
&lt;/h2&gt;

&lt;p&gt;A 12-item objective spec table, pricing model breakdown, parallel-trial steps, and decision-frame analysis are on our comparison page:&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://en.wpmm.jp/vs-wp-umbrella.php" rel="noopener noreferrer"&gt;WP Maintenance Manager vs WP Umbrella — Objective Spec Comparison&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Use it not as a single-tool review, but as input for deciding which fits your operating style.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;WP Umbrella is a remarkably strong tool for agencies that prioritize &lt;strong&gt;EU data residency, continuous uptime monitoring, and a polished interface&lt;/strong&gt;. A structurally GDPR-compliant design, daily file-level backups, and simple per-site pricing give it a distinctive position among SaaS maintenance tools.&lt;/p&gt;

&lt;p&gt;Every maintenance tool has the operating style it fits. WP Umbrella, with EU residency and continuous monitoring as core strengths, deserves to top the shortlist for teams running strict GDPR operations or 24×7 SLAs. WP Maintenance Manager's desktop + SSH approach fits a different operating style.&lt;/p&gt;

&lt;p&gt;The best path is to evaluate several candidates against the operating style of your own team.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>php</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>MainWP's Strengths and Who It Fits — An Honest Review from a Competing Tool Builder</title>
      <dc:creator>Susumu Takahashi</dc:creator>
      <pubDate>Mon, 08 Jun 2026 01:59:27 +0000</pubDate>
      <link>https://dev.to/susumun/mainwps-strengths-and-who-it-fits-an-honest-review-from-a-competing-tool-builder-4iap</link>
      <guid>https://dev.to/susumun/mainwps-strengths-and-who-it-fits-an-honest-review-from-a-competing-tool-builder-4iap</guid>
      <description>&lt;p&gt;Among WordPress maintenance tools, &lt;strong&gt;MainWP&lt;/strong&gt; stands out as the leader for agencies with a strong open-source bias. Founded in 2013, fully GPL-licensed, and architected so that the dashboard itself runs on your own WordPress installation, MainWP holds a distinctive position in the market — one shaped by the trust that comes from readable code and self-hosted data ownership.&lt;/p&gt;

&lt;p&gt;We at WP Maintenance Manager take a different approach, and our comparison pages outline where the two diverge. But before talking about differences, &lt;strong&gt;the strengths of MainWP deserve to be stated honestly&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here are the five points where MainWP fits an agency particularly well.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Fully open source (GPL) — you can read the code
&lt;/h2&gt;

&lt;p&gt;MainWP's biggest structural advantage is that its core is &lt;strong&gt;fully GPL-licensed open source&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Source code is published on GitHub&lt;/li&gt;
&lt;li&gt;You can fork and customize it yourself&lt;/li&gt;
&lt;li&gt;Security audits can be conducted in-house or by third parties&lt;/li&gt;
&lt;li&gt;The risk of vendor lock-in is structurally low&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For industries where "we can't contract for tools whose internals are a black box" is a real constraint (finance, healthcare, public sector), and for organizations whose engineers actually read source to audit, MainWP's transparency becomes a direct selection reason.&lt;/p&gt;

&lt;p&gt;With closed-source SaaS, "we don't know what's happening inside" is unavoidable. With MainWP, &lt;strong&gt;the verification you need is verification you can do yourself&lt;/strong&gt;. That's a structural strength no closed competitor can match.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Self-hosted — you keep complete control of the dashboard
&lt;/h2&gt;

&lt;p&gt;MainWP's other defining trait is that &lt;strong&gt;the dashboard itself runs as a WordPress plugin installed on your own WordPress site&lt;/strong&gt;. There's no need to entrust data to an outside SaaS.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The dashboard lives on infrastructure you own&lt;/li&gt;
&lt;li&gt;Client site info and credentials don't pass through a third party&lt;/li&gt;
&lt;li&gt;Data sovereignty stays entirely with you&lt;/li&gt;
&lt;li&gt;You're insulated from third-party outages, acquisitions, and shutdowns&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When the constraint is "client data must not sit in a third-party SaaS" or "our security policy doesn't permit SaaS," MainWP's self-hosted architecture is the direct answer. If your team has any experience running its own infrastructure, the operational overhead is manageable.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Lifetime licensing is available
&lt;/h2&gt;

&lt;p&gt;For organizations tired of subscription sprawl, MainWP's &lt;strong&gt;Pro Lifetime license at $599&lt;/strong&gt; (or Pro at $29/month) is a meaningful option.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pay once, use forever (Lifetime tier)&lt;/li&gt;
&lt;li&gt;Easier to capitalize on the books&lt;/li&gt;
&lt;li&gt;No monthly billing to manage&lt;/li&gt;
&lt;li&gt;Long-term budget becomes predictable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For organizations whose budget policies prefer "expenses that complete within a fiscal year" or "fewer subscriptions overall," simply having the buy-once option is a differentiator. Over a 5–10 year horizon, total cost favors lifetime models for long-running operations.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Browser-based dashboard — accessible from anywhere
&lt;/h2&gt;

&lt;p&gt;Despite the self-hosted architecture, the dashboard is browser-based, so you keep the "access from anywhere" benefit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;From phone or tablet&lt;/li&gt;
&lt;li&gt;From a client site or while traveling&lt;/li&gt;
&lt;li&gt;For multiple developers working together&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For "I don't want to be tied to a specific PC" needs, MainWP delivers. Self-hosted on the data side, cloud-like on the UI side — the design balances ownership with convenience.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Modular — pick only the extensions you need
&lt;/h2&gt;

&lt;p&gt;MainWP is &lt;strong&gt;modular&lt;/strong&gt;: features are delivered as discrete extensions. Core capabilities (updates, backups, basic monitoring) are free, and you add only the extensions your operation actually needs through the Pro tier.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Need staging? → Staging Extension&lt;/li&gt;
&lt;li&gt;Need detailed reports? → Pro Reports Extension&lt;/li&gt;
&lt;li&gt;Need security scanning? → WordFence Extension&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of "everything bundled for a monthly fee," you configure exactly what your operation requires. The same product accommodates a minimal setup and a fully equipped one, which is a flexibility most all-in-one tools don't offer.&lt;/p&gt;

&lt;h2&gt;
  
  
  What kind of agency MainWP fits
&lt;/h2&gt;

&lt;p&gt;Putting it together, MainWP fits a team with these traits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open source matters as a policy&lt;/li&gt;
&lt;li&gt;You want to manage the dashboard on your own infrastructure&lt;/li&gt;
&lt;li&gt;Lifetime licensing or long-horizon budgeting is preferred&lt;/li&gt;
&lt;li&gt;Security requirements rule out SaaS&lt;/li&gt;
&lt;li&gt;You want to pick only the features you need (avoid forced bundles)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If three or more of these apply, MainWP belongs on your shortlist.&lt;/p&gt;

&lt;h2&gt;
  
  
  How we're different
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;WP Maintenance Manager&lt;/strong&gt; takes a different shape.&lt;/p&gt;

&lt;p&gt;Where MainWP is a &lt;strong&gt;self-hosted dashboard built as a WordPress plugin&lt;/strong&gt;, WP Maintenance Manager is a &lt;strong&gt;desktop application (Mac &amp;amp; Windows) using SSH + WP-CLI&lt;/strong&gt; to reach client sites.&lt;/p&gt;

&lt;p&gt;That trade-off gives up MainWP's "access from anywhere" and built-in multi-user collaboration. In exchange:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No dashboard server to operate&lt;/strong&gt; (it runs on your local PC — no separate WordPress install to maintain)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No Child plugin on client sites&lt;/strong&gt; (SSH-native, so no Worker / Child plugin is needed)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pinpoint rollback&lt;/strong&gt; — if a single plugin update breaks the site, only that one is reverted while the rest of the maintenance continues&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This isn't "which is better." It's &lt;strong&gt;which operating style each fits&lt;/strong&gt;. Open source, self-hosted, team-oriented operations → MainWP. SSH-based, PC-centric, dashboard-overhead-averse operations → WP Maintenance Manager. That's the natural split.&lt;/p&gt;

&lt;h2&gt;
  
  
  For a more detailed comparison
&lt;/h2&gt;

&lt;p&gt;A 12-item objective spec table, pricing model breakdown, parallel-trial steps, and decision-frame analysis are on our comparison page:&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://en.wpmm.jp/vs-mainwp.php" rel="noopener noreferrer"&gt;WP Maintenance Manager vs MainWP — Objective Spec Comparison&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Use it not as a single-tool review, but as input for deciding which fits your operating style.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;MainWP is a remarkably strong tool for agencies that prioritize &lt;strong&gt;open source, self-hosting, and lifetime licensing&lt;/strong&gt;. A decade of track record, GPL transparency, and a stable community give it qualities no SaaS-only competitor can replicate.&lt;/p&gt;

&lt;p&gt;Every maintenance tool has the operating style it fits. MainWP, with its emphasis on transparency and ownership, deserves to top the shortlist for teams that need data sovereignty and source-readable code. WP Maintenance Manager's desktop + SSH approach fits a different operating style.&lt;/p&gt;

&lt;p&gt;The best path is to evaluate several candidates against the operating style of your own team.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>php</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>'Command not found' — and what's really blocking WP-CLI</title>
      <dc:creator>Susumu Takahashi</dc:creator>
      <pubDate>Sat, 06 Jun 2026 05:13:30 +0000</pubDate>
      <link>https://dev.to/susumun/command-not-found-and-whats-really-blocking-wp-cli-55o6</link>
      <guid>https://dev.to/susumun/command-not-found-and-whats-really-blocking-wp-cli-55o6</guid>
      <description>&lt;p&gt;"I downloaded &lt;code&gt;wp-cli.phar&lt;/code&gt;, uploaded it to &lt;code&gt;~/bin/wp&lt;/code&gt;, SSH'd in, and ran &lt;code&gt;wp&lt;/code&gt;. Got &lt;code&gt;-bash: wp: command not found&lt;/code&gt;. The file is right there. Why?"&lt;/p&gt;

&lt;p&gt;If you've set up WP-CLI on a shared host using a browser-based file manager, you may have hit exactly this. The file exists, permissions are 755, size is 6.8 MB (matches the official PHAR). And yet it refuses to run. The "command not found" message can hide an entirely different problem underneath.&lt;/p&gt;

&lt;h2&gt;
  
  
  The file is there, but nothing runs
&lt;/h2&gt;

&lt;p&gt;After SSHing in, you can confirm the file exists:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; ~/bin/wp
&lt;span class="nt"&gt;-rwxr-xr-x&lt;/span&gt; 1 c1234567 c1234567 7142777 May  8 10:00 /home/c1234567/bin/wp

&lt;span class="nv"&gt;$ &lt;/span&gt;wp &lt;span class="nt"&gt;--info&lt;/span&gt;
&lt;span class="nt"&gt;-bash&lt;/span&gt;: wp: &lt;span class="nb"&gt;command &lt;/span&gt;not found
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You might suspect &lt;code&gt;~/bin&lt;/code&gt; isn't on &lt;code&gt;PATH&lt;/code&gt; (the environment variable that lets you run a command by name alone). But calling it by absolute path gives a different, more telling error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;~/bin/wp &lt;span class="nt"&gt;--info&lt;/span&gt;
&lt;span class="nt"&gt;-bash&lt;/span&gt;: /home/c1234567/bin/wp: cannot execute binary file
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"Cannot execute binary file" — at this point, something is wrong with the file itself, not just the shell setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real cause — a PHAR &lt;code&gt;broken signature&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;WP-CLI ships as a single PHAR file (&lt;code&gt;wp-cli.phar&lt;/code&gt;) — a PHP archive format that bundles all of WP-CLI's code into one self-contained executable. PHARs carry a &lt;strong&gt;SHA1 signature&lt;/strong&gt; inside, and PHP verifies that signature when loading the archive. If the bytes don't match what was signed at build time, loading is refused.&lt;/p&gt;

&lt;p&gt;Running &lt;code&gt;wp&lt;/code&gt; through &lt;code&gt;php&lt;/code&gt; directly surfaces the error that was hidden until now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;php ~/bin/wp &lt;span class="nt"&gt;--info&lt;/span&gt;
Fatal error: Uncaught PharException: phar &lt;span class="s2"&gt;"/home/c1234567/bin/wp"&lt;/span&gt;
SHA1 signature could not be verified: broken signature &lt;span class="k"&gt;in&lt;/span&gt;
/home/c1234567/bin/wp:3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the smoking gun. &lt;strong&gt;If a single byte of the PHAR has been altered, the archive refuses to load.&lt;/strong&gt; Which means the PHAR that arrived on the server is not the same one that left the build server — something between download and upload changed it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the browser file manager corrupts it
&lt;/h2&gt;

&lt;p&gt;The typical workflow looked like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Download &lt;code&gt;wp-cli.phar&lt;/code&gt; from the WP-CLI website on a Mac&lt;/li&gt;
&lt;li&gt;Rename it to &lt;code&gt;wp&lt;/code&gt; in Finder (drop the &lt;code&gt;.phar&lt;/code&gt; extension)&lt;/li&gt;
&lt;li&gt;Use the host's &lt;strong&gt;browser-based file manager&lt;/strong&gt; to drag-and-drop the file into &lt;code&gt;~/bin/&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Step 3 is the trap. In the file manager, a file named &lt;code&gt;wp&lt;/code&gt; (with no extension) is shown as a &lt;strong&gt;text file&lt;/strong&gt; icon, and the file properties dialog labels it "Plain Text." Without an extension, MIME detection (the mechanism a file manager uses to guess what kind of file it is) falls back to a text guess, and the upload path applies subtle byte transformations along the way — line-ending conversion, character-encoding normalization, or similar — which corrupt the binary.&lt;/p&gt;

&lt;p&gt;We tested the same procedure across different accounts and time windows. Each upload produced a &lt;strong&gt;different MD5 hash&lt;/strong&gt; (a short fingerprint string used to confirm two files have identical contents) of corruption. It's not deterministic damage; it's some kind of transformation happening in flight.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix — never go through the Mac
&lt;/h2&gt;

&lt;p&gt;The reliable path is to &lt;strong&gt;SSH into the server first, then download with &lt;code&gt;curl&lt;/code&gt; directly on the server&lt;/strong&gt;. Neither Finder nor the file manager touches the file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &lt;span class="nt"&gt;-i&lt;/span&gt; ~/.ssh/&amp;lt;your-private-key.pem&amp;gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 8022 &amp;lt;ssh-user&amp;gt;@&amp;lt;host&amp;gt; &lt;span class="s1"&gt;'
  mkdir -p ~/bin &amp;amp;&amp;amp; cd ~/bin &amp;amp;&amp;amp;
  [ -f wp ] &amp;amp;&amp;amp; mv wp wp.broken_backup_$(date +%s);
  curl -sO https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar &amp;amp;&amp;amp;
  mv wp-cli.phar wp &amp;amp;&amp;amp; chmod 755 wp &amp;amp;&amp;amp;
  ~/bin/wp --info
'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This idempotent one-liner moves any existing &lt;code&gt;wp&lt;/code&gt; to a timestamped backup before pulling a fresh PHAR. The final &lt;code&gt;wp --info&lt;/code&gt; is the proof that it works. The whole thing takes about 30 seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway — binaries go through SSH/SFTP only
&lt;/h2&gt;

&lt;p&gt;Browser file managers are convenient, but they treat unfamiliar files as text by default, and &lt;strong&gt;transformations along the upload path can quietly corrupt binary content&lt;/strong&gt;. WP-CLI is fortunate: the PHAR's SHA1 self-check means corruption fails loud and early. Binaries without internal integrity checks (other executables, archives, image metadata) could carry the same corruption silently into production.&lt;/p&gt;

&lt;p&gt;There's a corollary: hash comparison alone is not enough to declare a PHAR healthy. We observed cases where two PHARs share an identical MD5 but one still fails with &lt;code&gt;broken signature&lt;/code&gt; on first execute. The final "this works" verdict has to come from &lt;code&gt;wp --info&lt;/code&gt;'s exit status (the number a command returns to indicate success or failure — &lt;code&gt;0&lt;/code&gt; means success) and its output — not from a hash.&lt;/p&gt;

&lt;p&gt;For the broader picture of how different shared hosts ship (or don't ship) WP-CLI, see also &lt;a href="https://en.wpmm.jp/blog/wp-cli-cross-host-investigation/" rel="noopener noreferrer"&gt;a four-host investigation of WP-CLI architectures&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>php</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
