How plugins work
A plugin is an executable plus a manifest. Nebo keeps no plugin process running between calls. Each time an employee uses the plugin, Nebo starts the binary with a command and arguments, waits for it to exit, and hands the output back to the employee.
- Commands are arguments. The employee chooses a command, such as
notes search, and named arguments. Nebo passes each argument as--name value. There is no shell, so pipes and redirects never apply. - Output is stdout. Write results to stdout. JSON works well.
- Standard input is closed for ordinary calls. Hooks, routes and chat providers use stdin; see the plugin.json reference.
Every installed and enabled plugin appears to employees as one tool named plugin__<slug>. An employee calls it like this:
{
"command": "notes search",
"args": { "query": "quarterly plan", "limit": "5" },
"display": "Search notes for the quarterly plan"
} Nebo runs:
notes-sync notes search --query "quarterly plan" --limit 5
| Parameter | Type | Meaning |
|---|---|---|
command | string, required | The plugin command and any positional words. |
args | object of strings | Named arguments. Each becomes --key value. |
timeout | integer | Seconds to wait. Default 120, capped by the manifest’s permissions.maxTimeoutSeconds. |
display | string | The sentence the owner sees when the call needs approval. |
Build your first plugin
Write a binary in any language that reads its command from the arguments and prints a result.
Then place it next to a plugin.json in the user plugins directory of a bot.
<data>/user/plugins/notes-sync/
├── plugin.json # runtime manifest
├── notes-sync # your binary, named after the slug
└── skills/ # optional: skills that teach employees to use it
└── notes-sync/
└── SKILL.md {
"id": "notes-sync",
"slug": "notes-sync",
"name": "Notes Sync",
"version": "0.1.0",
"description": "Search, read and create notes in Example Notes.",
"platforms": {}
} id, slug, name, version and platforms are required. In the user plugins directory the platforms map
can be empty: Nebo looks for a binary named after the slug next to plugin.json, then
in target/release/ and target/debug/. When you publish, the marketplace
fills in platforms for every binary you upload.
<data> is ~/Library/Application Support/Nebo on macOS, %APPDATA%\Nebo on Windows, and ~/.local/share/nebo on Linux, or the value
of NEBO_HOME. A plugin in the user directory overrides a marketplace plugin with the
same slug, which makes it the place to test a new build.
Teach employees to use it
Put a skills/ folder next to plugin.json. Each skill in it is available to
every employee while the plugin is installed, and lists the plugin as a dependency automatically.
Use the skill to say which commands exist, what they return, and when to use each. Name the most
useful skills in entrySkills so the plugin's tool description points to them first.
The marketplace listing is a separate file, PLUGIN.md. Nebo does not read it at run
time; it is the page people see before they install. See Shipping a plugin.
Environment
Each call inherits the bot's environment with loader and shell-injection variables removed, then adds these values. Later entries win.
| Variable | Value |
|---|---|
auth.env keys | Defaults from the manifest. |
<SLUG>_BIN | Path of this plugin’s binary, and of each plugin it depends on. The slug is uppercased with hyphens as underscores. |
NEBO_DATA_DIR | The plugin’s own data folder, <data>/appdata/plugins/<slug>/. It is also the working directory, and it survives uninstall. |
PATH | Plugin folders first, then the inherited path. |
| Settings | Values the owner saved for configSchema fields. |
NEBO_AGENT_ID | The employee making the call. |
profileDirEnv | The account folder for this employee and account, when the plugin supports accounts. |
permissions.envAllow and permissions.envDeny narrow what the plugin
inherits from the bot. They never remove the plugin's own auth variables.
Accounts and credentials
A plugin that signs in to a service should hold one account per employee. The bookkeeper signs in with the finance login and the assistant with its own, and neither sees the other's tokens. You declare this in the manifest; Nebo does the rest.
"auth": {
"type": "env",
"label": "Example Notes account",
"env": { "EXAMPLE_NOTES_TOKEN": "" },
"commands": { "login": "auth login", "status": "auth status", "logout": "auth logout" },
"profileDirEnv": "EXAMPLE_NOTES_CONFIG_DIR"
},
"capabilities": {
"configSchema": [
{ "key": "EXAMPLE_NOTES_TOKEN", "label": "API token", "fieldType": "password", "secret": true, "required": true }
]
} auth.envnames the credentials.capabilities.configSchemalabels them and sets their order in the connect form.auth.profileDirEnvnames the variable your plugin reads for its credential folder. Nebo creates one folder for each employee and account and sets that variable on every call.- When the owner connects an account, Nebo runs your
logincommand with the values the owner typed and the account folder set. Your plugin saves what it needs in that folder. Nebo does not keep the values. - Your
statuscommand prints JSON such as{"authenticated": true}. When a token expires, Nebo runsrefreshif you declare one.
For a browser sign-in, set type to oauth_cli and have login print or open the provider's sign-in URL. Employees never sign in by themselves:
when a call needs a login, the owner sees a connect card. An employee with several accounts picks
one by adding --account <label> to a command. Nebo removes that flag before
your binary sees it.
Name the service, not the shape. A plugin's name says what it connects to, such as "Example Notes". Never put "CLI" in a plugin name; to employees and owners it is a connection, not a command line.
Next
- plugin.json reference: every field, including events, hooks and typed operations.
- Shipping a plugin: platforms, binaries, signing and updates.
- Publishing: the marketplace flow for every artifact.