gpc-plugin-development
Build and publish GPC plugins using the @gpc-cli/plugin-sdk.
When to use
- Building a new GPC plugin
- Adding custom hooks (notifications, logging, metrics)
- Registering custom CLI commands
- Understanding the plugin lifecycle and permission system
- Debugging plugin loading or hook execution
- Publishing a plugin to npm
Inputs required
- Node.js 20+ and TypeScript 5+
- @gpc-cli/plugin-sdk package (peer dependency)
- Plugin name —
@gpc-cli/plugin-*(first-party) orgpc-plugin-*(third-party)
Procedure
0. Scaffold a new plugin
# Generate plugin boilerplate
gpc plugins init my-notifier --description "Send Slack notifications on release"
# This creates:
# gpc-plugin-my-notifier/
# ├── package.json
# ├── tsconfig.json
# ├── src/index.ts
# └── tests/plugin.test.ts
Or manually:
mkdir gpc-plugin-my-notifier && cd gpc-plugin-my-notifier
npm init -y
npm install --save-peer @gpc-cli/plugin-sdk
npm install --save-dev typescript vitest
1. Implement the plugin interface
Every plugin exports a GpcPlugin object:
import type { GpcPlugin, PluginHooks } from "@gpc-cli/plugin-sdk";
export const plugin: GpcPlugin = {
name: "gpc-plugin-my-notifier",
version: "1.0.0",
register(hooks: PluginHooks) {
// Register your hooks here
hooks.afterCommand(async (event, result) => {
if (event.command === "releases upload" && result.success) {
console.log(`✓ Upload complete in ${result.durationMs}ms`);
}
});
},
};
export default plugin;
Read: references/hooks-reference.md for all 6 hook types with full type signatures.
2. Available lifecycle hooks
Register hooks inside the register() method:
register(hooks: PluginHooks) {
// Before any command runs
hooks.beforeCommand(async (event) => {
console.log(`Running: gpc ${event.command}`);
});
// After successful command
hooks.afterCommand(async (event, result) => {
console.log(`Done: ${result.durationMs}ms, exit ${result.exitCode}`);
});
// On command failure
hooks.onError(async (event, error) => {
console.error(`Failed: ${error.code} — ${error.message}`);
});
// Before each API request
hooks.beforeRequest(async (event) => {
console.log(`API: ${event.method} ${event.path}`);
});
// After each API response
hooks.afterResponse(async (event, response) => {
console.log(`API: ${response.status} in ${response.durationMs}ms`);
});
// Register custom CLI commands
hooks.registerCommands((registry) => {
registry.add({
name: "notify",
description: "Send a test notification",
action: async () => {
console.log("Notification sent!");
},
});
});
}
3. Declare permissions (third-party plugins)
Third-party plugins (gpc-plugin-*) must declare permissions:
{
"gpc": {
"permissions": [
"hooks:afterCommand",
"hooks:onError",
"api:read"
]
}
}
Read: references/permissions-system.md for the full permission list and trust model.
Available permissions:
| Permission | Allows |
|---|---|
read:config |
Read .gpcrc.json |
write:config |
Modify config |
read:auth |
Access credentials |
api:read |
Make read API calls |
api:write |
Make write API calls |
commands:register |
Register new commands |
hooks:beforeCommand |
Hook before commands |
hooks:afterCommand |
Hook after commands |
hooks:onError |
Hook on errors |
hooks:beforeRequest |
Hook before API requests |
hooks:afterResponse |
Hook after API responses |
Only @gpc-cli/plugin-ci is treated as first party, and only when the installed package's own package.json name matches the specifier. Everything else needs an explicit gpc.permissions declaration.
Trust is no longer granted by name (v0.9.94+). GPC previously trusted any specifier starting with
@gpc-cli/, without checking that the resolved package was actually first party — so an npm alias or local replacement pointing at that name inherited unrestricted access. Trust is now read from the resolved package manifest and verified beforeimport(), so a mismatch is refused without running the plugin's module code (PLUGIN_IDENTITY_MISMATCH).@gpc-cli/plugin-sdkwas also removed from the first-party list; it is a library, not a plugin.Declaring permissions is now mandatory (v0.9.94+, breaking). A third-party plugin with no
gpc.permissionsin itspackage.jsonis refused withPLUGIN_PERMISSIONS_REQUIRED(exit 10) instead of receiving broad compatibility permissions. Approvals recorded before this change are grandfathered once, with a deprecation warning telling the author what to add. Approvals stored as relative paths must be approved again, because the old format did not record which project they belonged to — re-rungpc plugins approve ./path/to/pluginfrom the project directory.Loose plugin files are identified by path (v0.9.94+). A plugin loaded from a bare file with no
package.jsonbeside it used to report the name of whichever project enclosed it. It now lists under its own file path, matching what the approval record stores. A package sitting beside the module, or one that declaresgpc.permissions, still reports its own name.
Trust check order (v0.9.74+):
discoverPlugins()callsisPluginTrusted()before callingimport()on any plugin specifier. Untrusted plugins are silently skipped without their module code ever running. Previously, GPC imported first and checked approval afterward, which allowed top-level module side-effects to execute before the trust decision was made.
Permission enforcement (v0.9.80+): Permissions are now enforced at hook registration time, not just validated. A third-party plugin without
hooks:beforeRequestpermission that callshooks.beforeRequest()will see a warning instead of the hook being silently registered. Ifregister()throws, the error is caught and the plugin is skipped with a warning -- it cannot crash the CLI. Project.gpcrc.jsoncan no longer setapprovedPlugins-- only user config (~/.config/gpc/config.json) is trusted for plugin approval.
4. Test your plugin
// tests/plugin.test.ts
import { describe, it, expect, vi } from "vitest";
import { plugin } from "../src/index.js";
describe("my-notifier plugin", () => {
it("has required fields", () => {
expect(plugin.name).toBe("gpc-plugin-my-notifier");
expect(plugin.version).toBeDefined();
expect(typeof plugin.register).toBe("function");
});
it("registers afterCommand hook", () => {
const hooks = {
beforeCommand: vi.fn(),
afterCommand: vi.fn(),
onError: vi.fn(),
beforeRequest: vi.fn(),
afterResponse: vi.fn(),
registerCommands: vi.fn(),
};
plugin.register(hooks);
expect(hooks.afterCommand).toHaveBeenCalled();
});
});
npx vitest run
5. Install and configure
# Install locally
npm install ./gpc-plugin-my-notifier
# Or from npm
npm install -g @gpc-cli/cli-plugin-my-notifier
Add to .gpcrc.json:
{
"plugins": ["gpc-plugin-my-notifier"],
"approvedPlugins": ["gpc-plugin-my-notifier"]
}
Third-party plugins must be listed in approvedPlugins to load.
6. Publish to npm
# Build
npx tsc
# Test
npx vitest run
# Publish
npm publish
Naming convention:
- First-party:
@gpc-cli/plugin-<name>(reserved for official plugins) - Third-party:
gpc-plugin-<name>
Verification
gpc plugins listshows your plugin as loaded- Hooks fire at the expected lifecycle points
npx vitest runpasses all tests- Third-party permission errors show clear messages
- Plugin loads without blocking GPC startup
Failure modes / debugging
| Symptom | Likely Cause | Fix |
|---|---|---|
| Plugin not loading | Not in plugins config array |
Add to .gpcrc.json plugins list |
PLUGIN_INVALID_PERMISSION |
Unknown permission declared | Check valid permissions in references/permissions-system.md |
| Third-party plugin silently missing | Not in approvedPlugins |
Add plugin name to approvedPlugins in config — unapproved plugins are skipped without error |
| Hook not firing | Wrong hook name or not registered | Verify hook registration in register() method |
| Hook error crashes GPC | Error in beforeCommand handler |
onError and API hooks swallow errors; beforeCommand does not |
| Plugin not found | Wrong package name or not installed | Check node_modules for gpc-plugin-* or @gpc-cli/plugin-* |
| Standalone binary ignores plugins | Plugins disabled in binary mode | Use npm-installed GPC for plugin support |
gpc doctor warns on plugin |
Plugin fails to load | Run gpc doctor to see which plugin failed, then reinstall it (v0.9.71+) |
Related skills
- gpc-ci-integration — uses @gpc-cli/plugin-ci as an example of a first-party plugin
- gpc-setup — configuration file where plugins are registered
- gpc-troubleshooting — debugging plugin loading issues