RFC: Architecture for a Secure Plugin Ecosystem

1. Context & Problem Statement

Currently, the Joplin plugin ecosystem relies on a scheduled cron job that automatically pulls new plugins from the NPM registry every 30 minutes. This "pull-based, trust-by-default" architecture lacks automated security scanning, human review verification, and traceability back to the developer's original source code. If a compromised or malicious package is published to NPM, it is blindly ingested into the Joplin ecosystem.

The objective of this project is to transition the ecosystem to an "event-driven, verify-by-default" model. This will secure the supply chain and introduce automated malware scanning without sacrificing the frictionless Developer Experience (DX) that Joplin plugin creators currently rely on.

2. Proposed Architecture & Developer Experience (DX)

To achieve a secure pipeline without forcing developers to learn new workflows or install global tools, this architecture integrates directly into the native NPM lifecycle.

By using the prepublishOnly hook within the plugin's package.json template, we can securely intercept the developer's standard npm publish command. This approach maintains 100% backward compatibility. When triggered, the hook runs a bundled @joplin/plugin-repo-cli dependency that performs local metadata validation, authenticates the user via a GitHub Device Flow, and securely submits the repository state to a structured GitHub Issue.

To create a strict security failsafe, the script intentionally exits with a non-zero code after a successful Joplin submission. This locally crashes the final step of the NPM PUBLISH process, effectively blocking the plugin code from ever leaking to the public NPM registry.

To ensure a smooth Developer Experience (DX), the @joplin/plugin-repo-cli will intercept the standard NPM error stream. Before triggering the intentional exit 1, the CLI will output a clear, formatted success block in the terminal containing the URL of the newly created GitHub Issue. This prevents developers from seeing a "raw" crash and clarifies that the "NPM ERR!" is a security feature, not a bug.

Opt-in Privacy Strategy The yo joplin generator will include a configuration prompt: "Do you want to mirror this plugin on the public NPM registry? (y/N)". Based on this input, the generator modifies the package.json. By default (No), it adds the "private": true flag and appends && exit 1 to the prepublishOnly script to firmly block public leaks. If the user opts in (Yes), it omits the private flag and the exit code, allowing the Joplin submission to succeed right before the standard NPM public upload continues.

We don't touch the existing prepare hook as it compiles the .jpl file and will be used for the build later.

3. Security Scanning Pipeline & Tooling Trade-offs

Once the submission is routed to a GitHub Issue, an event-driven GitHub Actions pipeline instantly wakes up to analyze the developer's repository. Rather than relying on a single security tool, I am proposing a dual-scanner approach to cover both static analysis and supply chain vulnerabilities.

Tool Main Strength Pros Cons
Semgrep Custom code scanning Fast, easy to maintain custom rules, lightweight CI usage Less deep analysis than CodeQL
CodeQL Deep security analysis Very powerful multi-file vulnerability detection Slow CI runtime, difficult rule language
Socket.dev Supply chain security Detects malicious packages, typosquatting, install-script abuse Focused mostly on dependencies
Dependabot Dependency maintenance Built into GitHub, automatic dependency update PRs Mainly checks known CVEs
Snyk Vulnerability database + SAST Strong dependency and code scanning coverage Can become noisy/heavy for this use-case

For Static Application Security Testing , the pipeline will utilize Semgrep.
While tools like CodeQL offer deeper semantic analysis it takes a lot of time to scan (often 10+ min), we need something that is fast and easy to maintain in future too.

Semgrep is chosen here because its syntax for writing rules is very high level and closely mirrors standard source code. This makes it significantly faster to run and much easier for the Joplin core team to write and maintain custom security rules specific to the Joplin API.

Because Semgrep does not deeply analyze the nested node_modules tree, the pipeline will pair it with Socket.dev and npm audit to specifically monitor the supply chain for hijacked dependencies, network access anomalies, or zero-day malware.

Tools like Dependabot mainly focuses on automated dependency updates, while Snyk heavily uses vulnerability databases and dependency analysis (almost same as npm audit). Hence, Socket.dev comes out to be the best fit for this situation.

The outputs of these scanners are aggregated into a single, readable Markdown report on the GitHub Issue, drastically reducing the cognitive load on the human maintainer during review.

