Permissions
How your plugin declares the environment variables it needs, and how Kazibee resolves them from the right source.
Quick reminder: sandbox isolation
Your plugin runs in a sandbox. It cannot read process.env or access the host machine's environment directly. The only way your plugin gets credentials is through the env object passed to main(env). That object only contains variables you declared in permissions.json and the user explicitly granted.
Why permissions.json exists
Because plugins are sandboxed, you cannot just call process.env.MY_API_KEY like you would in a normal Node app. Instead, you create a permissions.json file that tells Kazibee: "My plugin needs these environment variables to function."
When a user installs your plugin, Kazibee reads this file and asks them to approve each variable. At runtime, only approved variables are injected into the env object your main(env) function receives. Think of it as a permission slip — "I need access to X, do you allow it?"
The permissions.json structure
The file has one top-level key: "env". Inside it, each key is the variable name your plugin code will see on the env object. The value tells Kazibee where to look for it.
The simplest case
If your plugin needs one API key, your permissions file looks like this:
{
"env": {
"API_KEY": "API_KEY"
}
}
The left side ("API_KEY") is the key your plugin code uses: env.API_KEY. The right side tells Kazibee which source key to resolve.
A plugin with many variables
The Chrome Browser plugin needs seven environment variables. Each one follows the same pattern:
{
"env": {
"GEMINI_API_KEY": "GEMINI_API_KEY",
"CHROME_PATH": "CHROME_PATH",
"CHROME_USER_DATA_DIR": "CHROME_USER_DATA_DIR",
"CHROME_HEADLESS": "CHROME_HEADLESS",
"CHROME_REMOTE_DEBUGGING_PORT": "CHROME_REMOTE_DEBUGGING_PORT",
"CHROME_CDP_URL": "CHROME_CDP_URL",
"CHROME_AUTO_LAUNCH": "CHROME_AUTO_LAUNCH"
}
}
At install time, the user is prompted for each of these seven variables. They grant or deny each one individually.
Don't forget package.json
Point to your permissions file from package.json so Kazibee knows where to find it:
{
"name": "my-plugin",
"kazibee": {
"permissions": "./permissions.json"
}
}
Where values come from: the four sources
When a user grants a permission, they choose where Kazibee should look for the actual value. There are four possible sources, listed from highest to lowest priority:
LOCAL (highest priority)
Directory-scoped values, set with kazibee env <plugin> --set KEY=value. These only apply when Kazibee runs from that specific directory. Useful when you have different credentials per project — for example, different API keys for a staging project vs. a production project.
ENV
Reads directly from the host machine's process.env. This is for values you already have set in your shell profile, .bashrc, or CI environment. Kazibee does not store the value — it reads it live at runtime.
GLOBAL
User-wide values, set with kazibee env --global <plugin> --set KEY=value. These apply no matter which directory you run from. Good for credentials that are the same everywhere, like a personal API key.
SYSTEM (lowest priority)
Values set automatically by a plugin's setup script during install. The plugin author writes a setup.ts that pre-populates default values (like a default database URL). These are always overridden by any other source.
Priority order
When multiple sources have a value for the same key, Kazibee uses the highest-priority one: LOCAL > ENV > GLOBAL > SYSTEM. A LOCAL value always wins. A SYSTEM value is only used if nothing else is available.
Unscoped vs. scoped candidates
The value side of each entry in permissions.json controls where Kazibee looks for the actual value. You have two options:
Unscoped (any source)
Just write the key name with no prefix. At install time, the user picks which source to use. If they choose "any source," Kazibee checks them in priority order: LOCAL, then ENV, then GLOBAL, then SYSTEM.
{
"env": {
"API_KEY": "API_KEY" // user picks the source at install time
}
}
This is the most common pattern. Most plugins use unscoped keys exclusively.
Scoped (exact source)
Prefix the key with a source name to force resolution from that exact source. If the value does not exist in that source, it will not be injected — Kazibee will not silently fall back to another source.
{
"env": {
"DB_URL": "LOCAL:DB_URL", // only from local project scope
"SHARED_KEY": "GLOBAL:SHARED_KEY" // only from global config
}
}
Use scoped candidates when you know the value should only ever come from one place. For example, a database URL that is always project-specific should be LOCAL:DB_URL so it never accidentally picks up a global value.
Array fallbacks
Sometimes you want to try multiple sources in a specific order. Use an array — Kazibee tries each candidate in order and uses the first one that has a value.
Real-world example: shared API keys
Both the Chrome Browser plugin and the Image Gen plugin need a GEMINI_API_KEY. A user might already have this key set in their shell environment from installing the Chrome Browser plugin. With array fallbacks, the Image Gen plugin can reuse that existing value instead of making the user configure it again:
{
"env": {
"GEMINI_API_KEY": [
"ENV:GEMINI_API_KEY", // first, check host process.env
"GLOBAL:GEMINI_API_KEY", // then, check global config
"LOCAL:GEMINI_API_KEY" // finally, check local project
]
}
}
At install time, the user can select which candidate to use, or let Kazibee try them in order at runtime.
Key renaming
The left side of each entry is the name your plugin code sees on the env object. The right side is the name Kazibee looks up in the source. These do not have to match.
When is this useful?
Suppose you are building a plugin that uses the Gemini API, but your code internally calls it LLM_KEY. The user already has a GEMINI_API_KEY saved from another plugin. Instead of making them save the same key under a new name, you rename it:
{
"env": {
"LLM_KEY": "GEMINI_API_KEY" // env.LLM_KEY in code, resolved from GEMINI_API_KEY source
}
}
Your plugin code uses env.LLM_KEY, but the value comes from the user's existing GEMINI_API_KEY. No duplicate configuration needed.
What happens at install time
When a user runs kazibee install or kazibee link, Kazibee reads your permissions.json and shows an interactive prompt for each variable. The user selects which source to allow, or denies it entirely.
Example: installing a Gmail plugin
dev@local:~/workspace $ kazibee link gmail ./gmail
Linking gmail...
Permissions required:
CLIENT_ID
Grant from? [LOCAL / ENV / GLOBAL / SYSTEM / deny]: LOCAL
CLIENT_SECRET
Grant from? [LOCAL / ENV / GLOBAL / SYSTEM / deny]: LOCAL
REFRESH_TOKEN
Grant from? [LOCAL / ENV / GLOBAL / SYSTEM / deny]: LOCAL
Linked successfully. 3 permissions granted.
The user chose LOCAL for each variable, which means Kazibee will look for these values in the local project scope at runtime. They still need to set the actual values with kazibee env or a login command.
Setting the actual value
Granting the permission just tells Kazibee where to look. The user still needs to provide the actual value:
dev@local:~/workspace $ # Set a local value (only in this directory)
dev@local:~/workspace $ kazibee env gmail --set CLIENT_ID=xxx CLIENT_SECRET=yyy
dev@local:~/workspace $ # Or set a global value (available everywhere)
dev@local:~/workspace $ kazibee env --global gmail --set CLIENT_ID=xxx CLIENT_SECRET=yyy
Skipping the interactive prompts
In CI environments or when reinstalling a plugin you have already configured, you can skip the permission prompts:
dev@local:~/workspace $ kazibee install gmail --skip-permissions
When --skip-permissions is used, any previously saved grants remain unchanged. New permissions that have never been granted will remain ungranted until the user runs the permission flow again.
What happens at runtime
Every time your plugin runs, Kazibee builds the env object by resolving each granted variable from the source the user selected:
For each key in permissions.json, check if the user granted it
Look up the value from the granted source (LOCAL store, host process.env, GLOBAL store, or SYSTEM store)
If the value exists, add it to the env object. If not, skip it
Pass the filtered env object to main(env)
No silent source substitution
If the user granted ENV:API_KEY but the key is not in their process.env, Kazibee will not silently fall back to LOCAL or GLOBAL. The variable simply will not be injected. This is a deliberate security choice — you always know exactly where your values are coming from.
Setup scripts (SYSTEM-level defaults)
If your plugin has sensible default values (like a default database URL or a default port), you can provide a setup script that runs once at install time. Values set by the setup script become SYSTEM-level entries — the lowest priority source, easily overridden by the user.
Declaring a setup script
{
"kazibee": {
"permissions": "./permissions.json",
"setup": "./setup.ts"
}
}
Writing the setup script
Export a default function that receives an empty env object. Set key-value pairs on it, and they become SYSTEM-level defaults:
export default async (env) => {
env.DATABASE_URL = "sqlite://local.db";
env.DEFAULT_PORT = "3000";
};
The setup script runs before the permission prompts, so SYSTEM values are available as an option during install. If the user later sets the same key via LOCAL or GLOBAL, their value takes precedence.
Troubleshooting
My plugin gets an empty env object
Check three things: (1) Your package.json points to the correct permissions.json path. (2) The user actually granted the permissions during install (re-run kazibee link without --skip-permissions to go through the prompts again). (3) The actual values were set in the granted source with kazibee env.
My variable is set, but the plugin does not see it
The value might exist in a different source than what was granted. If the user granted ENV:API_KEY but set the value with kazibee env --set (which writes to LOCAL), the plugin will not see it. Kazibee does not silently substitute sources. The source must match.
Multiple plugins need the same key
Use GLOBAL source for keys shared across plugins (like GEMINI_API_KEY). Set it once with kazibee env --global <plugin> --set KEY=value, and grant both plugins access from GLOBAL. Alternatively, use the ENV source if the key is already in your shell environment.