Automatically Upload WordPress Plugin and Theme Updates from macOS and Windows
If you manage multiple WordPress websites on your own Linux server, manually uploading premium plugin and theme updates to every site can become tedious.
A much faster workflow is to create a right-click action on your computer.
With this setup, you can:
- Download a premium plugin or theme ZIP.
- Right-click the ZIP in Finder or Windows Explorer.
- Choose Upload Plugin to Server or Upload Theme to Server.
- Upload the ZIP securely over SSH.
- Let the server automatically detect the completed upload.
- Update only sites where that plugin or theme already exists.
- Clear WordPress caches.
- Clear filesystem caches.
- Restart PHP-FPM and Nginx if required.
- Delete the processed ZIP from the server.
This is especially useful when managing many WordPress installations, including a mixture of traditional WordPress installations and Bedrock installations.
Using this process allows me to upload plugins or themes once directly from my desktop, and then my server auto updates every site on my server automatically.
This guide covers the desktop-side automation for both macOS and Windows.
How the Workflow Works
The basic workflow looks like this:
Plugin or Theme ZIP
↓
Right-click in Finder / Explorer
↓
Upload through SSH
↓
Temporary filename on server
↓
Upload completes
↓
Rename to .zip
↓
Linux watcher detects ZIP
↓
Plugin or theme updater runs
↓
Existing installations are updated
↓
Caches are cleared
↓
ZIP is deleted
One important design decision is that the uploaded file should initially use a temporary extension such as:
.uploading
and only be renamed to:
.zip
after the transfer finishes.
For example:
gravityforms.zip
might initially be uploaded as:
.gravityforms.zip.uploading
and then renamed on the server to:
gravityforms.zip
This prevents a server-side file watcher from trying to process a partially uploaded ZIP.
Server Requirements
This guide assumes your Linux server already has separate directories for incoming plugin and theme updates.
For example:
/root/plugins/
/root/themes/
The plugin watcher monitors:
/root/plugins/
while the theme watcher monitors:
/root/themes/
Your server might use different directories. If so, simply change the paths in the scripts below.
You will also need SSH access to the server.
For example:
[email protected]
or:
[email protected]
For automation, SSH key authentication is strongly recommended.
You should be able to run:
ssh [email protected]
without being prompted for the server account password every time.
You will need to update the instructions to match your server settings/credentials
Settings You Need to Customize
Throughout this guide you will see settings similar to:
REMOTE="[email protected]"
and:
REMOTE_DIR="/root/plugins"
Change these values for your own server.
For plugins:
REMOTE="[email protected]"
REMOTE_DIR="/root/plugins"
For themes:
REMOTE="[email protected]"
REMOTE_DIR="/root/themes"
If your SSH username is not root, use your actual account:
REMOTE="[email protected]"
If your server uses an IP address:
REMOTE="[email protected]"
If SSH runs on a non-standard port, such as port 2222, your scripts will also need the appropriate SSH options.
For example:
ssh -p 2222
and with rsync:
rsync -avP -e "ssh -p 2222"macOS Setup
macOS has an excellent built-in tool called Automator that can create Finder Quick Actions.
The final result will allow you to right-click a ZIP file and choose:
Quick Actions → Upload Plugin to Serveror:
Quick Actions → Upload Theme to Server
Step 1: Create the Plugin Upload Command on macOS
Open Terminal.
Create:
sudo nano /usr/local/bin/upload_pluginPaste:
#!/bin/bash REMOTE="[email protected]" REMOTE_DIR="/root/plugins" if [ "$#" -eq 0 ]; then echo "Usage: upload_plugin plugin.zip [plugin2.zip ...]" exit 1 fi FAILED=0 for FILE in "$@"; do if [ ! -f "$FILE" ]; then echo "ERROR: File not found: $FILE" FAILED=1 continue fi case "$FILE" in *.zip|*.ZIP) ;; *) echo "ERROR: Not a ZIP file: $FILE" FAILED=1 continue ;; esac FILENAME=$(basename "$FILE") REMOTE_TEMP="$REMOTE_DIR/.${FILENAME}.uploading" REMOTE_FINAL="$REMOTE_DIR/$FILENAME" echo echo "Uploading plugin: $FILENAME" if ! rsync \ -avP \ "$FILE" \ "$REMOTE:$REMOTE_TEMP"; then echo "ERROR: Upload failed." FAILED=1 continue fi if ! ssh "$REMOTE" \ "mv -- '$REMOTE_TEMP' '$REMOTE_FINAL'"; then echo "ERROR: Could not finalize upload." FAILED=1 continue fi echo "Plugin upload complete: $FILENAME" done exit "$FAILED"Change:
REMOTE="[email protected]"to your actual SSH server.
Then make the script executable:
sudo chmod 755 /usr/local/bin/upload_pluginTest it from Terminal:
upload_plugin ~/Downloads/plugin.zipIf everything is configured correctly, the ZIP should appear in:
/root/plugins/on your server.
Step 2: Create the Theme Upload Command on macOS
Create:
sudo nano /usr/local/bin/upload_themePaste:
#!/bin/bash REMOTE="[email protected]" REMOTE_DIR="/root/themes" if [ "$#" -eq 0 ]; then echo "Usage: upload_theme theme.zip [theme2.zip ...]" exit 1 fi FAILED=0 for FILE in "$@"; do if [ ! -f "$FILE" ]; then echo "ERROR: File not found: $FILE" FAILED=1 continue fi case "$FILE" in *.zip|*.ZIP) ;; *) echo "ERROR: Not a ZIP file: $FILE" FAILED=1 continue ;; esac FILENAME=$(basename "$FILE") REMOTE_TEMP="$REMOTE_DIR/.${FILENAME}.uploading" REMOTE_FINAL="$REMOTE_DIR/$FILENAME" echo echo "Uploading theme: $FILENAME" if ! rsync \ -avP \ "$FILE" \ "$REMOTE:$REMOTE_TEMP"; then echo "ERROR: Upload failed." FAILED=1 continue fi if ! ssh "$REMOTE" \ "mv -- '$REMOTE_TEMP' '$REMOTE_FINAL'"; then echo "ERROR: Could not finalize upload." FAILED=1 continue fi echo "Theme upload complete: $FILENAME" done exit "$FAILED"Make it executable:
sudo chmod 755 /usr/local/bin/upload_themeTest:
upload_theme ~/Downloads/theme.zip
Step 3: Create the Plugin Finder Quick Action
Open:
Applications → AutomatorChoose:
New DocumentThen choose:
Quick ActionAt the top of the workflow configure:
Workflow receives current: files or folders in: FinderSearch Automator actions for:
Run Shell ScriptDrag Run Shell Script into the workflow.
Set:
Shell: /bin/bashSet:
Pass input: as argumentsPaste:
#!/bin/bash if /usr/local/bin/upload_plugin "$@"; then /usr/bin/osascript -e \ 'display notification "Plugin upload completed." with title "WordPress Plugin Upload"' else /usr/bin/osascript -e \ 'display notification "Plugin upload failed." with title "WordPress Plugin Upload"' exit 1 fiSave it as:
Upload Plugin to ServerYou can now right-click a ZIP file in Finder and choose:
Quick Actions → Upload Plugin to Server
Step 4: Create the Theme Finder Quick Action
Create another Automator Quick Action.
Use the same settings:
Workflow receives: files or folders Application: Finder Shell: /bin/bash Pass input: as argumentsUse:
#!/bin/bash if /usr/local/bin/upload_theme "$@"; then /usr/bin/osascript -e \ 'display notification "Theme upload completed." with title "WordPress Theme Upload"' else /usr/bin/osascript -e \ 'display notification "Theme upload failed." with title "WordPress Theme Upload"' exit 1 fiSave it as:
Upload Theme to ServerYou will now have two Finder Quick Actions:
Upload Plugin to Server Upload Theme to ServerBoth can also handle multiple selected ZIP files.
macOS SSH Keys
The Finder automation works best when SSH authentication does not require entering your server password.
If you do not already have an SSH key, create one:
ssh-keygen -t ed25519Then install the public key on your server:
ssh-copy-id [email protected]If
ssh-copy-idis unavailable on your Mac, you can manually add the contents of:~/.ssh/id_ed25519.pubto:
~/.ssh/authorized_keyson the server.
Test:
ssh [email protected]Once that works without asking for the SSH account password, the Automator Quick Action should work without user interaction.
Windows Setup
Windows does not have Automator, but the same functionality can be created using:
PowerShell + Windows Explorer Send ToModern Windows installations include the Microsoft OpenSSH client, which gives us both:
ssh.exe scp.exeUnlike macOS, Windows does not normally include
rsync, so this example usesscp.For uploading ZIP files,
scpworks perfectly well.
Step 1: Verify SSH on Windows
Open PowerShell and run:
sshYou should see the OpenSSH usage information.
You can also verify:
Get-Command ssh Get-Command scpIf Windows cannot find these commands, install OpenSSH Client through Windows Optional Features.
Step 2: Configure SSH Key Authentication on Windows
Create an SSH key:
ssh-keygen -t ed25519The key is normally stored in:
C:\Users\YOUR_USERNAME\.ssh\The public key will be:
id_ed25519.pubAdd that public key to the server's:
~/.ssh/authorized_keysThen test:
ssh [email protected]The connection should work without requiring the SSH account password.
Step 3: Create the Windows Plugin Upload Script
Create a folder such as:
C:\ScriptsCreate:
C:\Scripts\Upload-Plugin.ps1Paste:
param( [Parameter(ValueFromRemainingArguments = $true)] [string[]]$Files ) $Remote = "[email protected]" $RemoteDir = "/root/plugins" if (-not $Files -or $Files.Count -eq 0) { Write-Host "Usage: Upload-Plugin.ps1 plugin.zip" exit 1 } $Failed = $false foreach ($File in $Files) { if (-not (Test-Path -LiteralPath $File -PathType Leaf)) { Write-Host "ERROR: File not found: $File" $Failed = $true continue } if ([System.IO.Path]::GetExtension($File) -ne ".zip") { Write-Host "ERROR: Not a ZIP file: $File" $Failed = $true continue } $Filename = [System.IO.Path]::GetFileName($File) $RemoteTemp = "$RemoteDir/.$Filename.uploading" $RemoteFinal = "$RemoteDir/$Filename" Write-Host "" Write-Host "Uploading plugin: $Filename" & scp.exe $File "${Remote}:${RemoteTemp}" if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: Upload failed." $Failed = $true continue } & ssh.exe $Remote "mv -- '$RemoteTemp' '$RemoteFinal'" if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: Could not finalize upload." $Failed = $true continue } Write-Host "Plugin upload complete: $Filename" } if ($Failed) { exit 1 } exit 0Change:
$Remote = "[email protected]"to your server.
Step 4: Create the Windows Theme Upload Script
Create:
C:\Scripts\Upload-Theme.ps1Use:
param( [Parameter(ValueFromRemainingArguments = $true)] [string[]]$Files ) $Remote = "[email protected]" $RemoteDir = "/root/themes" if (-not $Files -or $Files.Count -eq 0) { Write-Host "Usage: Upload-Theme.ps1 theme.zip" exit 1 } $Failed = $false foreach ($File in $Files) { if (-not (Test-Path -LiteralPath $File -PathType Leaf)) { Write-Host "ERROR: File not found: $File" $Failed = $true continue } if ([System.IO.Path]::GetExtension($File) -ne ".zip") { Write-Host "ERROR: Not a ZIP file: $File" $Failed = $true continue } $Filename = [System.IO.Path]::GetFileName($File) $RemoteTemp = "$RemoteDir/.$Filename.uploading" $RemoteFinal = "$RemoteDir/$Filename" Write-Host "" Write-Host "Uploading theme: $Filename" & scp.exe $File "${Remote}:${RemoteTemp}" if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: Upload failed." $Failed = $true continue } & ssh.exe $Remote "mv -- '$RemoteTemp' '$RemoteFinal'" if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: Could not finalize upload." $Failed = $true continue } Write-Host "Theme upload complete: $Filename" } if ($Failed) { exit 1 } exit 0
Step 5: Create Windows Right-Click Actions
One of the easiest ways to add custom commands to Windows Explorer is through the user's Send To folder.
Press:
Windows + REnter:
shell:sendtoPress Enter.
Windows Explorer will open your personal SendTo directory.
Create the Plugin Send To Command
Create a file named:
Upload Plugin to Server.cmdInside it put:
@echo off powershell.exe -NoProfile -ExecutionPolicy Bypass ^ -File "C:\Scripts\Upload-Plugin.ps1" %* if errorlevel 1 ( echo. echo Plugin upload failed. pause )Save it inside the SendTo folder.
Now you can right-click a ZIP file and choose:
Show more options → Send to → Upload Plugin to ServerDepending on your Windows version, Send to may appear directly in the classic context menu.
Create the Theme Send To Command
Create:
Upload Theme to Server.cmdUse:
@echo off powershell.exe -NoProfile -ExecutionPolicy Bypass ^ -File "C:\Scripts\Upload-Theme.ps1" %* if errorlevel 1 ( echo. echo Theme upload failed. pause )You will now have:
Send to → Upload Plugin to Server → Upload Theme to Server
Supporting Multiple Selected Files on Windows
Both PowerShell scripts accept multiple filenames.
That means you can select:
gravityforms.zip advanced-custom-fields-pro.zip admin-columns-pro.zipand send all three to:
Upload Plugin to ServerThe files will be uploaded one at a time.
The same works with multiple theme ZIP files.
Why Plugin ZIP Filenames Should Not Be Trusted
Premium plugin downloads frequently include version numbers or vendor-specific filenames.
For example:
admin-columns-pro-6.7.1.zipmight contain:
admin-columns-pro/and its WordPress plugin header may say:
Plugin Name: Admin Columns ProBecause of this, the server-side plugin updater should not assume:
ZIP filename = installed plugin folderA better updater:
- Extracts the ZIP.
- Locates the main PHP file containing:
Plugin Name:
- Determines the actual plugin directory.
- Looks for that directory on each WordPress site.
- If the folder name differs, compares the actual WordPress
Plugin Name:header.- Updates the matching existing installation.
This allows files such as:
plugin-pro-latest.zip plugin-pro-6.4.7.zip vendor-download-12345.zipto still update the correct WordPress plugin.
Themes Work Slightly Differently
WordPress themes normally have:
style.cssin the root of the theme directory.
The theme updater can therefore inspect:
style.cssto identify a valid theme package.
A typical theme looks like:
generatepress/ style.css functions.php assets/The updater should only replace:
wp-content/themes/generatepress/or, with Bedrock:
app/themes/generatepress/if that theme already exists.
A missing theme should not automatically be installed unless that behavior is explicitly desired.
Traditional WordPress vs Bedrock
A traditional WordPress installation generally uses:
wp-content/plugins/ wp-content/themes/For example:
/var/www/sites/example.com/www/wp-content/plugins/ /var/www/sites/example.com/www/wp-content/themes/A Bedrock installation typically uses:
app/plugins/ app/themes/For example:
/var/www/sites/example.com/web/app/plugins/ /var/www/sites/example.com/web/app/themes/Your server updater should search both structures.
Clearing WordPress Cache After an Update
If a plugin or theme is updated, it is useful to run:
wp cache flushfor that WordPress installation.
When websites run under separate Linux accounts, WP-CLI should ideally run as the account that owns that website.
For example:
sudo -u site_example_com -H \ wp cache flush \ --path="/var/www/sites/example.com/www"A Bedrock installation may require a different WP-CLI path depending on its directory layout.
The updater should remember every site that was successfully changed and flush each site only once.
For example, if three plugins are updated on:
example.comthere is no need to flush that site's cache three times.
Flush it once after all plugin updates have finished.
Clearing a Filesystem Cache Directory
Some server configurations also have a directory such as:
/var/www/sites/example.com/redis_cache/If you need to empty this directory after updates, delete its contents rather than deleting the directory itself.
For example:
find "/var/www/sites/example.com/redis_cache" \ -mindepth 1 \ -maxdepth 1 \ -exec rm -rf -- {} +This preserves the parent directory.
That is important when the directory has custom:
ownership group permissions SGID ACLsDeleting and recreating the entire directory could lose those settings.
Restarting PHP-FPM
If your server runs multiple PHP versions, you can restart all installed PHP-FPM services with:
systemctl list-units \ --type=service \ --all \ "php*-fpm.service" \ --no-legend | awk '{print $1}' | xargs -r -I{} systemctl restart {}This works well on servers hosting sites across several PHP versions.
For example:
php8.2-fpm.service php8.3-fpm.service php8.4-fpm.service php8.5-fpm.service
Restarting Nginx
After processing updates:
systemctl restart nginxIf your plugin or theme changes do not require Nginx to restart, this step can be omitted.
In many environments, restarting PHP-FPM and clearing caches is sufficient.
Recommended Server Directories
A simple layout is:
/root/ plugins/ themes/Use:
/root/plugins/for plugin ZIPs.
Use:
/root/themes/for theme ZIPs.
Keep the watchers separate so that uploading a theme never triggers the plugin updater and vice versa.
Recommended Linux Commands
For convenience, you can expose your updater scripts using commands such as:
update_plugins update_themesFor example:
ln -s \ /usr/local/sbin/update_plugins_from_zips \ /usr/local/sbin/update_pluginsand:
ln -s \ /usr/local/sbin/update_themes_from_zips \ /usr/local/sbin/update_themesYou can then manually test:
update_plugins --testor:
update_themes --testbefore allowing the automatic file watcher to process uploads.
Testing Before Going Live
Always test your updater scripts before relying on the automatic workflow.
A good plugin test is:
update_plugins --testA good theme test is:
update_themes --testThe test mode should display:
which sites would be updated which folder would be replaced which files rsync would change which ownership would be applied which permissions would be appliedbut should not:
modify files delete ZIPs flush caches delete cache files restart PHP restart NginxOnce the dry-run output looks correct, run the updater normally.
Security Notes
This workflow gives your workstation the ability to place files on your web server, so secure it appropriately.
SSH keys should be protected, and root SSH access may not be appropriate for every environment.
A more restrictive configuration could use a dedicated deployment user that only has permission to upload files to:
/root/plugins/and:
/root/themes/or equivalent deployment directories.
That account could then use tightly controlled
sudopermissions for only the commands required by the updater.You should also validate uploaded ZIP files before processing them.
For example:
unzip -tq plugin.zipcan verify that the ZIP itself is structurally valid before extracting it.
Final Workflow
Once configured, updating premium WordPress software becomes extremely simple.
On macOS:
Download ZIP ↓ Right-click ↓ Quick Actions ↓ Upload Plugin to Serveror:
Upload Theme to ServerOn Windows:
Download ZIP ↓ Right-click ↓ Send to ↓ Upload Plugin to Serveror:
Upload Theme to ServerThe server handles everything else.
For administrators managing a large collection of WordPress websites, this removes most of the repetitive work involved in distributing premium plugin and theme updates while still ensuring that software is only updated on sites where it is already installed.
