Tool OSS Pricing / Limits Notes
Semgrep Free for public/open-source repositories Very cost effective for custom rule-based scanning
CodeQL Free for public GitHub repositories No direct cost, but heavier CI runtime cost
Socket.dev Free OSS tier Good balance between supply-chain coverage and cost
Dependabot Fully free on GitHub Zero setup or infrastructure cost
Snyk Free for public OSS repositories Powerful, but larger projects may eventually require paid tiers

YAML-Based Issue Forms To avoid the fragility of parsing free-form Markdown, the submission CLI will utilize GitHub Issue Templates. This allows the CI to extract metadata (Repository URL, Commit Hash, Plugin ID) as structured JSON objects, ensuring 100% parsing accuracy and preventing "malformed issue" errors.

Update Lifecycle :
When a developer submits a version update, the pipeline performs a Comprehensive Scan of the full codebase at the new commit. While the automated report highlights the Differential Changes (the delta between the approved_commit and the new submission) as a convenience to the reviewer, the security scan itself is always executed across the entire repository. This ensures that cross-file vulnerabilities... where malicious logic is split between previously approved code and new updates are fully detected. By surfacing the delta separately, the load on the human maintainer is drastically reduced, allowing for a rapid, focused review of routine version bumps.

4. Approval, Registry Mutation, and UI Integration

Automated scanning alone cannot prevent targeted attacks if a bad actor updates their logic to bypass static analysis. Therefore, human review remains the final gatekeeper.

Once a Joplin maintainer reviews the automated report and comments /approve on the Issue, a GitHub Action is triggered. To maintain strict security perimeters, this process uses a split-job architecture :

  • The first job safely builds the .jpl file in an isolated environment. Runs in an environment restricted to contents: read. It executes npm ci --ignore-scripts to prevent malicious postinstall scripts from compromising the runner. It builds the .jpl and uploads it as a temporary internal artifact.

  • The second job holds contents: write permissions. This job never executes developer code. It simply downloads the static artifact from Job 1 and uploads the artifact to GitHub Releases and updates the central manifests.json registry via the GitHub REST API.
    This API-driven mutation is faster, safer, and less prone to merge conflicts than a full Git clone-and-push cycle.

This split-job design creates a clear separation between building untrusted plugin code and modifying the official registry. The build step runs with restricted read-only permissions, while the publishing step has write access but only handles builded static artifacts. This reduces the risk of a compromised build environment affecting the Joplin registry itself.

Authorization & Race Conditions : The /approve trigger is validated by checking the author_association property of the GitHub Issue comment. The workflow only proceeds if the commenter is an official OWNER or MEMBER of the Joplin organization. Furthermore, to prevent corrupted states when multiple maintainers approve plugins simultaneously, the mutation job utilizes GitHub Concurrency Groups to strictly queue all writes to the manifests.json file.

Finally, to make this security model meaningful to the end-user, the Joplin Desktop React/Electron UI will be updated. Plugins marked as reviewed in the registry will display a "Verified Shield" badge within the app, while unverified legacy plugins will feature a subtle warning tooltip. This empowers users to make informed decisions about the code they install.

In the event of a post-approval security breach, maintainers can update a plugin's status to suspended in the registry. The Joplin UI will react by hiding these plugins from the store and automatically disabling existing installations to protect the user's local environment.

Suspending a compromised plugin, will be handled via a workflow_dispatch GitHub Action, providing maintainers with a safe, UI-driven way to mutate the registry without risking manual JSON syntax errors.

5. Legacy Plugin Migration Strategy

With hundreds of active plugins, breaking existing workflows or delisting developers is not an option. Existing plugins will be kept into the registry with an unreviewed status. They will function normally but will not receive the Verified Badge in the UI.

To upgrade, a legacy developer updates their project using the latest generator, increments the version number in package.json, and runs npm publish. This automatically drops their code into the new CI review pipeline, allowing them to earn the Verified Badge upon approval. Once the new pipeline demonstrates stability, the legacy cron job pulling blindly from NPM will be deprecated and removed entirely.

After looking more closely at how plugins are currently processed, I realized there can be a better method by using the existing scheduled NPM-based build instead of a PR workflow.

