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 Server
or:
Quick Actions
→ Upload Theme to Server
Step 1: Create the Plugin Upload Command on macOS
Open Terminal.
Create:
sudo nano /usr/local/bin/upload_plugin
Paste:
#!/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_plugin
Test it from Terminal:
upload_plugin ~/Downloads/plugin.zip
If 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_theme
Paste:
#!/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_theme
Test:
upload_theme ~/Downloads/theme.zip
Step 3: Create the Plugin Finder Quick Action
Open:
Applications → Automator
Choose:
New Document
Then choose:
Quick Action
At the top of the workflow configure:
Workflow receives current:
files or folders
in:
Finder
Search Automator actions for:
Run Shell Script
Drag Run Shell Script into the workflow.
Set:
Shell:
/bin/bash
Set:
Pass input:
as arguments
Paste:
#!/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
fi
Save it as:
Upload Plugin to Server
You 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 arguments
Use:
#!/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
fi
Save it as:
Upload Theme to Server
You will now have two Finder Quick Actions:
Upload Plugin to Server
Upload Theme to Server
Both 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 ed25519
Then install the public key on your server:
ssh-copy-id [email protected]
If ssh-copy-id is unavailable on your Mac, you can manually add the contents of:
~/.ssh/id_ed25519.pub
to:
~/.ssh/authorized_keys
on 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 To
Modern Windows installations include the Microsoft OpenSSH client, which gives us both:
ssh.exe
scp.exe
Unlike macOS, Windows does not normally include rsync, so this example uses scp.
For uploading ZIP files, scp works perfectly well.
Step 1: Verify SSH on Windows
Open PowerShell and run:
ssh
You should see the OpenSSH usage information.
You can also verify:
Get-Command ssh
Get-Command scp
If 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 ed25519
The key is normally stored in:
C:\Users\YOUR_USERNAME\.ssh\
The public key will be:
id_ed25519.pub
Add that public key to the server’s:
~/.ssh/authorized_keys
Then 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:\Scripts
Create:
C:\Scripts\Upload-Plugin.ps1
Paste:
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 0
Change:
$Remote = "[email protected]"
to your server.
Step 4: Create the Windows Theme Upload Script
Create:
C:\Scripts\Upload-Theme.ps1
Use:
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 + R
Enter:
shell:sendto
Press Enter.
Windows Explorer will open your personal SendTo directory.
Create the Plugin Send To Command
Create a file named:
Upload Plugin to Server.cmd
Inside 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 Server
Depending 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.cmd
Use:
@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.zip
and send all three to:
Upload Plugin to Server
The 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.zip
might contain:
admin-columns-pro/
and its WordPress plugin header may say:
Plugin Name: Admin Columns Pro
Because of this, the server-side plugin updater should not assume:
ZIP filename = installed plugin folder
A 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.zip
to still update the correct WordPress plugin.
Themes Work Slightly Differently
WordPress themes normally have:
style.css
in the root of the theme directory.
The theme updater can therefore inspect:
style.css
to 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 flush
for 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.com
there 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
ACLs
Deleting 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 nginx
If 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_themes
For example:
ln -s \
/usr/local/sbin/update_plugins_from_zips \
/usr/local/sbin/update_plugins
and:
ln -s \
/usr/local/sbin/update_themes_from_zips \
/usr/local/sbin/update_themes
You can then manually test:
update_plugins --test
or:
update_themes --test
before 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 --test
A good theme test is:
update_themes --test
The 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 applied
but should not:
modify files
delete ZIPs
flush caches
delete cache files
restart PHP
restart Nginx
Once 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 sudo permissions for only the commands required by the updater.
You should also validate uploaded ZIP files before processing them.
For example:
unzip -tq plugin.zip
can 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 Server
or:
Upload Theme to Server
On Windows:
Download ZIP
↓
Right-click
↓
Send to
↓
Upload Plugin to Server
or:
Upload Theme to Server
The 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.
































