Building Your First Plugin
This is the guide we wish existed when we started: a plain walkthrough of what a klenua plugin actually is, and everything you need to go from a blank folder to a published listing. No prior experience with klenua required.
If you want the precise technical specification instead of a walkthrough, see the Plugin Manifest Specification and Plugin System docs — this guide links out to both at the relevant points.
What a plugin actually is
A klenua plugin is a small, focused tool — a formatter, an inspector, a generator, a request builder — that runs inside the klenua desktop host rather than as its own standalone app. You write it once, publish it to the marketplace, and anyone using klenua can install it in a couple of clicks.
Two files make every plugin package, no matter how simple or complex the plugin itself is:
manifest.json— describes what the plugin is: its identity, version, entry point, and what capabilities it needs.- An entry file — the actual code, referenced by the manifest's
entryfield.
Everything else — icons, extra source files, a README, whatever your build produces — is up to you. The marketplace stores your package exactly as uploaded and never opens, parses, or runs its contents; only the future desktop host does that, and only after checksums and a permission prompt.
What you can build — and what you can't
The honest version, before you invest time in an idea.
What's possible
Every capability a plugin can have maps to one of the permissions in the catalog, so "what can I build" is really just "what does the permission list add up to":
- UI, panels, and commands. Every plugin can render its own UI and register commands into the host's command palette — this is always available, no permission needed.
- Formatters, inspectors, and validators that work on data the user pastes or opens — JSON, regex, JWTs, colors, timestamps. No permissions needed at all beyond the UI itself; several of klenua's own official plugins (JSON Lab, Regex Playground, Color Inspector) are exactly this shape.
- Request builders and API clients — anything that needs to make outbound HTTP calls, gated by the
networkpermission. - Tools that touch specific files the user picks — converters, exporters, batch renamers — gated by
filesystem.read/filesystem.write. The user always picks the file through a native picker; your plugin never gets to enumerate a folder on its own. - Project-aware tools that read or modify files inside the user's currently open project, gated by
project.read/project.write. - Clipboard utilities and small local databases — snippet managers, generators that remember your last input — gated by
clipboard.read/clipboard.writeandlocalStorage. - Notifications for anything that finishes in the background, gated by
notifications. - Build, deploy, or automation tools that need to shell out to another program — gated by
shell.execute, the highest-risk permission in the catalog and the one users will think hardest about granting.
What isn't possible
This is architectural, not a policy someone could quietly change:
- No ambient filesystem or network access. A plugin that didn't declare
networkin its manifest doesn't get a brokenapp.networkthat throws an error — it gets noapp.networkat all. The same is true for every other permission. There's no way to smuggle in a capability the manifest didn't ask for. - No scanning the filesystem on your own. Even with
filesystem.readgranted, you receive files the user explicitly selected through a picker — a plugin can never enumerate or walk a directory tree unprompted. - No running outside the desktop host. A klenua plugin isn't a standalone executable or a background service; it only runs inside the host's runtime, in the context the host gives it.
- No skipping review. Every package is malware-scanned and every version is checked by a moderator before it's installable by anyone but you. There's no fast lane, and no way for a plugin to update itself without going through the same version/scan/review cycle again.
- No silent permission upgrades. If version 2.0 of your plugin asks for a permission version 1.0 didn't have, that's a version-scoped change the host is expected to surface to the user before they accept the update — you can't quietly expand what your plugin can do.
- No changing your bundle identifier. Whatever
idyou first publish with is permanent for that plugin's lifetime on klenua.
One more honest caveat
The marketplace side of this — publishing, versioning, permissions, malware scanning, moderation — is fully built and is exactly what this guide walks through. The desktop host that actually runs a plugin end-to-end is still being built, so you can publish and manage a plugin today, but running one on a real machine depends on how far along the host is. Nothing above is aspirational or hand-waved, though — it's the runtime contract the host is being built against, documented in full in Plugin System.
Before you start
You'll need a klenua account. If you haven't already, sign up, then head to Start Building and become a developer — it just asks for an optional studio name and support email, no approval wait.
You don't need any particular language or toolchain to follow along here. The manifest is plain JSON; the entry file in the examples below is JavaScript, matching the SDK surface documented in Plugin System, but the marketplace itself never inspects or cares what language is inside your package.
Step 1 — Write your manifest.json
Every package needs a manifest.json at its root (not nested inside a subfolder). Here's a complete, minimal one:
{
"id": "com.yourname.hello-klenua",
"name": "Hello klenua",
"version": "1.0.0",
"description": "A friendly first plugin that says hello.",
"author": "Your Name",
"entry": "main.js",
"minimumHostVersion": "1.0.0",
"permissions": ["notifications"]
}
Walking through each field:
id— your plugin's bundle identifier, reverse-DNS style (likecom.yourname.hello-klenua). This is how klenua tells your plugin apart from everyone else's. It's unique across the whole marketplace and can't be changed once your plugin is created, so pick something you're happy to keep.name— the display name shown in the store and on install.version— semantic version (MAJOR.MINOR.PATCH, e.g.1.0.0). Every version you publish needs a unique, higher number than the last.description— a short summary. It doubles as your listing's description when you publish straight from a package (more on that in Step 4).author— free-text attribution. Doesn't affect who owns the listing on klenua — that's tied to your account.entry— the path inside your package to the file the host should load, e.g.main.js.minimumHostVersion— the lowest desktop app version your plugin needs.1.0.0is a safe default while the host is young.permissions— the list of capabilities your plugin is asking for. Leave it as an empty array ([]) if you don't need any. See Permissions below before you reach for this.
Step 2 — Write your entry file
Your entry file is what the desktop host loads and runs. The host injects a namespaced app object into your plugin's context — you never touch the filesystem, network, or shell directly, only through whatever app.* modules your declared permissions unlock. A minimal main.js for the manifest above:
module.exports = {
activate(app) {
app.commands.register('hello-klenua.greet', () => {
app.notifications.show('Hello from your first klenua plugin!');
});
},
};
This plugin registers one command and shows a system notification when it runs — which is exactly why its manifest requests the notifications permission and nothing else. The full list of app.* modules and which permission unlocks each one is documented in Plugin System → The future SDK surface.
A complete sample plugin
"Hello klenua" above is deliberately the smallest thing that counts as a plugin. Here's a slightly more real one — a command that reads whatever JSON is on your clipboard, pretty-prints it, and writes the result back. It's still short enough to read end to end, but it exercises more of the SDK surface: reading input, handling a failure case, and writing output, across three permissions instead of one.
manifest.json:
{
"id": "com.yourname.clip-json-formatter",
"name": "Clip JSON Formatter",
"version": "1.0.0",
"description": "Pretty-prints whatever JSON is on your clipboard, in place.",
"author": "Your Name",
"entry": "main.js",
"minimumHostVersion": "1.0.0",
"permissions": ["clipboard.read", "clipboard.write", "notifications"]
}
main.js:
module.exports = {
activate(app) {
app.commands.register('clip-json-formatter.format', async () => {
const raw = await app.clipboard.read();
let formatted;
try {
formatted = JSON.stringify(JSON.parse(raw), null, 2);
} catch (err) {
app.notifications.show("Clipboard doesn't contain valid JSON.");
return;
}
await app.clipboard.write(formatted);
app.notifications.show('Clipboard formatted.');
});
},
};
Three permissions, three matching app.* calls — clipboard.read for app.clipboard.read(), clipboard.write for app.clipboard.write(), notifications for app.notifications.show(). Nothing in the manifest is unused, and nothing the code calls is undeclared. That symmetry is worth keeping in every plugin you publish, not just this example — it's the whole reason a permission prompt is trustworthy at install time.
Package and publish it exactly like Steps 3 through 5 below — nothing about that process changes based on how many permissions your plugin asks for.
Step 3 — Package it
Zip manifest.json and your entry file (and anything else your plugin needs) together, with manifest.json sitting at the top level of the archive — not inside a subfolder. On macOS or Linux, from inside your plugin's folder:
zip -r hello-klenua.zip manifest.json main.js
On Windows, select the files (not the containing folder) in File Explorer, right-click, and choose Send to → Compressed (zipped) folder.
Either a .zip or a .pluginpackage extension works — they're the same container format as far as klenua is concerned.
Step 4 — Publish it
You have two ways to get a package onto klenua. Use whichever fits how far along your plugin is.
The fast way: just upload the zip
From My Plugins → Create Plugin, pick your package file first. klenua reads its manifest.json and fills in the bundle identifier, name, description, and first version automatically — you only need to pick Supported OS and, optionally, any categories, URLs, or a license, before hitting Create draft. One upload, and your plugin and its first version both exist with the package already attached and scanning.
The same trick works for adding a new version to an existing plugin: open New version on the plugin's page, pick the updated package, and its version, minimum host version, and permissions are read straight from the manifest again.
The manual way
If you'd rather fill in the listing yourself first — say, you want to write a longer description or pick categories before you have a package ready — use Create Plugin without selecting a file, fill in the form, then add a version and upload your package to it afterward from the plugin's Versions tab.
Either way ends up in the same place: a draft plugin with a version that has your package attached.
Step 5 — Submit for review
Once your version has a package attached, hit Submit on it. From there:
- Malware scanning. Every package is scanned before it can reach a moderator, and independently checked again right before approval — two separate checks, neither trusting the other. This is usually fast (a returning file klenua has seen before resolves in under a second), but a genuinely new file can take a couple of minutes. If you submit right after uploading and see a "still scanning" message, wait a minute and try again — it almost always clears by then.
- Moderation review. Once the package is clean, your version enters the review queue. A moderator checks the listing and package metadata before it goes live.
- Published. Approval publishes the version immediately — it becomes visible and installable in the store.
Permissions
Permissions are the one part of your manifest worth spending real thought on, because they're the first thing a user sees before they trust your plugin with anything. Request only what your plugin actually uses — an unused permission doesn't make your plugin more capable, it just makes it look less trustworthy.
The full catalog, low risk to high:
- Low risk —
clipboard.read,clipboard.write,notifications,localStorage. Reading/writing the system clipboard, showing notifications, and persisting small amounts of your plugin's own data locally. - Medium risk —
filesystem.read,network,project.read. Reading files the user explicitly picks via a host-native picker (never arbitrary filesystem access), making outbound HTTP requests, and reading files within the user's current project. - High risk —
filesystem.write,project.write,shell.execute. Creating or modifying files the user has selected, modifying files in the current project, and executing shell commands. Only request these if your plugin's entire purpose requires it.
A plugin never gets a capability it didn't declare and the user didn't accept — this isn't just a policy klenua promises to enforce, the app.* object the host injects simply won't have that module if the permission wasn't granted. See the full catalog with exact risk labels in the Plugin Manifest Specification.
Updating your plugin
Publish a new version the same way you published the first one — bump version in your manifest, package it, and either drop the new zip straight into New version, or upload it manually. If the new version's permissions differ from the last one, the desktop host is expected to show users exactly what changed before they update, since permissions are tracked per-version rather than per-plugin for exactly this reason.
Tips for a listing people actually install
- Add an icon. A plugin with a colored-square placeholder reads as unfinished, even if the plugin itself isn't.
- Add a screenshot or two. People install what they can picture using.
- Write a short description that's actually short. It's what shows up in every listing card across the store — one clear sentence beats three vague ones.
- Keep your changelog honest. "Fixed a crash when exporting large files" tells a user more than "bug fixes."
Troubleshooting
- "This version number already exists for this plugin." Bump
versionin your manifest — every version needs a higher number than the last one for that plugin. - "This package has no manifest.json at its root." Check that
manifest.jsonisn't nested inside a subfolder in your zip — it needs to sit at the top level, alongside your entry file. - Stuck on "Scanning…". Give it a minute; hitting Submit again triggers a fresh status check and usually resolves it. If it's still pending after several minutes, re-upload the package.
- Bundle identifier is taken. Identifiers are unique across every plugin on klenua and can't be reused once claimed, even by a deleted plugin. Pick a more specific one.
Where to go next
- Plugin Manifest Specification — every field, the full permission catalog with risk levels, and how the format is versioned going forward.
- Plugin System — the architecture the desktop host is being built against, including the full
app.*SDK surface. - API Reference — if you'd rather script the publish flow than click through the dashboard.