Earlier, my proposal assumed a PR-based validation flow, where the developer has to raise a pr. I’ve now updated it to fit the existing cron pipeline by moving the validation and review logic into a post-build auditing layer.

This adds a quarantine + exception mechanism for high-risk plugins (with manual review), while safe plugins continue to flow automatically. The core Zero Trust runtime enforcement and permission system remain unchanged.

Thanks for looking into this however this proposal is not addressing the actual project requirements.

We are not looking to implement plugin sandboxing or a zero-trust runtime architecture. The goal of the project is described here:

https://github.com/joplin/gsoc/blob/master/ideas.md#6-strengthen-the-security-of-the-plugin-ecosystem

And the related RFC is here:

https://github.com/laurent22/joplin/issues/9582

The main idea is to strengthen the plugin review and publishing process, which could include:

  • review plugin source repositories
  • review dependencies
  • build plugins from reviewed commits
  • reduce reliance on npm as a trusted distribution channel

Your proposal instead focuses on runtime sandboxing, IPC isolation, permission systems, Electron lockdown, etc. That's a very different architectural direction and not something we are currently planning to implement.

So before going further, could you clarify whether you had read the project description and RFC? I'm asking because the proposal seems largely unrelated to the requested project scope.

Also what is this refering to? This is closer to what we want, but where is it?

Thank you for the direct feedback. To answer your question: Yes, I did read the project description and RFC, but I completely misinterpreted the core objective.

Regarding your second question "Where is it?" That part was me trying to adjust my plan, but I was still stuck on the wrong sandbox/ NPM heavy idea.

Proposed Architecture Overview

This needs to be significantly expanded with how it's meant to work and why, potentially with diagrams if it helps. You should not jump into describing code in a proposal.

What's the reasoning for this apparently external tool? When the developer setups their plugin they install our code (with yo joplin) so why not bundle that publish tool with it and add it to package.json? And the command then will be npm something. Also is it possible to override npm publish for backward compatibility?

The detailed implementation plan is not relevant if you don't explain in English what you want to do first, and I don't want to piece together your intention by reading git commands, json keys and yml files, most of which will be useless anyway once you start working on it. What would be useful is a high level discussion of what you want to do and why.

The reviewing tool is an important part of this project so you also need to provide a comparison of the possible tools, with your recommendations and why.

The primary thing that an external package holding the publish/update logic would be better is in case of future changes.
If all the pipeline code is written in the boilerplate plugin repo generated by yo joplin and ran using node for publish/update , in case of future changes to the pipeline it would be very hard for the developer who already have the repository created to have an updated pipeline code.

Also, it just clicked into my mind that in case of future update of the external package version will increase,
so during the publish/update execution there can be a check which validates if the external package is up to date or not and stop the publish/update and tell the dev respectively to run an update.

I don't think it is possible to override the npm publish but what we can do is have a script called publish: joplin-plugin publish, so dev have to write npm run publish

I think that make sense, we can have the external package in devDependencies, so publish: joplin-plugin publish will be valid as soon as the yo joplin run finishes.

It can be a separate package, but not something that needs to be installed separately with npm i -g because that's error prone and an additional step. What I mean is that you can bundle it by adding it to the package.json template.

In general please take your time and investigate issues before answering, to avoid "I think" / "I don't think" answers. If you check our existing plugin templating tool, we control publishing via the files and prepare key in package.json - can that be done here too?

npm run publish still breaks backward compatibility.

Sorry and thanks for clarification, I realized I was looking an outdated package template

To be clear it's been working like this since day 1. What package template were you looking at? Just want to make sure you're not looking at the wrong thing. It's in generator-joplin package in the main repo.

I was actually seeing the repository of generator-joplin which was archived + my local plugin setup, So i was originally confused too that why my local pkg.json and the template one was too different , but after following the original link from the npm package and readme I got the real template.
Sorry for that

I’ve updated the proposal based on your feedback. I reworked it to focus more on the higher-level architecture, DX/workflow decisions, tooling tradeoffs, migration strategy, and the reasoning behind the design choices instead of going too deep into implementation specifics.

Thanks for the update, this is way more readable than the previous proposal. Keeping it high level like this means we can discuss the important parts without losing ourselves in implementation details.

I think we will let it publish something to npm like we do now. I don't think ending with NPM ERR even with additional messages is proper. Not to mention that's going to hide actual errors.

