Plugins
Creating a Plugin
A plugin is a shared native binary that Nebo downloads once and makes available to any skill that declares it as a dependency. If three skills depend on gws (Google Workspace CLI), only one copy of the binary exists on disk.
Skills access the binary through an environment variable (GWS_BIN, FFMPEG_BIN, etc.) — your script calls it like any other CLI tool.
When to Use a Plugin
| Pattern | Use When | Example |
|---|---|---|
| Plugin | Multiple skills share the same binary, or it's large (>5MB) | gws, ffmpeg, nebo-office |
| Skill with binary | One skill bundles its own small binary | A custom tool for a single skill |
If your binary serves exactly one skill, embed it as a skill binary. If multiple skills share it, publish it as a plugin.
The Plugin Manifest
Every plugin has a plugin.json that describes the binary, its platforms, and optional capabilities. This is the core of your plugin.
{
"slug": "gws",
"name": "Google Workspace CLI",
"version": "1.2.3",
"description": "Google Workspace integration for email, calendar, and drive",
"author": "Your Name",
"platforms": {
"darwin-arm64": {
"binaryName": "gws",
"sha256": "a1b2c3...",
"signature": "base64...",
"size": 45678900,
"downloadUrl": "https://cdn.neboloop.com/..."
},
"linux-amd64": {
"binaryName": "gws",
"sha256": "d4e5f6...",
"signature": "base64...",
"size": 42000000,
"downloadUrl": "https://cdn.neboloop.com/..."
}
},
"signingKeyId": "",
"envVar": ""
}
The slug is the identifier skills use to reference your plugin. It must be kebab-case, URL-safe, and unique on NeboLoop.
Supported Platforms
Build your binary for as many platforms as possible. At minimum, target darwin-arm64 (Apple Silicon) and linux-amd64.
| 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 |
Your binary must be a single, self-contained executable with no runtime dependencies.
How Skills Use Your Plugin
Skills declare your plugin as a dependency in their SKILL.md frontmatter:
---
name: gmail
description: Send and read Gmail messages
plugins:
- name: gws
version: ">=1.2.0"
---
When the skill is installed, Nebo automatically downloads the gws binary for the user's platform. The skill's scripts receive the binary path via the GWS_BIN environment variable.
Script Examples
Bash:
#!/bin/bash
$GWS_BIN gmail list --limit 10
Python:
#!/usr/bin/env python3
import os, subprocess
gws_bin = os.environ["GWS_BIN"]
result = subprocess.run([gws_bin, "gmail", "list", "--limit", "10"],
capture_output=True, text=True)
print(result.stdout)
TypeScript:
import { execSync } from "child_process";
const gwsBin = process.env.GWS_BIN!;
const output = execSync(`${gwsBin} gmail list --limit 10`, { encoding: "utf-8" });
console.log(output);
The env var naming convention is {SLUG}_BIN with the slug uppercased and hyphens replaced by underscores. A plugin with slug my-tool becomes MY_TOOL_BIN.
Adding Authentication
If your plugin requires user credentials (OAuth, API keys, etc.), declare an auth block in the manifest. Nebo handles the entire auth UX -- login button in settings, WebSocket-driven OAuth flow, status checks.
{
"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": {
"GOOGLE_CLIENT_ID": "your-client-id",
"GOOGLE_CLIENT_SECRET": "your-client-secret"
}
}
}
How Auth Works
- User clicks "Connect" in Nebo settings
- Nebo spawns
<your-binary> auth loginwith the declaredenvvars - If your binary writes an OAuth URL to stderr or stdout, Nebo opens the browser automatically
- When the process exits with code 0, auth is complete
<your-binary> auth statusis called to verify (exit 0 = authenticated)
Auth Commands
| Command | Required | Purpose |
|---|---|---|
login |
Yes | Triggers the authentication flow. Can be interactive (opens browser) |
status |
No | Checks if user is authenticated. Exit code 0 = yes. Can output JSON with details |
logout |
No | Clears stored credentials |
The commands are appended to your binary path. So "login": "auth login" becomes <binary-path> auth login.
Declaring Events
If your plugin can watch for external changes (new emails, calendar updates, file changes), declare them in the events array. This lets agents subscribe to your events without hardcoding CLI commands.
{
"events": [
{
"name": "email.new",
"description": "Fires when a new email arrives in Gmail",
"command": "gmail +watch --format ndjson --project {{gcp_project}}"
},
{
"name": "calendar.event",
"description": "Fires on calendar event changes",
"command": "calendar +watch --format ndjson",
"multiplexed": true
}
]
}
The NDJSON Protocol
Your watch command must output one JSON object per line to stdout. Two modes are supported:
Single event type (multiplexed: false, the default):
Each line is emitted as the declared event. The entire JSON line becomes the payload.
{"messageId": "123", "from": "alice@example.com", "subject": "Hello"}
{"messageId": "456", "from": "bob@example.com", "subject": "Meeting"}
Both lines emit as gws.email.new.
Multiplexed (multiplexed: true):
Each line may contain an "event" field that selects the event type. The field is stripped from the payload.
{"event": "email.new", "messageId": "123", "from": "alice@example.com"}
{"event": "email.read", "messageId": "456"}
{"event": "calendar.updated", "eventId": "789", "title": "Standup"}
Lines emit as gws.email.new, gws.email.read, and gws.calendar.updated respectively. If a line has no event field, the declared event name is used as fallback.
Template Substitution
Event commands support {{key}} placeholders that are replaced with values from the agent's input configuration at runtime. This lets you parameterize watch commands without hardcoding credentials or project IDs.
"command": "gmail +watch --format ndjson --project {{gcp_project}}"
How Agents Consume Events
An agent references your plugin event in its agent.json:
{
"email-watcher": {
"trigger": {
"type": "watch",
"plugin": "gws",
"event": "email.new"
},
"description": "React to new emails",
"activities": [...]
}
}
The agent doesn't need to know the CLI command -- it's resolved from your manifest. Events are also auto-emitted into Nebo's EventBus, so other agents can subscribe to gws.email.new without running their own watch process.
Bundling Skills with Your Plugin
The preferred distribution method is a .napp archive that bundles your binary with embedded skills:
gws.napp
├── manifest.json # Package identity
├── plugin.json # Your plugin manifest
├── PLUGIN.md # Documentation
├── gws # The binary
└── skills/ # Skills that use this plugin
├── gws-gmail/
│ └── SKILL.md
├── gws-calendar/
│ └── SKILL.md
└── gws-drive/
└── SKILL.md
When a user installs the plugin, all embedded skills are installed automatically. The skill loader discovers them via the skills/ directory.
Publishing to NeboLoop
Prerequisites
- A NeboLoop developer account
- Your binary compiled for target platforms
- A
PLUGIN.mddocumenting your plugin
Step by Step
Select your developer account:
developer(resource: account, action: select, id: "<your-dev-account-id>")Create the plugin:
plugin(action: create, name: "my-plugin", category: "connectors")Get an upload token:
plugin(action: binary-token, id: "<PLUGIN_ID>")Returns a curl command with a 5-minute expiry.
Upload your binary for each platform:
curl -X PUT "<upload-url>" \ -F "binary=@./build/my-plugin-darwin-arm64" \ -F "platform=darwin-arm64" \ -F "manifest=@./PLUGIN.md"Repeat for
linux-amd64,windows-amd64, etc.Submit for review:
plugin(action: submit, id: "<PLUGIN_ID>", version: "1.0.0")
After approval, your plugin receives a PLUG-XXXX-XXXX install code. Users can paste it into Nebo, but more commonly your plugin is installed automatically as a dependency of the skills that use it.
Testing Locally
Place your binary in the expected directory structure without NeboLoop:
macOS:
mkdir -p ~/Library/Application\ Support/nebo/plugins/my-plugin/0.1.0/
cp ./build/my-plugin ~/Library/Application\ Support/nebo/plugins/my-plugin/0.1.0/
chmod 755 ~/Library/Application\ Support/nebo/plugins/my-plugin/0.1.0/my-plugin
Linux:
mkdir -p ~/.local/share/nebo/plugins/my-plugin/0.1.0/
cp ./build/my-plugin ~/.local/share/nebo/plugins/my-plugin/0.1.0/
chmod 755 ~/.local/share/nebo/plugins/my-plugin/0.1.0/my-plugin
Then create a skill in your user/skills/ directory with plugins: [{name: "my-plugin", version: "*"}]. Nebo resolves it locally -- no NeboLoop needed.
To test auth or events locally, add a plugin.json alongside your binary with the relevant auth or events configuration.
Security
Nebo verifies every plugin binary:
- SHA256 hash checked on download -- any mismatch rejects the binary
- ED25519 signatures verified when the signing key is available
- Quarantine on revocation -- binary is deleted, a
.quarantinedmarker is written, and dependent skills are dropped - Offline-safe -- after install, everything resolves locally with no network needed
Next Steps
- Plugin Reference -- complete field reference for plugin.json
- Creating a Skill -- write skills that depend on your plugin
- Creating an Agent -- build agents with watch triggers for plugin events
- Publishing to the Marketplace -- submit for review