An IntelliJ IDEA plugin that provides an HTTP REST endpoint for dynamically reloading plugins without IDE restart.
-
HTTP REST Endpoint (
/api/plugin-hot-reload):GET: Returns this README documentationPOST: Accepts a plugin .zip file and performs hot reload with streaming progress output
-
Streaming Progress: POST responses stream progress updates in real-time using chunked transfer encoding
-
IDE Notifications: Shows balloon notifications in the IDE for reload progress and results
-
Process Discovery: Creates a marker file
.<pid>.hot-reloadin the user's home directory for external tools to discover running IDE instances -
Authentication: POST requests require a Bearer token that's stored in the marker file
-
Nested Jar Support: Extracts plugin ID from both flat structure (
META-INF/plugin.xml) and IntelliJ's standard nested jar structure (plugin-name/lib/plugin-name.jarcontainingMETA-INF/plugin.xml) -
Self-Reload Prevention: The hot-reload plugin cannot reload itself (the reload code would be unloaded mid-execution). Attempting to do so returns a clear error message.
-
Gradle Integration: Provides
deployPlugintask for plugin developers using IntelliJ Platform Gradle Plugin
- IntelliJ IDEA 2025.3 or later (build 253+)
- Java 21+
This plugin relies heavily on IntelliJ Platform internal APIs (@ApiStatus.Internal) because there is no public API for dynamic plugin loading/unloading.
Internal APIs used:
DynamicPlugins.checkCanUnloadWithoutRestart()- Check if plugin supports hot reloadDynamicPlugins.unloadPlugins()- Unload a plugin (must use plural version!)DynamicPlugins.UnloadPluginOptions- Unload configurationloadDescriptorFromArtifact()- Returns internalPluginMainDescriptortypePluginInstaller.installAndLoadDynamicPlugin()- Takes internalIdeaPluginDescriptorImpltype
This plugin may break in future IntelliJ releases. See and join the discussion: https://youtrack.jetbrains.com/issue/IJPL-224753/Provide-API-to-dynamically-reload-a-plugin
Also related bug with workaround https://youtrack.jetbrains.com/issue/IJPL-225253/IdeScriptEngineManagerImpl.AllPluginsLoader-leak-classloader
- Download the plugin zip from Releases
- In IntelliJ: Settings > Plugins > Install Plugin from Disk
- Restart IDE
If you're developing an another IntelliJ plugin using the IntelliJ Platform Gradle Plugin, add this task to your build.gradle.kts:
import java.net.HttpURLConnection
import java.net.URI
val deployPlugin by tasks.registering {
group = "intellij platform"
description = "Deploy plugin to running IDEs"
dependsOn(tasks.named("buildPlugin"))
doLast {
val zip = tasks.named("buildPlugin").get().outputs.files.singleFile
val home = File(System.getProperty("user.home"))
val endpoints = home.listFiles { f -> f.name.matches(Regex("\\.\\d+\\.hot-reload")) }
?.mapNotNull { f ->
val pid = Regex("\\.(\\d+)\\.").find(f.name)?.groupValues?.get(1)?.toLongOrNull() ?: return@mapNotNull null
if (!ProcessHandle.of(pid).isPresent) return@mapNotNull null
val lines = f.readLines().takeIf { it.size >= 2 } ?: return@mapNotNull null
lines[0] to lines[1]
}?.distinctBy { it.first } ?: emptyList()
if (endpoints.isEmpty()) { println("No running IDEs found"); return@doLast }
endpoints.forEach { (url, token) ->
println("\n→ $url")
val conn = (URI(url).toURL().openConnection() as HttpURLConnection).apply {
requestMethod = "POST"; doOutput = true
setRequestProperty("Authorization", token)
setRequestProperty("Content-Type", "application/octet-stream")
connectTimeout = 5000; readTimeout = 300000
}
conn.outputStream.use { out -> zip.inputStream().use { it.copyTo(out) } }
if (conn.responseCode in 200..299) {
conn.inputStream.bufferedReader().forEachLine { println(" $it") }
} else {
println(" ✗ HTTP ${conn.responseCode}")
}
}
}
}Then run:
./gradlew deployPluginThis will:
- Build your plugin
- Find all running IDEs with the hot-reload plugin installed
- Deploy your plugin to each IDE with streaming progress output
First, get the token from the marker file:
# Find the marker file (PID varies)
ls ~/.*hot-reload
# Read the token (second line)
TOKEN=$(sed -n '2p' ~/.<pid>.hot-reload)
# Read the URL abs IntelliJ port number may chnage
URL=$(sed -n '1p' ~/.<pid>.hot-reload)
# Deploy the plugin with streaming output
# -N disables buffering for real-time streaming
curl -N -X POST -H "Authorization: $TOKEN" --data-binary @my-plugin.zip $URL
Important: Use curl -N (or --no-buffer) to see streaming output in real-time. Without it, curl buffers the response and only shows output when complete.
Example streaming output:
Starting plugin hot reload, zip size: 7,601,343 bytes
Extracting plugin ID from zip...
Plugin ID: com.example.my-plugin
Looking for existing plugin...
Existing plugin: My Plugin, path: /path/to/plugins/my-plugin
Unloading existing plugin: My Plugin
Plugin unloaded successfully
Removing old plugin at: /path/to/plugins/my-plugin
Old plugin folder removed
Loading plugin descriptor from zip...
Installing and loading plugin: My Plugin (1.0.0)
Plugin My Plugin reloaded successfully
SUCCESS
The POST endpoint returns a streaming text/plain response with chunked transfer encoding. Progress messages appear one per line as each step completes.
The last line is always either SUCCESS or FAILED.
Error messages are prefixed with ERROR: .
The plugin creates a marker file at ~/.<pid>.hot-reload with the following format:
http://localhost:63342/api/plugin-hot-reload
Bearer <random-uuid-token>
2024-01-15T10:30:00+01:00
IntelliJ IDEA 2025.3
Build #IU-253.12345
Built on January 1, 2025
Runtime version: 21.0.1+12-39
VM: OpenJDK 64-Bit Server VM by JetBrains s.r.o.
OS: Mac OS X 14.0 (aarch64)
GC: G1 Young Generation, G1 Old Generation
Memory: 2048 MB
- Line 1: POST URL for hot-reload endpoint
- Line 2: Bearer token for authentication
- Line 3: Marker file creation timestamp
- Line 4+: IDE information (similar to Help > About > Copy)
The plugin reload process:
-
Extract plugin ID from the uploaded zip file
- First checks for
META-INF/plugin.xmlat the top level (flat structure) - If not found, searches inside jars in
*/lib/*.jarforMETA-INF/plugin.xml(nested structure) - This handles both development builds and production IntelliJ plugin zips
- First checks for
-
Check for self-reload - if the plugin ID matches the hot-reload plugin itself, reject with error (cannot reload ourselves)
-
Find existing plugin by ID using
PluginManagerCore.getPlugin(pluginId) -
Check if dynamic unload is possible using
DynamicPlugins.checkCanUnloadWithoutRestart() -
Unload existing plugin using
DynamicPlugins.unloadPlugin() -
Delete old plugin folder (renames to
.old.<timestamp>first for safety, then deletes) -
Load plugin descriptor from the zip using
loadDescriptorFromArtifact() -
Install and load the new plugin using
PluginInstaller.installAndLoadDynamicPlugin()
Progress is streamed to the HTTP response in real-time using chunked transfer encoding, and also shown as IDE balloon notifications.
Note: Not all plugins support dynamic reload. If dynamic reload fails, an IDE restart will be required.
- Cannot reload itself: The hot-reload plugin cannot reload itself. To update the hot-reload plugin, restart the IDE.
- Not all plugins support dynamic reload: Some plugins have extensions or services that prevent unloading. The plugin will report this and may require an IDE restart.
- Memory leaks possible: If a plugin doesn't properly clean up resources, memory leaks may occur after reload.
# Build the plugin
./gradlew build
# Run tests
./gradlew test
# Run specific tests
./gradlew test --tests "*PluginHotReloadServiceTest*"
# Run plugin in a sandboxed IntelliJ instance
./gradlew runIde
# Build distributable plugin ZIP
./gradlew buildPlugin
# Deploy to all running IDEs
./gradlew deployPluginThe built plugin will be in build/distributions/.
src/main/kotlin/com/jonnyzzz/intellij/hotreload/
├── HotReloadBundle.kt # Message bundle for i18n
├── HotReloadHttpHandler.kt # HTTP REST endpoint with streaming response
├── HotReloadMarkerService.kt # Marker file management (Disposable)
├── HotReloadNotifications.kt # IDE balloon notifications
├── HotReloadStartupActivity.kt # Startup trigger for marker service
└── PluginHotReloadService.kt # Plugin reload business logic with progress callbacks
src/main/resources/
├── META-INF/plugin.xml # Plugin descriptor
├── messages/HotReloadBundle.properties # Localized messages
└── hot-reload/README.md # This file (served via GET endpoint)
src/test/kotlin/com/jonnyzzz/intellij/hotreload/
├── MarkerFileTest.kt # Marker file tests
└── PluginHotReloadServiceTest.kt # Plugin ID extraction and reload tests
Internal APIs (@ApiStatus.Internal):
DynamicPlugins.checkCanUnloadWithoutRestart()- Check if plugin supports hot reloadDynamicPlugins.unloadPlugins()- Unload plugins (use plural version, not singular!)DynamicPlugins.UnloadPluginOptions- Configuration for unload operationPluginInstaller.installAndLoadDynamicPlugin()- Install and load a pluginloadDescriptorFromArtifact()- Load plugin descriptor from zip file
Public APIs:
PluginManagerCore.getPlugin()- Find plugin by IDPluginManager.getPluginByClass()- Get plugin descriptor for a class (used for self-detection)NotificationGroupManager- IDE balloon notifications
Important: Use unloadPlugins() (plural), not unloadPlugin() (singular). The singular version does not set isMarkedForLoading=false, causing assertion errors when loading the new version.
The extractPluginId() function handles two plugin zip structures:
-
Flat structure (development builds):
plugin-name/META-INF/plugin.xml -
Nested jar structure (production builds from
buildPlugin):plugin-name/lib/plugin-name.jar └── META-INF/plugin.xml
The plugin dynamically detects its own ID using:
fun getSelfPluginId(): String {
return PluginManager.getPluginByClass(PluginHotReloadService::class.java)
?.pluginId?.idString
?: "com.jonnyzzz.intellij.hot-reload" // fallback for tests
}- Authentication is required for POST requests
- Each IDE instance generates a unique Bearer token on startup
- The token is stored in the marker file (only readable by the user)
- Marker files are automatically deleted when the IDE exits
Apache 2.0