Opt-in Privacy Strategy The yo joplin generator will include a configuration prompt: "Do you want to mirror this plugin on the public NPM registry? (y/N)". Based on this input, the generator modifies the package.json. By default (No), it adds the "private": true flag and appends && exit 1 to the prepublishOnly script to firmly block public leaks. If the user opts in (Yes), it omits the private flag and the exit code, allowing the Joplin submission to succeed right before the standard NPM public upload continues.

Let's not do that. This is implementation details that's going to confuse the user. Let's publish to npm if we have to.

If npm publish is a problem, another approach to solve backward compatibility is to make it throw an error that says what should be used instead. i.e. As of August 2026, to publish your plugin, use "npm run publish"

For Static Application Security Testing , the pipeline will utilize Semgrep .
While tools like CodeQL offer deeper semantic analysis it takes a lot of time to scan (often 10+ min), we need something that is fast and easy to maintain in future too.

I think what may be missing in your proposal is that you don't ask much questions to the people who will do the reviews. Please try to integrate this into your analysis to confirm (or not) your assumptions. The reason I bring that up is that here you assume that an automated tool running for 10+ min is a problem, but nobody said that. I would be fine with something that run for one hour if the analysis is good. And then if you work based on these invalid assumptions you end up with the wrong implementation.

The paragraph below is also about the fact that's it's fast, which is irrelevant.

So, how long the tool runs is not a problem. What could be the factors that could decide what is a good tool or not? I was going to write down a list but it's probably better if you think through this or maybe ask around, or create a forum post to discuss it with the community.

It's ok to make assumptions - but please validate them.

By the way, how relevant these tools are in the age of LLMs? Claude for example is good at spotting security vulnerabilities - should LLMs be considered here? Or do these tools already use them?

I like your idea of aggregating the output of all tools in a clear Markdown report. We'll have to make sure we include only what's important in these reports so as to keep them lean and allow us to quickly review plugins.

Thank you for the tool pricing table, but it's standing there without any explanation or label.

YAML-Based Issue Forms

That's a reasonable approach, but let's see once you get to that part. How much data are we talking about? The reason I'm asking is that actual Markdown is more readable, and perhaps data is not so complex that we require YAML. But we don't have to decide now, just something to keep in mind

Good point, we indeed need to scan everything every time.

Would it make sense to use GitHub labels to track the status of the review? Comments are often lost in the middle of other comments so while the bot will find them, it's often more difficult for a human to do so.

This split-job design creates a clear separation between building untrusted plugin code and modifying the official registry. The build step runs with restricted read-only permissions, while the publishing step has write access but only handles builded static artifacts. This reduces the risk of a compromised build environment affecting the Joplin registry itself.

What's missing in your proposal is to describe how the current publishing process works - how does it go from the developer's computer to everybody's applications. Please add a section about this.

I'm not too sure about your two step idea to have CI build and publish the plugins. I'm not certain it makes things more secure and we don't want to overcomplicate things.

Using labels I believe would solve all this without any check since only admins can add or remove labels.

In the event of a post-approval security breach, maintainers can update a plugin's status to suspended in the registry. The Joplin UI will react by hiding these plugins from the store and automatically disabling existing installations to protect the user's local environment.

Suspending a compromised plugin, will be handled via a workflow_dispatch GitHub Action, providing maintainers with a safe, UI-driven way to mutate the registry without risking manual JSON syntax errors.

Please make sure you review carefully how it currently works. This is already implemented and ideally we don't want to change features that already work. The current repo features are documented in /readme: https://github.com/joplin/plugins

Hmm, maybe. Would it be crazy to have the tool push all existing plugins for review? If the process is designed in an efficient way it might be manageable for us to check them all. This should at least be considered as an option.

Thanks for the detailed feedback. I'll make sure to consider all of them in the next updated proposal.

That makes sense. Earlier I was treating execution time as a much bigger constraint than it actually is, although there were also other factors I was considering like maintainability, complexity, and how easy writing custom rules is.

I agree it would be better to gather direct feedback from the maintainers/reviewers instead of assuming those priorities myself. I’ll open a dedicated discussion soon around the tooling comparisons and reviewer workflow tradeoffs I currently have in mind.