Awesome-claude intellij-plugin
IntelliJ Plugin Development
git clone https://github.com/andreiverdes/awesome-claude
skills/intellij-plugin/skill.mdIntelliJ Plugin Development
Expert skill for building IntelliJ IDEA / Android Studio plugins using the IntelliJ Platform SDK, Kotlin, and Gradle Plugin 2.x.
TRIGGER when: user asks to create, modify, or debug an IntelliJ plugin, JetBrains plugin, or Android Studio plugin. Also triggers on mentions of plugin.xml, ToolWindowFactory, AnAction, or IntelliJ Platform SDK.
Build Setup (IntelliJ Platform Gradle Plugin 2.x)
build.gradle.kts
plugins { id("org.jetbrains.intellij.platform") version "2.6.0" id("org.jetbrains.kotlin.jvm") version "2.1.20" } repositories { mavenCentral() intellijPlatform { defaultRepositories() } } dependencies { intellijPlatform { // Pick ONE target IDE: intellijIdeaCommunity("2025.2") // or: // intellijIdeaUltimate("2025.2") // androidStudio("2025.2.1.3") // Bundled plugins your plugin depends on: // bundledPlugin("org.jetbrains.android") // bundledPlugin("com.intellij.java") pluginVerifier() zipSigner() instrumentationTools() } } intellijPlatform { pluginConfiguration { id = "com.example.myplugin" name = "My Plugin" version = project.version.toString() ideaVersion { sinceBuild = "252" } } } kotlin { jvmToolchain(21) }
gradle.properties
org.gradle.jvm.args=-Xmx2g kotlin.stdlib.default.dependency=false
Shadow JAR for dependency conflicts
If bundling libraries that conflict with IDE classpath (e.g., protobuf, Gson):
plugins { id("com.gradleup.shadow") version "9.0.0-beta12" } tasks { shadowJar { relocate("com.google.protobuf", "com.example.shadow.protobuf") archiveClassifier.set("") } named("prepareSandbox") { dependsOn("shadowJar") } }
plugin.xml
<idea-plugin> <id>com.example.myplugin</id> <name>My Plugin</name> <vendor>MyCompany</vendor> <description><![CDATA[Plugin description here.]]></description> <depends>com.intellij.modules.platform</depends> <!-- Add more depends as needed --> <extensions defaultExtensionNs="com.intellij"> <toolWindow id="My Tool" factoryClass="com.example.MyToolWindowFactory" anchor="bottom" icon="/icons/toolwindow.svg"/> <applicationConfigurable parentId="tools" instance="com.example.MyConfigurable" id="com.example.settings" displayName="My Plugin"/> <applicationService serviceImplementation="com.example.MySettings"/> </extensions> </idea-plugin>
Service Registration
Modern approach: @Service annotation (preferred for 252+)
// Application-level (singleton) @Service class MyAppService { companion object { fun getInstance(): MyAppService = ApplicationManager.getApplication().getService(MyAppService::class.java) } } // Project-level (one per project) @Service(Service.Level.PROJECT) class MyProjectService(private val project: Project) { companion object { fun getInstance(project: Project): MyProjectService = project.getService(MyProjectService::class.java) } }
No plugin.xml entry needed for @Service classes. Use plugin.xml
<applicationService> only for PersistentStateComponent.
Tool Window Factory
class MyToolWindowFactory : ToolWindowFactory, DumbAware { override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) { val panel = MyPanel(project) val content = toolWindow.contentManager.factory.createContent(panel, "", false) toolWindow.contentManager.addContent(content) Disposer.register(toolWindow.disposable, panel) } }
Always implement
DumbAware unless you need indexing. Always register panels as Disposable.
UI Kit — MANDATORY Components
NEVER use raw Swing defaults. Always use IntelliJ wrappers:
| Instead of | Use |
|---|---|
+ | + |
| |
| |
| |
| |
| |
with text | with |
ColoredTreeCellRenderer (theme-aware tree rendering)
class MyTreeRenderer : ColoredTreeCellRenderer() { override fun customizeCellRenderer( tree: JTree, value: Any?, selected: Boolean, expanded: Boolean, leaf: Boolean, row: Int, hasFocus: Boolean ) { val node = value as? DefaultMutableTreeNode ?: return when (val obj = node.userObject) { is MyCategory -> { append(obj.name, SimpleTextAttributes.REGULAR_BOLD_ATTRIBUTES) icon = AllIcons.Nodes.Folder } is MyItem -> { append(obj.name, SimpleTextAttributes.REGULAR_ATTRIBUTES) icon = AllIcons.FileTypes.Any_type } } } }
SimpleTextAttributes constants:
— normal textREGULAR_ATTRIBUTES
— bold headersREGULAR_BOLD_ATTRIBUTES
— disabled/secondary textGRAYED_ATTRIBUTES
— error textERROR_ATTRIBUTES
ActionToolbar (instead of raw JButtons)
val actionGroup = DefaultActionGroup().apply { add(object : AnAction("Refresh", "Refresh data", AllIcons.Actions.Refresh) { override fun actionPerformed(e: AnActionEvent) { /* ... */ } override fun update(e: AnActionEvent) { e.presentation.isEnabled = /* condition */ } override fun getActionUpdateThread() = ActionUpdateThread.EDT }) addSeparator() } val toolbar = ActionManager.getInstance() .createActionToolbar("MyPlugin.Toolbar", actionGroup, true) // true=horizontal toolbar.targetComponent = this add(toolbar.component, BorderLayout.NORTH)
Always override
getActionUpdateThread() returning ActionUpdateThread.EDT.
Common AllIcons
AllIcons.Actions.Refresh // Refresh AllIcons.Actions.Download // Download/save AllIcons.Actions.GC // Delete/trash AllIcons.Actions.Execute // Run AllIcons.Actions.Pause // Pause/inactive AllIcons.Actions.MenuSaveall // Save AllIcons.General.Add // Add/create AllIcons.General.Remove // Remove AllIcons.General.Information // Info AllIcons.General.ChevronDown // Dropdown arrow AllIcons.Nodes.Package // Package AllIcons.Nodes.Folder // Folder AllIcons.Nodes.DataSchema // Database/schema AllIcons.FileTypes.Any_type // Generic file
Empty State with CardLayout
private val cardLayout = CardLayout() private val contentPanel = JPanel(cardLayout) // In init: contentPanel.add(emptyStatePanel, "empty") contentPanel.add(mainContent, "content") cardLayout.show(contentPanel, "empty") // When data available: cardLayout.show(contentPanel, "content")
Loading Indicator
icon = AnimatedIcon.Default() // Spinner in tree nodes
Threading Model
Rule: Heavy work on pooled thread, UI updates on EDT
ApplicationManager.getApplication().executeOnPooledThread { val data = heavyOperation() // Background SwingUtilities.invokeLater { if (gen != generation) return@invokeLater // Discard stale updateUI(data) // EDT } }
Generation counter — prevents stale async results
@Volatile private var generation = 0L // When user changes selection: generation++ // In async callback: val gen = generation ApplicationManager.getApplication().executeOnPooledThread { val result = fetchData() SwingUtilities.invokeLater { if (gen != generation) return@invokeLater // State changed, discard applyResult(result) } }
ScheduledExecutorService for polling
private val executor = Executors.newSingleThreadScheduledExecutor { r -> Thread(r, "MyPlugin-Monitor").apply { isDaemon = true } } executor.scheduleWithFixedDelay({ /* poll */ }, 0, 3, TimeUnit.SECONDS) // In dispose(): executor.shutdownNow()
Tree View with Lazy Loading
// Add placeholder so node appears expandable val node = DefaultMutableTreeNode(MyData("name")) node.add(DefaultMutableTreeNode(LoadingMarker)) // Placeholder child // Lazy load on expand tree.addTreeWillExpandListener(object : TreeWillExpandListener { override fun treeWillExpand(event: TreeExpansionEvent) { val node = event.path.lastPathComponent as? DefaultMutableTreeNode ?: return if (/* not loaded yet */) { node.removeAllChildren() node.add(DefaultMutableTreeNode(LoadingMarker)) treeModel.reload(node) loadDataAsync(node) // Fetch on background thread } } override fun treeWillCollapse(event: TreeExpansionEvent) {} }) // Incremental node insertion (streaming) treeModel.insertNodeInto(newNode, parentNode, parentNode.childCount)
Preserve state during tree updates
// Before reload: capture expanded paths val expanded = getExpandedPackages() val existingNodes = collectExistingNodes() // After rebuild: reuse nodes, restore expanded state for (pkg in expanded) { findNode(pkg)?.let { tree.expandPath(TreePath(it.path)) } }
Settings & Persistence
@State(name = "MySettings", storages = [Storage("MySettings.xml")]) class MySettings : PersistentStateComponent<MySettings.State> { data class State(var myValue: Int = 100) private var state = State() override fun getState(): State = state override fun loadState(state: State) { this.state = state } }
Settings UI with Kotlin DSL:
class MyConfigurable : Configurable { override fun createComponent(): JComponent = panel { group("Section") { row("Label:") { spinner(1..1000, 10).bindIntValue(::localValue) } } } override fun isModified(): Boolean = /* compare local vs saved */ override fun apply() { /* save */ } }
Credential & Secret Storage
stores as PLAIN TEXT XML in PersistentStateComponent
~/.config/JetBrains/<IDE>/options/. Any API key stored there is readable by every process on the machine. Never store secrets in state components.
PasswordSafe — the ONLY correct way to store credentials
PasswordSafe delegates to the OS keychain (macOS Keychain, Windows Credential Manager, KWallet/GNOME Keyring):
import com.intellij.credentialStore.CredentialAttributes import com.intellij.credentialStore.Credentials import com.intellij.credentialStore.generateServiceName import com.intellij.ide.passwordSafe.PasswordSafe object MyCredentialStore { private val attributes = CredentialAttributes( generateServiceName("MyPlugin", "apiKey") ) fun getToken(): String? = PasswordSafe.instance.getPassword(attributes) fun setToken(token: String) = PasswordSafe.instance.setPassword(attributes, token) fun getCredentials(): Credentials? = PasswordSafe.instance.get(attributes) fun setCredentials(user: String, password: String) = PasswordSafe.instance.set(attributes, Credentials(user, password)) }
Environment variable fallback (CI / headless)
fun resolveToken(): String? = System.getenv("MY_PLUGIN_API_KEY") ?: MyCredentialStore.getToken()
Settings UI with password field
class MyConfigurable : Configurable { private var tokenField = JBPasswordField() override fun createComponent(): JComponent = panel { group("Authentication") { row("API Token:") { cell(tokenField) } } } override fun isModified(): Boolean = String(tokenField.password) != (MyCredentialStore.getToken() ?: "") override fun apply() { MyCredentialStore.setToken(String(tokenField.password)) } override fun reset() { tokenField.text = MyCredentialStore.getToken() ?: "" } }
NEVER do any of these:
- Hardcode keys in source code
- Store secrets in
(plain text XML)PersistentStateComponent - Log tokens at any level, including DEBUG
- Include credentials in exception messages
- Commit
or.env
containing secretslocal.properties
Security Hardening
Shell command injection
Never build shell commands via string concatenation. Use
ProcessBuilder with an explicit argument list:
// BAD — injectable Runtime.getRuntime().exec("adb shell run-as $pkg cat $path") // GOOD — each argument is a separate element ProcessBuilder("adb", "shell", "run-as", pkg, "cat", path) .redirectErrorStream(true) .start()
Input validation
Allowlist characters for user-provided strings passed to shell or file system:
private val SAFE_PATH = Regex("^[a-zA-Z0-9._/-]+$") private val SAFE_PACKAGE = Regex("^[a-zA-Z0-9._]+$") fun validatePath(input: String): String { require(SAFE_PATH.matches(input)) { "Invalid path: $input" } return input }
File path traversal prevention
fun safePath(base: File, userInput: String): File { val resolved = File(base, userInput).canonicalFile require(resolved.path.startsWith(base.canonicalPath)) { "Path escapes base directory" } return resolved }
Network security
- Always use
for proxy-aware HTTPHttpConfigurable.getInstance() - Enforce TLS — never disable certificate verification, even during development
- Never use
on untrusted data — prefer JSON/protobuf with schema validationObjectInputStream
Logging & Error Handling
Logger — never println
private val log = Logger.getInstance(MyService::class.java) log.error("Unexpected failure in sync") // Shown in IDE error reporter log.warn("Device disconnected mid-read") // Recoverable issue log.info("Plugin initialized") // Lifecycle events log.debug("Parsed ${entries.size} items") // Off by default, zero cost when disabled
Never use
println, System.out, or System.err. The IDE aggregates Logger output in idea.log (Help > Diagnostic Tools > Browse Logs).
Notification API for user-facing messages
Register the notification group in
plugin.xml:
<extensions defaultExtensionNs="com.intellij"> <notificationGroup id="MyPlugin.Notifications" displayType="BALLOON"/> </extensions>
Then use it in code:
NotificationGroupManager.getInstance() .getNotificationGroup("MyPlugin.Notifications") .createNotification("Sync completed", NotificationType.INFORMATION) .notify(project)
Anti-patterns
- Never show raw stack traces to users — use
with a human messageNotification - Never swallow exceptions silently — at minimum
log.warn() - Never use
for expected conditions like "device not connected"log.error()
Testing Strategy
Plain JUnit for logic-only classes
Use JUnit directly for classes with no IDE dependency (parsers, utilities):
class MyParserTest { @Test fun `parse valid input`() { val result = MyParser.parse(testBytes) assertEquals(3, result.size) } }
BasePlatformTestCase for IDE integration
class MyServiceTest : BasePlatformTestCase() { fun testServiceReturnsData() { val service = project.getService(MyProjectService::class.java) val result = service.fetchData() assertNotNull(result) } }
Note:
BasePlatformTestCase uses JUnit 3 conventions — test methods must start with test, no annotations.
Mock services
Replace real services with test doubles using
ServiceContainerUtil:
import com.intellij.testFramework.ServiceContainerUtil fun setUp() { super.setUp() ServiceContainerUtil.replaceService( ApplicationManager.getApplication(), MyAppService::class.java, FakeMyAppService(), testRootDisposable ) }
Test fixtures
Place test data in
src/test/resources/ and load via:
val bytes = javaClass.getResourceAsStream("/fixtures/sample.pb")!!.readBytes()
UI testing
Test logic separately from UI. For integration tests that need EDT:
import com.intellij.testFramework.runInEdtAndWait fun testUIUpdate() { runInEdtAndWait { panel.loadData(testEntries) assertEquals(3, table.rowCount) } }
Full UI testing (clicking buttons, expanding trees) is fragile and usually not worth the maintenance cost.
Icons
File structure
src/main/resources/icons/ toolwindow.svg # 13x13, #6E6E6E fill (light theme) toolwindow_dark.svg # 13x13, #AFB1B3 fill (dark theme, auto-detected by _dark suffix) pluginIcon.svg # 40x40, full color (marketplace listing)
Icon loader
object MyIcons { @JvmField val ToolWindow: Icon = IconLoader.getIcon("/icons/toolwindow.svg", MyIcons::class.java) }
IntelliJ auto-selects
_dark.svg variant based on active theme.
Disposable Lifecycle
Every component that creates threads, executors, sockets, or listeners MUST implement
Disposable:
class MyComponent : Disposable { private val executor = Executors.newSingleThreadScheduledExecutor(...) override fun dispose() { executor.shutdownNow() // Close sockets, cancel tasks, clear state } }
Register with parent:
Disposer.register(toolWindow.disposable, myComponent)
Plugin Signing & Publishing
Signing configuration
Add to
build.gradle.kts. Credentials come from environment variables — never commit them:
intellijPlatform { signing { certificateChainFile = file("chain.crt") privateKeyFile = file("private.pem") password = providers.environmentVariable("PRIVATE_KEY_PASSWORD") } publishing { token = providers.environmentVariable("PUBLISH_TOKEN") } }
Generate a key pair via the JetBrains Marketplace. Store
chain.crt, private.pem, and the token outside version control.
Plugin icons for Marketplace
Place in
META-INF/ (not icons/). These are distinct from toolwindow icons:
src/main/resources/META-INF/ pluginIcon.svg # 40x40, full color — used in Marketplace listing and IDE plugin manager pluginIcon_dark.svg # 40x40, dark variant — auto-selected by IDE
Version compatibility
— minimum IDE build your plugin supportssinceBuild = "252"- Omit
to support all future versions (Gradle plugin does this by default)untilBuild - Run
to catch binary incompatibilities before release./gradlew runPluginVerifier - Add target IDE versions to verify against:
intellijPlatform { pluginVerification { ides { recommended() } } }
Pre-submission checklist
-
passes with no API compatibility errors./gradlew runPluginVerifier - Plugin description is at least 40 words
-
has<vendor>
andurl
attributes setemail - Change notes describe user-facing changes (not commit log)
- Test install from disk on a clean IDE instance
- No
or deprecated API usage@ApiStatus.Internal - Plugin icon renders correctly in dark and light themes
-
file lists all bundled third-party dependencies with their licensesMETA-INF/NOTICE
Third-party license notice
If your plugin bundles any third-party libraries (e.g., protobuf, Gson, OkHttp), include a
NOTICE file in src/main/resources/META-INF/NOTICE with:
- Plugin copyright
- Each dependency's name, copyright holder, license type, and full license text
src/main/resources/META-INF/ NOTICE # Third-party license attributions (bundled in JAR)
This is required for BSD, MIT, and Apache-licensed dependencies. GPL dependencies cannot be bundled in proprietary plugins.
Architecture Patterns
Service level decision
| Level | Scope | When to use | Example |
|---|---|---|---|
| Application | Global singletons, shared state across projects | Credential store, HTTP client |
| Project | State tied to a specific project | File monitors, project caches |
Rule of thumb: if the constructor needs a
Project parameter, it is project-level.
Message bus — decoupled event communication
Define a topic:
interface DataChangedListener { fun onDataChanged(entries: List<DataEntry>) companion object { val TOPIC = Topic.create("MyPlugin.DataChanged", DataChangedListener::class.java) } }
Publish:
project.messageBus.syncPublisher(DataChangedListener.TOPIC).onDataChanged(entries)
Subscribe (always pass a
Disposable to prevent leaks):
project.messageBus.connect(parentDisposable).subscribe( DataChangedListener.TOPIC, object : DataChangedListener { override fun onDataChanged(entries: List<DataEntry>) { /* update UI */ } } )
Coroutines (2024.1+)
For plugins targeting recent IDE versions, coroutines provide structured concurrency:
@Service(Service.Level.PROJECT) class MyService(private val project: Project, private val scope: CoroutineScope) { fun refreshAsync() = scope.launch { val data = withContext(Dispatchers.IO) { fetchFromDevice() } withContext(Dispatchers.EDT) { updateUI(data) } } }
The
executeOnPooledThread + SwingUtilities.invokeLater pattern remains simpler and compatible with all IDE versions.
Virtual File System (VFS)
Prefer VFS over
java.io.File when working with IDE-managed files:
val vFile = LocalFileSystem.getInstance().findFileByPath("/path/to/file") vFile?.let { VfsUtil.loadText(it) }
VFS provides change notifications, is thread-safe, and integrates with IDE indexing.
Performance & Compatibility
Memory leak prevention
- Use
to chain disposal — never register a listener without a corresponding disposalDisposer.register(parent, child) - Message bus connections auto-disconnect when the parent is disposed:
messageBus.connect(parentDisposable) - Use
for listeners registered on long-lived objects (application, project)WeakReference - Never store
orProject
references in application-level servicesComponent
Lazy initialization
Services are already lazy (instantiated on first
getService() call). For expensive fields within services:
private val cache by lazy { buildExpensiveCache() }
EDT blocking detection
Enable in development to catch slow operations on the UI thread:
-Dide.slow.operations.assertion=true
Any operation over ~300ms on EDT triggers a warning. Common offenders: file I/O, network calls,
Process.waitFor(), large tree rebuilds.
IDE version compatibility
- Run
against target IDE versions before every release./gradlew runPluginVerifier - Never call
APIs — they change without notice between IDE versions@ApiStatus.Internal - Avoid
in production — they may be removed@ApiStatus.Experimental - For version-specific APIs, use reflection or try/catch:
try { // New API available in 2025.2+ NewApi.doSomething() } catch (e: NoSuchMethodError) { // Fallback for older versions OldApi.doSomething() }
Security Audit Prompt
When asked to audit an IntelliJ plugin for security vulnerabilities, systematically check every source file for the following categories. Report each finding with file path, line number, code snippet, risk description, and severity (Critical/High/Medium/Low).
1. XML External Entity (XXE) Injection
Any use of
DocumentBuilderFactory, SAXParserFactory, XMLInputFactory, or TransformerFactory without disabling external entities. A malicious XML file (config, data, or user-uploaded) can read arbitrary files from the host machine (~/.ssh/id_rsa, ~/.env, /etc/passwd).
What to grep for:
DocumentBuilderFactory, SAXParser, XMLInputFactory, TransformerFactory, .parse(
Fix pattern:
val factory = DocumentBuilderFactory.newInstance().apply { setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false) setFeature("http://xml.org/sax/features/external-general-entities", false) setFeature("http://xml.org/sax/features/external-parameter-entities", false) }
2. Shell Command Injection
Any place where external input (package names, file paths, user strings, device identifiers) flows into
ProcessBuilder, Runtime.exec(), or especially sh -c commands via string interpolation.
What to grep for:
ProcessBuilder, Runtime.getRuntime().exec, "sh", "-c", string templates inside command lists
Red flags:
— full shell injectionsh -c "$userInput"
— injectable even with ProcessBuilderProcessBuilder("sh", "-c", "command $variable")- String concatenation in any argument passed to a shell interpreter Verify: Every variable interpolated into a shell command has strict allowlist validation upstream. Trace the data flow from origin to shell invocation.
3. Input Validation — Overly Permissive Allowlists
Regex-based validators that permit characters unnecessary for the domain. Wider allowlists = wider attack surface, especially when values end up in shell commands or file paths.
What to grep for:
Regex(, matches(, require(, validation patterns
Check: For each validator, ask: does this allow any character that could be a shell metacharacter (; | & $ \ ' " ( ) { } < > ! #) or path traversal (..)? If the validated string ends up in a shell command, even inside quotes, the allowlist must exclude the quote character used. **Rule:** SAFE_PATHshould be^[a-zA-Z0-9._/-]+$` — no spaces, semicolons, tildes, percent signs, or other shell-meaningful characters unless explicitly required.
4. Credential & Secret Exposure
Secrets stored in
PersistentStateComponent (plain text XML), logged at any level, hardcoded in source, or committed in .env/config files.
What to grep for:
PersistentStateComponent, password, token, secret, apiKey, credential, log.debug, log.info, log.warn, log.error (check what they log), .env, local.properties
Check:
- Are secrets stored via
(OS keychain) or in plain XML state files?PasswordSafe - Do log statements include tokens, passwords, command output containing auth data?
- Is
/.env
inlocal.properties
?.gitignore - Do exception messages or error notifications expose credentials?
5. File Path Traversal
User-controlled paths that aren't validated against directory escape (
../../../etc/passwd).
What to grep for:
File(, Paths.get(, resolve(, filePath, fileName, user-provided strings used in file operations
Check: Every path derived from external input must be canonicalized and confirmed to stay within an expected base directory:
val resolved = File(base, userInput).canonicalFile require(resolved.path.startsWith(base.canonicalPath))
6. Unsafe Deserialization
ObjectInputStream on data from external sources (files from devices, network responses, plugin state). Can lead to arbitrary code execution.
What to grep for:
ObjectInputStream, readObject(), Serializable
Fix: Use JSON, protobuf, or other schema-validated formats instead.
7. Network Security
HTTP instead of HTTPS, disabled certificate verification, ignoring proxy settings, sending local data to external services.
What to grep for:
http:// (not https://), TrustManager, HostnameVerifier, SSLContext, HttpURLConnection, OkHttpClient, setHostnameVerifier
Check:
- All URLs use HTTPS
- No custom
that accepts all certificatesTrustManager - Uses
for proxy-aware connectionsHttpConfigurable.getInstance() - No local file contents, paths, or environment data sent to remote endpoints without user consent
8. Temporary File Handling
Temp files with sensitive content that aren't cleaned up, or temp files in world-readable locations.
What to grep for:
createTempFile, createTempDir, File.createTempFile, /tmp/, System.getProperty("java.io.tmpdir")
Check: Are temp files deleted in a finally block or use {} scope? Do they contain sensitive data (credentials, user content from devices)?
9. Build Configuration Leakage
Signing credentials, publish tokens, or API keys in
build.gradle.kts that could leak through build logs, CI artifacts, or Gradle cache.
What to grep for:
signing, publishing, password, token in *.gradle.kts and *.properties files
Check: Credentials come from environment variables or secure vaults, not hardcoded strings. Build scripts don't print sensitive values.
10. Information Disclosure via Logging
Log statements that include sensitive data: file contents, user data from devices, full command lines with paths/credentials, stack traces with sensitive context.
What to grep for:
log.warn, log.error, log.info, log.debug — inspect what variables are interpolated
Rule: Log the event and error type, not the data. E.g., log.warn("Failed to read file", e) not log.warn("Failed to read $filePath: content=$data").
Audit Procedure
- Glob all
,.kt
,.java
,.gradle.kts
, and.properties
source files.xml - For each category above, grep for the listed patterns across all files
- For each match, trace the data flow: where does the input originate, how is it validated, where does it end up?
- Report findings grouped by severity, with exact file:line references and fix recommendations
- Also note positive security measures already in place (proper validation, PasswordSafe usage, etc.)
Pitfall Checklist
- Use
, NOTColoredTreeCellRenderer
(broken in dark theme)DefaultTreeCellRenderer - Use
withActionToolbar
, NOT rawAnAction
for toolbarsJButton - Override
returninggetActionUpdateThread()
in everyActionUpdateThread.EDTAnAction - Set
on everytoolbar.targetComponent = thisActionToolbar - Use generation counters for all async operations that update UI
- Call
in everyexecutor.shutdownNow()Disposable.dispose() - Use
notexec-out
for binary-safe ADB data transfershell - Batch ADB operations in shell scripts to minimize round-trips
- Test with real device data, not just assumptions about wire formats
- Shadow/relocate dependencies that conflict with IDE classpath
- Provide both
andtoolwindow.svg
for icon themingtoolwindow_dark.svg - Implement
onDumbAware
unless indexing is requiredToolWindowFactory - Use
for DPI-aware sizesJBUI.scale() - Never store API keys or tokens in
— usePersistentStateComponentPasswordSafe - Never build shell commands via string concatenation — use
argument listsProcessBuilder - Validate all user-provided file paths against directory traversal (
)../ - Use
, neverLogger.getInstance()
orprintln
/System.outSystem.err - Never log credentials, tokens, or sensitive user data at any log level
- Register notification groups in
before usingplugin.xmlNotificationGroupManager - Run
before every Marketplace submissionrunPluginVerifier - Provide
(40x40) andMETA-INF/pluginIcon.svg
for Marketplace listingpluginIcon_dark.svg - Connect message bus subscriptions to a
to prevent leaksDisposable - Use
for listeners on long-lived objects (application, project)WeakReference - Never call
APIs in production plugin code@ApiStatus.Internal - Sign the plugin with
task before publishing to MarketplacesignPlugin