Publishing Plugins
This guide covers everything you need to publish a plugin to the Nebo marketplace -- from prerequisites and supported platforms through upload, review, bundling skills, testing, and security.
Prerequisites
- A NeboAI developer account
- Your binary compiled for at least one target platform
- Binary must be a single executable file (no runtime dependencies)
Supported platforms
| Platform key | OS | Architecture |
|---|---|---|
darwin-arm64 | macOS | Apple Silicon |
darwin-amd64 | macOS | Intel |
linux-arm64 | Linux | ARM64 |
linux-amd64 | Linux | x86_64 |
windows-arm64 | Windows | ARM64 |
windows-amd64 | Windows | x86_64 |
Publish for as many platforms as you support. At minimum, target darwin-arm64 and linux-amd64.
Step-by-step publishing
1. Select your developer account
developer(resource: account, action: select, id: "<your-dev-account-id>")
2. Create the plugin artifact
plugin(action: create, name: "gws", category: "connectors")
3. Get an upload token (per platform)
plugin(action: binary-token, id: "<PLUGIN_ID>")
This returns a curl command with a 5-minute expiry.
4. Upload binary + manifest per platform
Use the returned curl command via the command line, replacing the file path and platform:
curl -X PUT "<upload-url>" \ -F "binary=@./build/gws-darwin-arm64" \ -F "platform=darwin-arm64" \ -F "manifest=@./PLUGIN.md"
Repeat for each platform you support.
5. Submit for review
plugin(action: submit, id: "<PLUGIN_ID>", version: "1.0.0")
Plugin tool actions
| Action | Description |
|---|---|
plugin(action: list) | List all your plugins |
plugin(action: get, id: "...") | Get plugin details |
plugin(action: create, name: "...", category: "...") | Create a new plugin |
plugin(action: update, id: "...") | Update plugin metadata |
plugin(action: delete, id: "...") | Delete a plugin |
plugin(action: submit, id: "...", version: "...") | Submit for review |
plugin(action: list-binaries, id: "...") | List uploaded binaries |
plugin(action: binary-token, id: "...") | Generate upload token |
plugin(action: delete-binary, id: "...") | Delete a binary |
Install codes
After your plugin is approved, NeboAI assigns a PLUG-XXXX-XXXX install code. Users can paste this code into Nebo's chat to install the plugin directly. However, plugins are typically installed as dependencies of skills -- users rarely install plugins standalone.
Bundling skills with your plugin
Plugins can embed skills inside a .napp archive. This is the preferred distribution method -- when the plugin installs, its skills install automatically.
.napp archive structure
gws.napp
+-- manifest.json # Package identity
+-- plugin.json # PluginManifest (the full manifest)
+-- PLUGIN.md # Plugin documentation
+-- gws # Native binary (platform-specific)
+-- skills/ # Embedded skills (optional)
+-- gws-gmail/
| +-- SKILL.md
+-- gws-calendar/
| +-- SKILL.md
+-- gws-drive/
+-- SKILL.mdAfter installation, embedded skills are discovered by the skill loader automatically. They appear as Tier 2.5 -- between marketplace-installed skills and user skills.
Directory structure on disk
What the user sees on disk after install (under the platform-native Nebo data directory — ~/Library/Application Support/Nebo/ on macOS, %APPDATA%\Nebo\ on Windows, ~/.local/share/nebo/ on Linux; NEBO_HOME overrides):
<data_dir>/
nebo/plugins/ # CODE -- replaceable on upgrade
gws/
1.2.3/
manifest.json # Package identity
plugin.json # Cached PluginManifest
PLUGIN.md # Documentation
gws # Your binary (chmod 755)
skills/ # Embedded skills (if bundled)
gws-gmail/
SKILL.md
gws-calendar/
SKILL.md
appdata/plugins/ # DATA -- never touched by updates
gws/
sidecar.log # Process output
cache.db # Your plugin's persistent data
credentials.json # Whatever your plugin storesCode and data are physically separated. The update system operates on nebo/plugins/ but never touches appdata/plugins/. Your plugin's databases, caches, and user files survive all version upgrades and reinstalls -- same model as iOS apps. Multiple versions can coexist. Each skill resolves to the highest installed version matching its semver range.
Security
- SHA256 verification: Every binary is hashed on upload. On download, the hash is verified before the binary is written to disk. Any mismatch = download rejected.
- ED25519 signatures: Binaries are signed with NeboAI's ED25519 key. Signatures are verified on download when the signing key is available.
- Quarantine: If a plugin is revoked (security issue, policy violation), Nebo deletes the binary and writes a
.quarantinedmarker. The plugin becomes unresolvable, and any skills depending on it are dropped from the loaded set. - No network required after install: Once downloaded,
resolve()is fully local. Works offline.
Versioning and updates
- Multiple versions of the same plugin can coexist on disk (e.g.,
gws/1.2.0/andgws/1.3.0/) - Each skill resolves to the highest installed version matching its semver range
- When you publish a new version, users get it automatically the next time Nebo resolves the dependency
- Old versions are cleaned up by garbage collection when no skill references them
Garbage collection
Nebo periodically checks which plugin slugs are referenced by loaded skills. Unreferenced plugin directories are removed. This is deferred (not eager) -- plugins aren't deleted the moment a skill is uninstalled.
Testing during development
During development, you can manually place a plugin binary in the expected directory structure:
mkdir -p ~/Library/Application\ Support/Nebo/nebo/plugins/my-plugin/0.1.0/ cp ./build/my-plugin ~/Library/Application\ Support/Nebo/nebo/plugins/my-plugin/0.1.0/ chmod 755 ~/Library/Application\ Support/Nebo/nebo/plugins/my-plugin/0.1.0/my-plugin
Then create a skill with plugins: [{name: "my-plugin", version: "*"}] in your user/skills/ directory. The loader will resolve the plugin locally without contacting NeboAI.
Testing authentication
To test auth locally, create a plugin.json in the version directory with your auth config:
{
"id": "my-plugin",
"slug": "my-plugin",
"name": "My Plugin",
"version": "0.1.0",
"platforms": {},
"auth": {
"type": "oauth_cli",
"label": "My Service",
"description": "Connect to My Service",
"commands": {
"login": "auth login",
"status": "auth status",
"logout": "auth logout"
},
"env": {}
}
}The auth endpoints (/api/v1/plugins/{slug}/auth/*) read from this cached manifest.
Testing events
Declare events in your local plugin.json:
{
"id": "my-plugin",
"slug": "my-plugin",
"name": "My Plugin",
"version": "0.1.0",
"platforms": {},
"events": [
{
"name": "item.created",
"description": "Fires when a new item is created",
"command": "watch --format ndjson"
}
]
}Then create an agent with a watch trigger referencing event: "item.created". The watch process should output NDJSON to stdout.
Manifest validation rules
Nebo validates plugin.json during installation. Invalid manifests are rejected before the binary is written to disk.
| Field | Rule |
|---|---|
slug | Required. Lowercase alphanumeric + hyphens only. No leading/trailing hyphens. Max 64 characters. |
version | Required. Must be valid semver (e.g., "1.2.3", not "latest"). |
platforms | At least one platform entry required. |
binaryName | No path separators (/, \). No ... Cannot be empty. |
auth.commands.login | Must be non-empty if auth is present. |
events[].name | Must be non-empty. No path separators. |
events[].command | Must be non-empty. |
Common validation errors
"slug is required" "slug 'My Plugin!' contains invalid characters -- use lowercase alphanumeric and hyphens" "slug must not start or end with a hyphen" "version 'latest' is not valid semver" "platforms must have at least one entry" "binary_name '../evil' contains path separator" "auth.commands.login must be non-empty when auth is present"
Plugin runtime environment
Plugins are spawned on-demand -- each tool call, hook invocation, or command execution spawns a fresh process with the CLI arguments from the capability definition. There is no persistent plugin process between invocations. The exception is watch triggers, which spawn a long-running process that emits NDJSON events continuously.
Each invocation runs in a sandboxed environment with a controlled set of environment variables.
Environment variables
| Variable | Example | Description |
|---|---|---|
NEBO_APP_ID | gws | Plugin identifier (from manifest) |
NEBO_APP_NAME | Google Workspace CLI | Display name |
NEBO_APP_VERSION | 1.2.3 | Manifest version |
NEBO_APP_DIR | <data_dir>/nebo/plugins/gws/1.2.3 | Plugin binary directory (code -- replaceable) |
NEBO_APP_SOCK | <data_dir>/nebo/plugins/gws/1.2.3/gws.sock | Unix socket path for gRPC |
NEBO_APP_DATA | <data_dir>/appdata/plugins/gws | Persistent data directory (separate from code -- survives upgrades) |
PATH | system path | Standard path |
HOME | user home | Home directory |
TMPDIR | temp directory | Temporary files |
LANG | locale | System locale |
TZ | timezone | System timezone |
All other environment variables (API keys, secrets, database URLs) are stripped -- your plugin must not depend on the user's shell environment.
Data persistence
Store all persistent data in $NEBO_APP_DATA. This directory is physically separated from the code directory -- it lives at <data_dir>/appdata/plugins/<slug>/, not inside the version directory. This means:
- Upgrading the plugin binary never touches your data
- Your plugin is responsible for its own schema migrations across versions
- Data survives reinstalls, version upgrades, and Nebo updates
Common storage patterns:
- SQLite -- for structured data and queries
- JSON files -- for simple configuration state
- File store -- for cached downloads, processed outputs
Blocked variables
These environment variables are always stripped for security:
ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_API_KEY, JWT_SECRET, DATABASE_URL, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, GITHUB_TOKEN, STRIPE_SECRET_KEY
WebSocket events
Events broadcast during plugin operations:
| Event | Payload | When |
|---|---|---|
plugin_installing | { plugin, platform } | Before download starts |
plugin_installed | { plugin } | After successful install |
plugin_error | { plugin, error } | On download/verify failure |
plugin_auth_started | { plugin, label } | Auth login begins |
plugin_auth_url | { plugin, url } | OAuth URL discovered in output |
plugin_auth_complete | { plugin } | Auth login succeeded |
plugin_auth_error | { plugin, error } | Auth login failed |
Quick reference
Environment variable naming
{SLUG}_BIN -- slug uppercased, hyphens become underscores.
Install code prefix
PLUG-XXXX-XXXX -- Crockford Base32, case-insensitive.
SKILL.md frontmatter
plugins:
- name: <slug> # Required
version: "<range>" # Optional, default "*"
optional: <bool> # Optional, default falseNeboAI qualified name
@org/plugins/name@version
Same scoping and version resolution rules as skills. See Packaging for details.
Complete plugin.json example
{
"id": "gws",
"slug": "gws",
"name": "Google Workspace CLI",
"version": "1.2.3",
"description": "Google Workspace integration for email, calendar, and drive",
"author": "NeboAI Inc.",
"platforms": {
"darwin-arm64": {
"binaryName": "gws",
"sha256": "a1b2c3...",
"signature": "base64...",
"size": 45678900,
"downloadUrl": "https://cdn.neboai.com/..."
}
},
"signingKeyId": "key-001",
"envVar": "",
"auth": {
"type": "oauth_cli",
"label": "Google Account",
"description": "Authenticate with your Google Workspace account.",
"commands": {
"login": "auth login",
"status": "auth status",
"logout": "auth logout"
},
"env": {}
},
"events": [
{
"name": "email.new",
"description": "Fires when a new email arrives",
"command": "gmail +watch --format ndjson",
"multiplexed": false
},
{
"name": "calendar.event",
"description": "Fires on calendar event changes",
"command": "calendar +watch --format ndjson",
"multiplexed": true
}
],
"dependencies": [
{
"name": "ffmpeg",
"version": ">=5.0.0"
}
],
"capabilities": {
"tools": [
{
"name": "gws.gmail.triage",
"description": "Triage Gmail inbox",
"command": "gmail +triage",
"inputSchema": {
"type": "object",
"properties": {
"limit": { "type": "integer", "description": "Max emails to process" },
"label": { "type": "string", "description": "Gmail label to filter" }
}
},
"approval": true,
"timeoutSeconds": 120
},
{
"name": "gws.calendar.create",
"description": "Create a Google Calendar event",
"command": "calendar +create",
"approval": true,
"timeoutSeconds": 30
}
],
"hooks": [
{
"hook": "tool.pre_execute",
"hookType": "filter",
"priority": 50,
"command": "hooks tool-pre-execute",
"timeoutMs": 500
}
],
"commands": [
{
"name": "/gmail",
"description": "Quick access to Gmail operations",
"command": "gmail",
"slash": true
}
]
}
}