How to Update Private WordPress Plugins from GitHub
Self-hosted WordPress plugin updates work by adding your own version information to the update_plugins site transient, so WordPress shows the update and downloads the ZIP from a URL you control.
If you write plugins for clients, or keep your own private toolkit, you’ve hit this problem: WordPress only offers one-click updates for plugins from WordPress.org. Everything else means downloading a ZIP, logging into each site and uploading it again. This guide explains how plugin updates actually work and how to get the same Update now button for private plugins, with GitHub as the source.
How WordPress checks for updates
Twice a day (wp_schedule_update_checks()), WordPress sends the list of installed plugins and their versions to api.wordpress.org and gets back which ones have a newer version and where to download it. The result is stored in the update_plugins site transient.
Plugins that aren’t on WordPress.org can take part in the same process: they hook into that transient (pre_set_site_transient_update_plugins) and add their own answer, “version 1.2.0 is available at this ZIP URL”. WordPress then shows the normal update notice, and Update now installs it like any other plugin. That piece of code is called an updater.
A self-hosted update is not a custom updater screen. WordPress’s own update notices and one-click installs are reused; only the source of the version information changes from api.wordpress.org to your server.
Where a self-hosted plugin hooks in
Since WordPress 5.8 there is a hook made for this. Add an Update URI header to the plugin, and WordPress calls the filter update_plugins_{hostname} for it, where the hostname comes from that header. The header also stops WordPress.org from offering an update for a plugin that happens to share your slug.
<?php
/**
* Plugin Name: My Plugin
* Version: 1.0.0
* Update URI: https://updates.example.com/my-plugin
*/
add_filter( 'update_plugins_updates.example.com', function ( $update, $plugin_data, $plugin_file ) {
if ( plugin_basename( __FILE__ ) !== $plugin_file ) {
return $update;
}
$remote = get_transient( 'my_plugin_update' );
if ( false === $remote ) {
$response = wp_remote_get( 'https://updates.example.com/my-plugin.json' );
if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
return $update;
}
$remote = json_decode( wp_remote_retrieve_body( $response ), true );
set_transient( 'my_plugin_update', $remote, 12 * HOUR_IN_SECONDS );
}
return array(
'slug' => 'my-plugin',
'version' => $remote['version'],
'url' => $remote['homepage'],
'package' => $remote['download_url'],
);
}, 10, 3 );
Older updaters filter the transient directly (pre_set_site_transient_update_plugins or site_transient_update_plugins). Both ways end in the same place. For the View details popup, filter plugins_api as well.
What the update server returns
A small JSON file is enough. These are the fields WordPress can show:
| Field | Used for |
|---|---|
version | Compared with the installed version |
download_url | The ZIP that Update now installs |
requires, tested, requires_php | Compatibility notes on the update screen |
sections (description, changelog) | The View details popup |
banners, icons | Images in the popup and the updates list |
Cache the answer in a transient, as in the example, so a site doesn’t call your server on every admin page load.
The DIY route
You need two things:
- An update source that returns the latest version number, the download URL and (ideally) the changelog.
- An updater inside the plugin that asks that source and injects the answer into WordPress’s update transient.
With GitHub as the source, a common pattern is to tag a release (v1.2.0), build the plugin ZIP in a GitHub Action, attach it to the release, and have the updater read GitHub’s “latest release” API. It works, but you’re maintaining the update logic yourself, including private-repository tokens, rate limits, changelogs and the “View details” screen. A ready-made library for this route is Plugin Update Checker.
The self-hosted route with Hoster
Hoster is the update server we build at DPlugins, developed by Marko Krstić, and the way we ship our own plugin updates. It turns one WordPress site into an update server for your plugins and themes (Create Plugin Update).
- Create a download for your plugin in Hoster (a name, version, required and tested WordPress versions, author info and changelog).
- Add the updater to your plugin: an update file inside an
inc/folder, which Hoster uses to deliver updates to every site running that plugin. - Connect GitHub (optional): add your GitHub user and repository, and Hoster fetches the latest release ZIP and version number automatically. Public and private repositories both work.
- Release an update: raise the version number, write the changelog and publish the release (or upload a new ZIP). Every site with the plugin sees the update notice and the changelog, and updates with one click.
A typical plugin structure:
my-plugin/
├── my-plugin.php
├── assets/
│ └── style.css
└── inc/
└── update.php
How licences fit into the flow
For paid plugins, the update request carries the customer’s licence key. With Hoster, the server checks the key for that site. A valid key gets a signed download link that expires; an invalid or expired key gets the version information and a message, but no download link. The plugin keeps working, it just stops receiving updates until the licence is renewed.
A GitHub release workflow
To make releases hands-off, keep only source files in Git (add node_modules/ and .DS_Store to .gitignore) and let a GitHub Action build and publish the release when you push a tag:
git tag v1.2.0
git push origin v1.2.0
The workflow packages the plugin as a ZIP and attaches it to the release; Hoster picks up the new version from there.
Why not just use WordPress.org?
WordPress.org is great for public plugins, but it has rules: code review, restrictions on what you can include and load from external services, and it’s public by definition. Client-specific plugins, premium plugins and internal tools don’t belong there. Self-hosting keeps them private and under your control. For the full trade-off, see self-hosted plugin updates vs WordPress.org.
FAQ
Can I host a WordPress plugin on GitHub and get automatic updates?
Yes: publish a release with a ZIP and add an updater that reads it. Private repositories need a token, and GitHub’s API has rate limits.
What is the Update URI plugin header?
A plugin header added in WordPress 5.8. It tells WordPress where the plugin’s updates come from, prevents WordPress.org from offering an update for the same slug, and enables the update_plugins_{hostname} filter.
Do self-hosted plugins show “View details”?
Only if the updater also filters plugins_api and returns the description and changelog.
What happens when a licence expires?
The plugin keeps working. It stops receiving updates, because the update server no longer returns a download link.
Sources
- update_plugins_{$hostname}, WordPress Code Reference, since 5.8.0 (read 2 October 2026)
- Header Requirements, Plugin Handbook: the
Update URIheader (read 2 October 2026) - plugins_api, WordPress Code Reference (read 2 October 2026)
- wp_schedule_update_checks(), WordPress Code Reference (read 2 October 2026)
- Create Plugin Update and GitHub, Hoster documentation (read 2 October 2026)
Tested with WordPress 7.1.2: the Update URI example offers version 1.2.0 for an installed 1.0.0.
Related guides
- Nulled WordPress Plugins: What You Really Get (and a Safer Way to Save Money): Backdoors, no updates, and a safer way to save money.
- How to Enable and Read the WordPress Error Log: Turn on logging and read errors, without FTP.
- How to Search and Replace in the WordPress Database Safely: Change URLs site-wide without breaking data.
- How to Regenerate WordPress Salts and Security Keys: Log everyone out and kill stolen cookies, safely.
Browse all WordPress tutorials.
