Plugins
September 2, 2026 · View on GitHub
Portable Agent Plugins
GemiHub also loads portable Agent Plugins v1.0.0. This is additive to the GemiHub-specific manifest.json + main.js plugin format documented below.
Open Settings > Plugins > Agent Plugins, enter a GitHub repository (owner/repo), and preview the package before installing it. GemiHub uses the latest GitHub Release tag when one exists and otherwise uses the repository's default branch. The resolved commit SHA is stored so updates are reproducible.
Self-hosted deployments should set the optional server-side AGENT_PLUGINS_GITHUB_TOKEN environment variable to a fine-grained GitHub token with read-only access to the repositories being installed. The token is never sent to the browser or stored in Drive; it only raises the GitHub API rate limit for server-side discovery requests.
Supported portable components:
| Component | Support |
|---|---|
Agent Skills in skills/<name>/SKILL.md | Yes |
streamable-http servers in root mcp.json | Yes |
stdio MCP servers | No (reported and skipped) |
Legacy sse MCP servers | No (reported and skipped) |
| Client extension namespaces | Ignored unless GemiHub adds explicit support |
The repository root must contain plugin.json using the canonical v1.0.0 schema identifier. Skills appear in chat under an internal {plugin-name}.{skill-name} ID, which prevents collisions with Drive-native skills. Imported MCP URLs and literal headers remain package-managed; OAuth credentials discovered or configured by GemiHub are preserved across plugin updates.
Agent Plugin packages are stored separately under gemihub/agent-plugins/{plugin-name}/. Disabling a package hides its skills and MCP servers without deleting it. Uninstalling removes the package and only the MCP entries owned by that package.
Install and update call tools/list for each bundled MCP server and cache its
tool definitions. A discovery failure produces a warning without discarding the
package; retry with Test in Settings > MCP Servers after correcting the
connection or credentials. Enabled Agent Plugin MCP servers are selected in
chat once by default when this behavior is first introduced. Later updates do
not overwrite the user's explicit selection stored in localStorage.
Extend Gemini Hub with community plugins installed from GitHub Releases. Inspired by Obsidian + BRAT.
Features
- GitHub Release Install: Install plugins from any public GitHub repo
- Trusted Execution: Plugins run with the same privileges as the host app. Only install plugins you trust
- Drive Storage: Plugin files and data stored in Google Drive (
plugins/folder) - IndexedDB Cache: Plugin assets cached client-side for fast loading
- Hot Enable/Disable: Toggle plugins without page reload
- Plugin API: Access Gemini AI, Drive files, Google Calendar, Gmail, and plugin-scoped storage
For Users
Installing a Plugin
- Go to Settings > Plugins
- Enter the GitHub repository (e.g.
owner/repoorhttps://github.com/owner/repo) - Click Install
The latest GitHub Release must contain at least manifest.json and main.js. styles.css is optional.
Managing Plugins
| Action | Description |
|---|---|
| Enable/Disable | Toggle the power icon next to the plugin |
| Update | Click the refresh icon to pull the latest release |
| Uninstall | Click the trash icon (removes all plugin data from Drive) |
Where Plugin Data is Stored
gemihub/
plugins/
{plugin-id}/
manifest.json ← Plugin metadata
main.js ← Plugin code
styles.css ← Plugin styles (optional)
data.json ← Plugin-scoped storage
For Plugin Developers
Project Structure
A plugin release must contain these assets:
| File | Required | Description |
|---|---|---|
manifest.json | Yes | Plugin metadata |
main.js | Yes | Plugin entry point (bundled) |
styles.css | No | Plugin styles |
manifest.json
{
"id": "my-plugin",
"name": "My Plugin",
"version": "1.0.0",
"minAppVersion": "1.0.0", // reserved for future use
"description": "What my plugin does",
"author": "Your Name",
"assets": [
{ "name": "model.wasm", "url": "https://cdn.example.com/model.wasm" }
]
}
The id must be unique and is used as the Drive folder name and IndexedDB cache key.
The optional assets array declares external files (e.g. WASM binaries, large models) that the plugin needs at runtime. Each entry has a name (filename to reference from code) and a url (HTTPS URL the server downloads from). See External Assets for details.
main.js
Build with esbuild (or similar) with react and react-dom as externals. The entry point must export a class with an onload method:
class MyPlugin {
onload(api) {
// Called when plugin is loaded
// Use api to register views, commands, or call host APIs
}
onunload() {
// Called when plugin is disabled or uninstalled
// Clean up event listeners, timers, etc.
}
}
// Both export styles are supported:
module.exports = MyPlugin;
// or: module.exports.default = MyPlugin;
Example esbuild config:
require("esbuild").build({
entryPoints: ["src/main.tsx"],
bundle: true,
outfile: "main.js",
format: "cjs",
external: ["react", "react-dom", "react-dom/client"],
jsx: "automatic",
});
Plugin API
The api object passed to onload provides:
Language
// Current language setting ("en", "ja", etc.)
const lang = api.language;
UI Registration
// Register a sidebar or main view
api.registerView({
id: "my-view",
name: "My View",
icon: "puzzle", // optional icon identifier
location: "sidebar", // or "main"
extensions: [".csv", ".tsv"], // optional: dot-prefixed file extensions (main views only)
component: MyReactComponent, // receives { api, fileId?, fileName? } as props
});
// Register a slash command for the chat
api.registerSlashCommand({
name: "my-command",
description: "Does something",
execute: async (args) => {
return "result text";
},
});
// Register a settings tab (shown in Settings > Plugins via gear icon)
api.registerSettingsTab({
component: MySettingsComponent, // receives { api, onClose } as props
});
// Register a custom dashboard widget type (same registry as core widgets)
api.registerWidget({
type: "base", // referenced from a .dashboard widget's `type`
label: "Base",
defaultConfig: {},
render: (config, ctx) => <MyWidget config={config} ctx={ctx} />,
// optional ConfigEditor shown in the widget settings panel
});
A .dashboard references a plugin widget simply by type (e.g. { type: "base", config: { base: "Dashboards/x.base", view: "..." } }). Dashboards load before plugins, so a not-yet-registered type renders as a placeholder (UnknownWidget) with its config preserved on save; once the plugin calls registerWidget, the dashboard re-renders and swaps in the real widget automatically (via a dashboard-widgets-changed event). The widget render receives a ctx with size, widgetId, dashboardFileId, and onConfigChange(config) (persist config directly from the widget itself, without opening the settings panel). To read/create the referenced data file (e.g. a .base), use api.drive.readFile/searchFiles — note api.drive.listFiles does not enumerate the Dashboards/ folder.
Gemini AI
const response = await api.gemini.chat(
[
{ role: "user", content: "Hello" },
],
{
model: "gemini-3.8-flash", // optional
systemPrompt: "You are helpful", // optional
}
);
// response is the model's text reply
Active File Change Listener
// Subscribe to file selection changes in the IDE
const unsubscribe = api.onActiveFileChanged(({ fileId, fileName, mimeType }) => {
if (fileId) {
console.log(`File opened: ${fileName} (${fileId})`);
} else {
console.log("File closed");
}
});
// Call the returned function to unsubscribe
unsubscribe();
Google Drive
All drive operations are local-first — reads come from the IndexedDB cache (with server fallback on cache miss), and writes update the local cache and edit history. Changes are synced to Google Drive when the user pushes.
// Read a file
const content = await api.drive.readFile(fileId);
// Search files by name
const files = await api.drive.searchFiles("query");
// List files in a folder (omit folder to list files in the gemihub root folder)
const files = await api.drive.listFiles(folder);
// Create a text file (created in the gemihub root folder)
const { id, name } = await api.drive.createFile("notes.md", "# Notes");
// Create a binary file (pass ArrayBuffer)
const response = await fetch(imageUrl);
const buffer = await response.arrayBuffer();
const { id, name } = await api.drive.createFile("photo.png", buffer);
// Update a file (text)
await api.drive.updateFile(fileId, "new content");
// Update a file (binary — updated locally, push to sync to Drive)
await api.drive.updateFile(fileId, arrayBuffer);
// Rebuild the file tree from Drive
await api.drive.rebuildTree();
Note:
createFilereturns a temporarynew:*prefixed ID for newly created files. This ID works with all otherapi.drivemethods. After a push, the file receives a real Drive ID.updateFileautomatically resolves stalenew:*IDs by falling back to file name lookup.
Google Calendar (Premium)
Requires the premium plan with calendar scope. The calendar property is optional — check for its existence before use.
if (api.calendar) {
// List events
const events = await api.calendar.listEvents({
timeMin: "2026-04-01T00:00:00Z",
timeMax: "2026-04-30T23:59:59Z",
maxResults: 10,
});
// Create an event
const { eventId, htmlLink } = await api.calendar.createEvent({
summary: "Meeting",
start: "2026-04-10T10:00:00+09:00",
end: "2026-04-10T11:00:00+09:00",
});
// Update an event
await api.calendar.updateEvent(eventId, { summary: "Updated Meeting" });
// Delete an event
await api.calendar.deleteEvent(eventId);
}
Gmail (Premium)
Requires the premium plan with gmail.send scope. The gmail property is optional — check for its existence before use.
if (api.gmail) {
const { messageId } = await api.gmail.sendEmail({
to: "recipient@example.com",
subject: "Hello from GemiHub",
body: "<p>This is an HTML email.</p>",
cc: "cc@example.com", // optional
bcc: "bcc@example.com", // optional
});
}
Plugin Storage
Each plugin has its own data.json on Drive for persistent key-value storage:
// Get a value
const value = await api.storage.get("myKey");
// Set a value
await api.storage.set("myKey", { count: 42 });
// Get all stored data
const all = await api.storage.getAll();
External Assets
Plugins can declare external assets (WASM binaries, ML models, etc.) in manifest.json. The server acts as a caching proxy — it downloads the file from the declared URL on the first request and serves it from the local cache (data/plugins/{id}/) on subsequent requests.
// Fetch a declared asset by name (returns ArrayBuffer)
const buf = await api.assets.fetch("model.wasm");
const module = await WebAssembly.instantiate(buf);
Rules:
- Assets must be declared in the
assetsarray ofmanifest.json. Requests for undeclared names are rejected (403). - URLs must use HTTPS. Private/internal addresses (localhost, 169.254.x, 10.x, etc.) are blocked.
- The asset cache is automatically cleared on plugin update or uninstall.
- Asset names must be flat filenames (no
/,\, or leading.).
React
The host's React instances are available to avoid version mismatches:
const React = api.React;
const ReactDOM = api.ReactDOM;
View Components
View components receive { api, fileId?, fileName? } as props. fileId and fileName are provided when a file is currently open (always set for extension-matched views, optional for panel-selected main views):
function MyPanel({ api, fileId, fileName }) {
const React = api.React;
const [data, setData] = React.useState(null);
React.useEffect(() => {
if (fileId) {
api.drive.readFile(fileId).then(setData);
}
}, [fileId]);
return <div>{fileName}: {JSON.stringify(data)}</div>;
}
- Sidebar views appear as tabs in the right panel (alongside Chat and Workflow)
- Main views replace the file editor in the main area
Security
- Plugins run with the same privileges as the host app (full access to DOM,
fetch, etc.) - Only install plugins from sources you trust
require()shim only providesreact,react-dom, andreact-dom/client- Plugin code runs in the browser, not on the server
Credential boundary
PluginAPI never exposes the decrypted Gemini API key (api-key-cache.ts) or
the decrypted RSA private key and encryption password (crypto-cache.ts), and
there is no "decrypt this for me" call — a plugin sees .encrypted files only
as ciphertext. plugin-file-guard.ts additionally refuses api.drive access to
the files that would make an offline attack on those credentials possible, or
that would let one plugin rewrite another:
| Path | Why |
|---|---|
settings.json, _encrypted-auth.json | hold encryptedApiKey, encryptedPrivateKey, and the PBKDF2 salt |
plugins/** | another plugin's code — writable code is privilege escalation |
_sync-meta.json | the sync registry; rewriting it corrupts push/pull state |
Reads, writes, listings, and searches are all filtered, including id-addressed calls (the id is resolved to its path first).
This is a boundary on the API surface, not a sandbox: plugin code shares the
page's realm, so a hostile plugin can still patch globals such as fetch and
observe what the host itself sends. Isolating plugins would require moving them
out of the realm (worker or sandboxed iframe with a postMessage bridge), the way
workflow script nodes already run. plugin-file-guard.test.ts fails the build
if the API surface grows a member that carries credential material.
Local Development
In development mode (NODE_ENV !== "production"), plugins can be loaded directly from the local filesystem without installing via GitHub. Place your plugin files in the plugins/{id}/ directory at the project root:
plugins/
my-plugin/
manifest.json
main.js
styles.css ← optional
Local plugins are automatically detected and always enabled. The IndexedDB cache is bypassed, so changes to main.js or styles.css take effect on the next page reload without needing to update the version. Local plugins cannot be uninstalled from the UI — simply remove the directory to unload them.
Publishing
- Create a GitHub repository for your plugin
- Build
main.jsandmanifest.json(and optionallystyles.css) - Create a GitHub Release and attach the files as release assets
- Users install via
owner/repoin Settings > Plugins
To publish an update, create a new release with an updated version tag. Users click the update button to pull the latest.
API Routes
| Method | Path | Description |
|---|---|---|
| GET | /api/plugins | List installed plugins |
| POST | /api/plugins | Install plugin { repo } |
| GET | /api/plugins/:id?file={name} | Serve plugin file (main.js, styles.css, manifest.json) |
| GET | /api/plugins/:id?asset={name} | Serve a cached external asset declared in manifest |
| POST | /api/plugins/:id | Actions: toggle, getData, setData, update, checkUpdate |
| DELETE | /api/plugins/:id | Uninstall plugin |
| POST | /api/calendar | Calendar operations: list, create, update, delete |
| POST | /api/gmail | Gmail operations: send |