Just want to use plugins? The plugin gallery has seven ready-made plugins — Checksum & Verify, Bulk Metadata, Project Cleaner, Image Toolkit, Git Quick Actions, GitHub Browser and Share as Gist — with install instructions. This guide is for writing your own.
Overview
Captain's Deck plugins are written in JavaScript and run in a sandboxed environment. Plugins can:
- Add custom context menu items
- Read and write files
- Show notifications and dialogs
- Show results in report and table windows that users can copy or save
- Access the clipboard
- Make network requests
- Register custom file system providers
- Store persistent settings
Plugins declare which APIs they intend to use in their manifest. At runtime, an API the plugin didn’t declare doesn’t work. For file access and dialogs, the call returns a rejected Promise — for example captain.fs.write on a plugin that declared fileSystem.read but not fileSystem.write, or showQuickPick without dialogs. For the other namespaces (clipboard, contextMenu, http, shell, statusBar, settings, providers), and for captain.fs with no file-system permission at all, the methods aren’t there, so calling one throws a TypeError. captain.ui.notify without notifications simply shows nothing. Shell access has a second gate: even with the shell permission declared, the plugin only runs commands when Allow plugins to execute shell commands is checked in Settings → Plugins.
Users approve a plugin’s permissions when they first enable it, and again whenever an updated manifest asks for more. Until they do, the plugin doesn’t run.
Getting Started
Plugin Location
Plugins are installed in:
~/Library/Application Support/CaptainsDeck/Plugins/
Each plugin lives in its own folder:
Installing and removing
In Settings → Plugins, Install Plugin… takes a ZIP of the plugin folder; tick the checkbox to turn it on. To uninstall, select the plugin and click Remove Plugin…: it’s turned off, its folder goes to the Trash, and its saved data, the permissions you granted it and its Keychain secrets are cleared.
Minimal Plugin
Create a folder and two files:
manifest.json{
"name": "Hello World",
"id": "com.example.hello-world",
"version": "1.0.0",
"apiVersion": "1",
"main": "main.js",
"description": "A simple hello world plugin",
"permissions": ["notifications"]
}
main.js
// Called when the plugin is activated
exports.activate = function(context) {
console.log("Hello World plugin activated!");
captain.ui.notify({
title: "Hello!",
message: "Plugin loaded successfully"
});
};
Plugin Structure
Lifecycle
- Discovery - Captain's Deck scans the Plugins folder for
manifest.jsonfiles - Validation - Manifests are parsed and validated
- Activation - An enabled plugin's
main.jsis loaded when the app starts, and when you tick its checkbox in Settings → Plugins - Runtime - Plugin responds to events and user actions
- Deactivation - Plugin is unloaded when disabled or app quits; its file watchers, menu bar items and providers are removed with it
Entry Points
Your main.js exports an activate function:
// Called when plugin activates
exports.activate = function(context) {
// Initialize your plugin here
// Register event handlers, context menus, etc.
};
You can also export a deactivate function. Since Captain’s Deck 1.5.5.1 it runs when the plugin is turned off, removed, reinstalled or reloaded. Since 1.5.5, turning a plugin off or removing it also stops its file watchers, removes its menu bar items and unregisters its providers for you.
// Called when the plugin is turned off (1.5.5.1 and later)
exports.deactivate = function() {
// save state, close connections...
};
Manifest Reference
Required Fields
| Field | Type | Description |
|---|---|---|
name |
string | Display name of the plugin |
id |
string | Unique identifier in reverse-domain form: two or more dot-separated parts, each starting with a lowercase letter and using only lowercase letters, digits and hyphens (e.g. com.example.hello-world; com.example.helloWorld is rejected) |
version |
string | Semantic version (e.g., "1.0.0") |
main |
string | Entry point JavaScript file |
Optional Fields
| Field | Type | Description |
|---|---|---|
apiVersion |
string | Defaults to "1" when left out, and "1" is the only accepted value. Any other value is rejected and the plugin won’t load. Newer APIs are detected at runtime with captain.apiLevel (see API Level 2) |
description |
string | Short description of the plugin |
author |
string | Author name or organization |
homepage |
string | URL to plugin homepage |
license |
string | License identifier (e.g., "MIT") |
icon |
string | Reserved: read from the manifest but not shown anywhere in the app yet |
Permissions
Declare required permissions in the permissions array:
| Permission | Description | Risk Level |
|---|---|---|
fileSystem.read |
Read files and directories | Low |
fileSystem.write |
Create, modify, and move files to the Trash | High |
fileSystem.watch |
Watch for file changes | Low |
contextMenu |
Add context menu items | Low |
notifications |
Show system notifications | Low |
statusBar |
Display status bar items | Low |
dialogs |
Show dialogs and prompts | Low |
clipboard |
Access system clipboard | Medium |
network |
Make HTTP requests | High |
shell |
Execute shell commands | High |
providers |
Register file providers | High |
settings |
Store the plugin’s own settings; read display settings | Medium |
Activation Events
The manifest may contain an activationEvents array, but Captain’s Deck doesn’t use it yet: every enabled plugin is activated when the app starts and when it’s switched on in Settings. You can leave the field out.
Contributions
Add items to the file context menu with contributes.contextMenus, then handle them with captain.contextMenu.register (see the API Reference):
{
"contributes": {
"contextMenus": [
{
"id": "myPlugin.compress",
"title": "Compress Files",
"when": "selectedCount > 0",
"group": "actions"
}
]
}
}
The optional when condition decides whether the item appears for the current selection:
| Condition | Shows the item when |
|---|---|
selectedCount > 0 / selectedCount >= 1 |
At least one item is selected |
selectedCount == 1 |
Exactly one item is selected |
isDirectory |
Exactly one folder is selected |
isFile |
Exactly one file is selected |
filesOnly |
One or more items are selected, and none of them is a folder (1.5.4+) |
extension:sha256,md5 |
Exactly one file is selected and its extension is one of those listed (1.5.4+) |
name:SHA256SUMS,MD5SUMS |
Exactly one file is selected and its full name is one of those listed (1.5.4+) |
a || b |
Any of the conditions matches, e.g. extension:sha256 || name:SHA256SUMS (1.5.4+) |
extension: and name: ignore upper/lower case; separate several values with commas. Any other condition — or none — shows the item for every selection, so check file types in your handler. Captain’s Deck versions before 1.5.4 don’t know the 1.5.4 conditions and show those items for every selection, so check captain.apiLevel in the handler too. contributes.keybindings is accepted in the manifest but not used yet. contributes.commands puts commands in Quick Open (see Commands).
API Reference
All APIs are accessed through the global captain object.
File System (captain.fs)
Requires fileSystem.read and/or fileSystem.write permissions.
// Read file as base64
const data = await captain.fs.read("/path/to/file");
// Read file as text
const text = await captain.fs.readText("/path/to/file.txt");
// Write file (string or base64)
await captain.fs.write("/path/to/file.txt", "Hello World");
// Get file info
const info = await captain.fs.stat("/path/to/file");
// Returns: { name, path, isDirectory, size, modificationDate, creationDate, permissions }
// List directory
const files = await captain.fs.ls("/path/to/directory");
// Returns: [{ name, path, isDirectory, size }, ...]
// Check if path exists (needs fileSystem.read)
const exists = await captain.fs.exists("/path/to/file");
// Create directory
await captain.fs.mkdir("/path/to/new/directory");
// Move file or directory to the Trash
await captain.fs.rm("/path/to/file");
// Watch a file or folder for changes (needs fileSystem.watch)
const watchId = captain.fs.watch("/path/to/file", (event) => {
// event = { type, path }; type is "write", "delete", "rename", "attrib" or "change"
});
// Stop watching
captain.fs.unwatch(watchId);
Paths must be absolute (~ is expanded). Symlinks are resolved and paths are compared case-insensitively before the check, and access is denied to protected locations: SSH/GPG/cloud credentials (~/.ssh, ~/.aws, …), shell startup files (~/.zshrc, …), keychains, browser profiles, launch agents/daemons, ~/Library/Preferences, Captain’s Deck’s own data folder (including the plugins folder), /etc, /System and /var. That includes exists — probing for a protected file is denied too.
rm moves items to the Trash rather than deleting them permanently, and refuses any folder that contains a protected location.
User Interface (captain.ui)
notify requires the notifications permission; showQuickPick and showInputBox require dialogs.
// Show a notification
captain.ui.notify({
title: "Operation Complete",
message: "Processed 42 files"
});
// Let the user pick one string from a list.
// The dialog's title and placeholder are properties on the array.
const options = ["MD5", "SHA-1", "SHA-256"];
options.title = "Select an algorithm";
options.placeholder = "Choose one";
const choice = await captain.ui.showQuickPick(options);
// Returns the chosen string, or null if the user cancels
// Ask for a line of text
const name = await captain.ui.showInputBox({
title: "Enter a name",
placeholder: "my-file",
value: "default"
});
// Returns the text, or null if the user cancels
Context Menu (captain.contextMenu)
Requires contextMenu permission.
// Register a context menu handler
captain.contextMenu.register("myPlugin.compress", async (files) => {
// files = array of selected file items
for (const file of files) {
console.log(`Selected: ${file.name} at ${file.path}`);
console.log(` Size: ${file.size} bytes`);
console.log(` Is directory: ${file.isDirectory}`);
}
// Do something with the files...
});
Clipboard (captain.clipboard)
Requires clipboard permission.
// Read clipboard
const text = await captain.clipboard.read();
// Write to clipboard
await captain.clipboard.write("Hello from plugin!");
Storage (captain.storage)
Persistent key-value storage for your plugin.
// Save a value
captain.storage.set("myKey", { count: 42, name: "test" });
// Retrieve a value
const value = captain.storage.get("myKey");
// Returns: { count: 42, name: "test" }
// Remove a value
captain.storage.set("myKey", null);
Settings (captain.settings)
Requires settings permission. set writes only to your plugin’s own settings — it can’t change Captain’s Deck’s settings. get returns your plugin’s own value for a key, or else one of the app’s display settings (theme and similar); anything to do with connections, licences, plugins or admin access stays hidden.
// Save one of your plugin's settings
captain.settings.set("showHidden", true);
// Read it back (or a display-only app setting)
const showHidden = captain.settings.get("showHidden");
// Your keys plus the readable app keys
const keys = captain.settings.keys();
Events (captain.events)
A simple event bus inside your plugin: emit reaches the listeners your own plugin registered, not other plugins. Captain’s Deck doesn’t send its own events (selection, navigation, theme changes) to plugins yet; to react to changes on disk, use captain.fs.watch.
// Listen for an event
function handler(data) {
console.log("Received:", data.foo);
}
captain.events.on("myPlugin.customEvent", handler);
// Emit an event to this plugin's listeners
captain.events.emit("myPlugin.customEvent", { foo: "bar" });
// Stop listening
captain.events.off("myPlugin.customEvent", handler);
Commands (captain.commands)
Register commands and run them by name. Since Captain’s Deck 1.5.5.1, a command that you also list under contributes.commands in the manifest appears in Quick Open (Cmd+P) as “Category: Title”. The category is optional and defaults to your plugin’s name. Commands you register without listing them stay internal to your plugin.
"contributes": {
"commands": [
{ "id": "myPlugin.sayHello", "title": "Say Hello", "category": "My Plugin" }
]
}
When the command is run from Quick Open, your handler gets the active pane’s selected files, or the file under the cursor, as an array in the same format as a context-menu handler. The array is empty when nothing is selected.
// Register a command
captain.commands.register("myPlugin.sayHello", (files) => {
captain.ui.notify({ title: "Hello!", message: "Command executed" });
});
// Execute one of this plugin's commands
captain.commands.execute("myPlugin.sayHello");
Network (captain.http)
Requires network permission.
// GET
const res = await captain.http.get("https://api.example.com/items");
// res = { ok, status, statusText, text, data } (data is the body as base64)
// POST a JSON string (sent with Content-Type: application/json)
await captain.http.post("https://api.example.com/items", JSON.stringify({ name: "test" }));
// Full control
await captain.http.fetch("https://api.example.com/items/1", {
method: "PUT",
headers: { "Authorization": "Bearer …" },
body: "…"
});
Shell (captain.shell)
Requires shell permission and “Allow plugins to execute shell commands” in Settings → Plugins. Commands run with /bin/zsh -c; quote file paths.
const result = await captain.shell.exec(`shasum -a 256 "${file.path}"`);
// result = { exitCode, stdout, stderr }
Status Bar (captain.statusBar)
Requires statusBar permission. Adds an item to the macOS menu bar; icon is an SF Symbol name.
const item = captain.statusBar.create({ title: "Toolkit", icon: "hammer" });
captain.statusBar.update(item, { title: "Toolkit (3)" });
// Run a function when the item is clicked (called with no arguments)
captain.statusBar.onClick(item, () => {
captain.ui.notify({ title: "Toolkit", message: "Clicked" });
});
captain.statusBar.remove(item);
onClick handlers are called from Captain’s Deck 1.5.4 on; earlier versions ignored clicks on plugin status bar items.
File Providers (captain.providers)
Requires providers permission. Registers a read-only location under your own URL scheme. Each handler receives a path and may return a value or a Promise.
captain.providers.register({
scheme: "notes",
name: "My Notes",
handlers: {
ls: (path) => [ { name: "todo.txt", path: "/todo.txt", isDirectory: false, size: 12 } ],
read: (path) => "…",
stat: (path) => ({ name: "todo.txt", path, isDirectory: false, size: 12 }),
exists: (path) => true
}
});
// Remove it again when you no longer need it (turning the plugin off does this for you)
captain.providers.unregister("notes");
Console
Standard console methods are available for debugging:
console.log("Info message");
console.warn("Warning message");
console.error("Error message");
Output goes to the app’s standard output, prefixed with [Plugin]. To see it, quit Captain’s Deck and start it from Terminal:
"/Applications/Captain's Deck.app/Contents/MacOS/Captain's Deck"
API Level 2 (Captain’s Deck 1.5.4)
Captain’s Deck 1.5.4 adds native file helpers, result windows and new when conditions (see Contributions). Keep "apiVersion": "1" in your manifest — that doesn’t change. Instead, check the API level at runtime so your plugin can tell people on older versions to update:
Feature detection
// 2 on Captain's Deck 1.5.4, 3 on 1.5.5 and later; undefined on older versions
captain.apiLevel
// The app's version string, e.g. "1.5.4" (also undefined before 1.5.4)
captain.appVersion
function supported() {
if (captain.apiLevel >= 2) return true;
captain.ui.notify({ title: "My Plugin", message: "This plugin needs Captain's Deck 1.5.4 or later." });
return false;
}
File helpers (captain.fs)
Require fileSystem.read. They run in the background, so the app stays responsive on big files and folders, and follow the same path rules as the rest of captain.fs (absolute paths, protected locations denied). Each returns a Promise that rejects with an error message if the path can’t be read.
// Checksum of a file as lowercase hex. The file is read in pieces,
// so multi-GB files are fine. Rejects for folders.
const hex = await captain.fs.hash("/path/to/file.dmg"); // SHA-256 by default
const md5 = await captain.fs.hash("/path/to/file.dmg", "md5");
// Algorithms: "sha256", "sha1", "md5", "sha384", "sha512"
// Total size of a file or folder tree (symlinks are not followed)
const usage = await captain.fs.du("/path/to/folder");
// usage = { bytes, files, folders }
// bytes: sum of file sizes; folders doesn't count the folder you passed in.
// For a single file: { bytes: size, files: 1, folders: 0 }
// Everything Spotlight knows about a file, plus photo EXIF/GPS data
const meta = await captain.fs.metadata("/path/to/photo.jpg");
// meta = { "PixelWidth": 4032, "ContentCreationDate": "2026-05-01T09:30:00Z",
// "Exif.FNumber": 1.8, "GPS.Latitude": 37.97, "TIFF.Model": "…", ... }
metadata keys are Spotlight attribute names without the kMDItem prefix (Authors, DurationSeconds, NumberOfPages…). For images, ImageIO properties are added with a prefix: Exif., GPS., TIFF., IPTC., and Image.PixelWidth / Image.PixelHeight / Image.Orientation / Image.ColorModel. Values are strings, numbers, booleans or arrays; dates are ISO 8601 strings. Files Spotlight hasn’t indexed return fewer keys, but photo EXIF/GPS data is read from the file itself. EXIF GPS coordinates are unsigned — check GPS.LatitudeRef ("S") and GPS.LongitudeRef ("W").
Result windows and confirmation (captain.ui)
Require dialogs. Both windows can be resized, and have Copy and Close buttons plus a save button. Always pass a title — it’s the window title, and the suggested file name when saving.
// A window of selectable, monospaced text, with Copy and Save… (.txt)
captain.ui.showReport({
title: "SHA-256 — 3 files",
text: lines.join("\n")
});
// A table, with Copy and Save CSV… (both export CSV with a header row)
captain.ui.showTable({
title: "Build clutter in my-project",
columns: ["Folder", "Size"],
rows: [["node_modules", "412 MB"], ["DerivedData", "1.2 GB"]],
summary: "2 folders · 1.6 GB" // optional; defaults to "N rows"
});
// Ask before doing something. Resolves to true (confirmed) or false (Cancel).
const ok = await captain.ui.confirm({
title: "Move 2 folders to the Trash?",
message: "They can be rebuilt by your tools.",
confirmTitle: "Move to Trash", // optional, default "OK"
destructive: true // optional: warning style, red button
});
showReport and showTable return nothing and open the window straight away; table cells are shown as text, and null shows as an empty cell. confirm always adds a Cancel button. Without the dialogs permission, confirm rejects and the two windows don’t open.
The gallery plugins built for API level 2 — Checksum & Verify, Bulk Metadata, Project Cleaner, Image Toolkit and Git Quick Actions — use these features, and their source is in the ZIPs.
API Level 3 (Captain’s Deck 1.5.5)
Captain’s Deck 1.5.5 raises captain.apiLevel to 3. It adds a safe place for tokens, a way to open your provider in a pane, and a few refinements. The manifest still says "apiVersion": "1"; check captain.apiLevel >= 3 before using these.
Secrets (captain.secrets)
Keep passwords and API tokens out of captain.storage: secrets go into the login Keychain, one item per key, under a service named after your plugin (com.captainsdeck.plugin.<your id>), so a plugin only ever sees its own. They’re deleted when the plugin is removed. No permission is needed.
// A string, or null when nothing is saved under that key
const token = captain.secrets.get("token");
// Save (true on success). Setting null or undefined deletes the key.
captain.secrets.set("token", value);
// Delete
captain.secrets.delete("token");
Asking for a token (showInputBox)
showInputBox takes two more options: message, a line of explanation under the title, and secure, which turns the field into a password field that hides what’s typed.
const token = await captain.ui.showInputBox({
title: "GitHub token",
message: "Paste a personal access token to see your private repositories.",
placeholder: "ghp_…",
secure: true
});
if (token) captain.secrets.set("token", token); // null when the user cancels
Opening your location in a pane (captain.providers.open)
A provider you registered can now be shown in the active pane. Pass your scheme and a path your ls handler understands; it returns false if the scheme isn’t one of yours.
captain.commands.register("notes.open", () => {
captain.providers.open("notes", "/");
});
People can also type notes:// or notes://some/path in Go to Path (Cmd+G). Plugin locations are read-only: files can be viewed with F3 or Quick Look, opened with Enter, and copied out with F5, while anything that would change them says the location is read-only. Going to a folder on disk from Go to Path returns the pane to your Mac.
Binary files from a provider
A provider’s read handler can return { base64: "…" } for binary data such as images. A plain string is always treated as text.
read: async (path) => {
const res = await captain.http.get(urlFor(path));
return isText(path) ? res.text : { base64: res.data }; // res.data is already base64
}
Turning a plugin off cleans up
When a plugin is turned off or removed, Captain’s Deck now stops its file watchers, removes its menu bar items and unregisters its providers for you.
GitHub Browser and Share as Gist in the gallery use all of these.
Examples
Context Menu: Open in VS Code
manifest.json{
"name": "Open in VS Code",
"id": "com.example.open-in-vscode",
"version": "1.0.0",
"apiVersion": "1",
"main": "main.js",
"permissions": ["contextMenu", "shell"],
"contributes": {
"contextMenus": [
{
"id": "vscode.openFile",
"title": "Open in VS Code",
"when": "selectedCount > 0"
}
]
}
}
main.js
exports.activate = function() {
captain.contextMenu.register("vscode.openFile", async (files) => {
for (const file of files) {
// Uses shell permission
await captain.shell.exec(`code "${file.path}"`);
}
});
};
File Processor: Markdown to HTML
manifest.json{
"name": "Markdown Converter",
"id": "com.example.markdown-converter",
"version": "1.0.0",
"apiVersion": "1",
"main": "main.js",
"permissions": ["contextMenu", "fileSystem.read", "fileSystem.write", "notifications"],
"contributes": {
"contextMenus": [
{
"id": "markdown.convert",
"title": "Convert to HTML",
"when": "isFile"
}
]
}
}
main.js
exports.activate = function() {
captain.contextMenu.register("markdown.convert", async (files) => {
for (const file of files) {
if (!file.path.endsWith('.md')) continue;
// Read markdown
const markdown = await captain.fs.readText(file.path);
// Simple conversion (use a real library in production)
const html = simpleMarkdownToHtml(markdown);
// Write HTML file
const htmlPath = file.path.replace(/\.md$/, '.html');
await captain.fs.write(htmlPath, html);
captain.ui.notify({
title: "Converted",
message: `Created ${htmlPath}`
});
}
});
};
function simpleMarkdownToHtml(md) {
return md
.replace(/^### (.*$)/gm, '<h3>$1</h3>')
.replace(/^## (.*$)/gm, '<h2>$1</h2>')
.replace(/^# (.*$)/gm, '<h1>$1</h1>')
.replace(/\*\*(.*)\*\*/gm, '<strong>$1</strong>')
.replace(/\*(.*)\*/gm, '<em>$1</em>')
.replace(/\n/gm, '<br>');
}
File Watcher: Notify on Change
Needs the contextMenu, fileSystem.watch and notifications permissions, and a context-menu item with the id watcher.watch.
const watches = {};
exports.activate = function() {
captain.contextMenu.register("watcher.watch", (files) => {
for (const file of files) {
if (watches[file.path]) continue;
watches[file.path] = captain.fs.watch(file.path, (event) => {
captain.ui.notify({
title: "File changed",
message: `${event.type}: ${event.path}`
});
});
}
});
};
// Turning the plugin off stops these watchers for you (1.5.5+)
Showcase Plugins
These official plugins demonstrate what's possible with the Captain's Deck plugin API. Download them to see real-world examples or use them as starting points for your own plugins.
For examples of the API level 2 features, see the plugin gallery: Checksum & Verify, Bulk Metadata, Project Cleaner, Image Toolkit and Git Quick Actions (Captain’s Deck 1.5.4 or later). GitHub Browser and Share as Gist show the API level 3 features (1.5.5 or later).
Folder Stats
Instantly analyze any folder's contents with a single right-click. Folder Stats recursively scans directories to show:
- Total size of the folder
- File and folder counts
- Breakdown by file type
- The 10 largest files
Results are automatically copied to clipboard as a formatted report.
Captain's Toolkit
A power-user's Swiss army knife demonstrating the full plugin API. Features include:
- File analysis and metadata inspection
- Checksum calculation (MD5, SHA-1, SHA-256)
- Real-time file change monitoring
- Quick shell command execution
Adds a menu-bar item that counts active watches. Right-click any file and choose Toolkit: Open Menu for the rest of the tools. It asks for many permissions, so read the approval list before enabling it.
Best Practices
Performance
- Batch operations - Process multiple files in one go
- Cache results - Use
captain.storageto cache expensive computations
Security
- Request minimal permissions - Only ask for what you need
- Validate input - Never trust file contents blindly
- Handle errors - Wrap async code in try/catch
exports.activate = function() {
captain.contextMenu.register("myPlugin.action", async (files) => {
try {
for (const file of files) {
await processFile(file);
}
} catch (error) {
console.error("Plugin error:", error);
captain.ui.notify({
title: "Error",
message: error.message
});
}
});
};
User Experience
- Provide feedback - Use notifications for long operations
- Respect preferences - Check user settings when available
- Fail gracefully - Show helpful error messages
Publishing
Plugin Package Structure
Distribution
Distribute as a ZIP archive containing the plugin folder. Users install with Settings → Plugins → Install Plugin… and pick the ZIP — Captain’s Deck expands it into the Plugins folder, validates the manifest, and lists the plugin in Settings. The user enables it with the row’s checkbox.
For development, you can also work directly inside ~/Library/Application Support/CaptainsDeck/Plugins/your-plugin/ — click Reload Plugins in Settings → Plugins to pick up new plugins and code changes.
Version Guidelines
- Use Semantic Versioning
- Increment MAJOR for breaking changes
- Increment MINOR for new features
- Increment PATCH for bug fixes
Troubleshooting
Plugin Not Loading
- Check manifest.json is valid JSON
- Verify the
iduses reverse domain notation with only lowercase letters, digits and hyphens (e.g.,com.example.my-plugin; capitals or underscores make the plugin fail to load) - Ensure
mainpoints to an existing .js file - Run the app from Terminal and check the
[Plugin]messages (see Console)
Permission Denied
- Verify the permission is declared in
manifest.jsonunderpermissions. - For
shell: also enable “Allow plugins to execute shell commands” in Settings → Plugins. Without that global toggle, even a properly-declaredcaptain.shell.execcall rejects. - For file system:
fileSystem.readandfileSystem.writeare separate. Reading a file with onlywritedeclared still fails. - “Access denied: path is restricted” means the path is relative or points into a protected location (see File System). Use absolute paths.
API Not Working
- Check you have the required permission
- Ensure you're using
awaitfor async methods - Run the app from Terminal and check for
[Plugin ERROR]messages
Support
Plugin API level 3 - Captain's Deck 1.5.5+ (manifest apiVersion stays "1")