Plugin Development
Extend GPC with custom commands, lifecycle hooks, and integrations -- without forking the core. Plugins can react to command execution, register new CLI commands, and observe GPC API request attempts.
Agent Skills
GPC also ships agent skills that teach AI coding assistants (Claude Code, Cursor, Copilot, Windsurf, and others) how to use GPC. These are not plugins — they're structured guides that help AI assistants run the right GPC commands for your task.
gpc install-skillsThe interactive wizard lets you pick from 16 available skills covering setup, releases, metadata, CI/CD, monetization, and more. See the Agent Skills page for the full list.
Getting Started
Build and install your first GPC plugin in four steps.
Step 1: Scaffold
Use the built-in generator to create a plugin project:
gpc plugins init my-pluginThis creates a my-plugin/ directory with a package.json, tsconfig.json, src/index.ts skeleton, and a basic test file.
Step 2: Implement
Edit my-plugin/src/index.ts to add a hook. Here is a minimal beforeCommand hook that logs a message before every command:
import { definePlugin } from "@gpc-cli/plugin-sdk";
export const plugin = definePlugin({
name: "gpc-plugin-my-plugin",
version: "0.1.0",
register(hooks) {
hooks.beforeCommand(async (event) => {
console.log(`Hello from my-plugin! Running: gpc ${event.command}`);
});
},
});2
3
4
5
6
7
8
9
10
11
12
Step 3: Install Locally
Build the plugin and link it so GPC can discover it:
cd my-plugin
npm install
npm run build
npm link2
3
4
Then approve it for use (required for third-party plugins):
gpc plugins approve gpc-plugin-my-pluginStep 4: Verify
Run any GPC command and confirm your hook fires:
gpc apps list
# Output includes: Hello from my-plugin! Running: gpc apps list2
Check that your plugin appears in the loaded plugins list:
gpc plugins listPublishing
When your plugin is ready to share, publish it to npm. Follow the gpc-plugin-* naming convention, and have users add the package name to their plugins config. Declare required permissions in your package.json under the gpc key (see Permissions below).
Plugin Interface
Every GPC plugin implements the GpcPlugin interface:
interface GpcPlugin {
/** Unique plugin name (e.g., "@gpc-cli/plugin-ci" or "gpc-plugin-slack") */
name: string;
/** Plugin version (semver) */
version: string;
/** Called once when the plugin is loaded. Register hooks here. */
register(hooks: PluginHooks): void | Promise<void>;
}2
3
4
5
6
7
8
9
10
Lifecycle Hooks
Six hooks are available. Register them in the register() method.
interface PluginHooks {
/** Run before a command executes */
beforeCommand(handler: BeforeCommandHandler): void;
/** Run after a command completes successfully */
afterCommand(handler: AfterCommandHandler): void;
/** Run when a command fails with an error */
onError(handler: ErrorHandler): void;
/** Register additional CLI commands from the plugin */
registerCommands(handler: CommandRegistrar): void;
/** Run before an @gpc-cli/api request attempt is sent */
beforeRequest(handler: BeforeRequestHandler): void;
/** Run after an @gpc-cli/api request attempt completes */
afterResponse(handler: AfterResponseHandler): void;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Handler Signatures
type BeforeCommandHandler = (ctx: CommandEvent) => void | Promise<void>;
type AfterCommandHandler = (ctx: CommandEvent, result: CommandResult) => void | Promise<void>;
type ErrorHandler = (ctx: CommandEvent, error: PluginError) => void | Promise<void>;
type CommandRegistrar = (registry: CommandRegistry) => void;
type BeforeRequestHandler = (event: RequestEvent) => void | Promise<void>;
type AfterResponseHandler = (event: RequestEvent, response: ResponseEvent) => void | Promise<void>;2
3
4
5
6
Event Types
CommandEvent
Passed to beforeCommand, afterCommand, and onError handlers.
interface CommandEvent {
command: string; // e.g., "releases upload"
args: Record<string, unknown>; // Resolved options + positional arguments; credentials are redacted
app?: string; // Package name (if available)
startedAt: Date; // When the command started
}2
3
4
5
6
CommandResult
Passed to afterCommand handlers.
interface CommandResult {
success: boolean;
data?: unknown;
durationMs: number;
exitCode: number;
}2
3
4
5
6
PluginError
Passed to onError handlers.
interface PluginError {
code: string;
message: string;
exitCode: number;
cause?: Error;
}2
3
4
5
6
RequestEvent
Passed to beforeRequest handlers.
interface RequestEvent {
method: string;
path: string;
startedAt: Date;
}2
3
4
5
ResponseEvent
Passed to afterResponse handlers.
interface ResponseEvent {
status: number; // HTTP status, or 0 when transport fails before a response
durationMs: number;
ok: boolean;
}2
3
4
5
Command Registration
Plugins can add new CLI commands through the registerCommands hook.
interface CommandRegistry {
add(definition: PluginCommand): void;
}
interface PluginCommand {
name: string;
description: string;
options?: PluginCommandOption[];
arguments?: PluginCommandArgument[];
action: (args: Record<string, unknown>, options: Record<string, unknown>) => void | Promise<void>;
}
interface PluginCommandOption {
flags: string;
description: string;
defaultValue?: unknown;
sensitive?: boolean; // Redact the value from lifecycle and webhook metadata
}
interface PluginCommandArgument {
name: string;
description: string;
required?: boolean;
sensitive?: boolean; // Redact the value from lifecycle and webhook metadata
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Example: Adding a Custom Command
hooks.registerCommands((registry) => {
registry.add({
name: "notify",
description: "Send release notification to Slack",
options: [
{ flags: "--channel <channel>", description: "Slack channel" },
{ flags: "--message <message>", description: "Custom message" },
{ flags: "--api-key <key>", description: "Integration API key", sensitive: true },
],
action: async (args, options) => {
const channel = options.channel as string;
const message = (options.message as string) || "New release published";
await sendSlackMessage(channel, message);
},
});
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Registered commands appear under gpc <command-name> and show up in gpc --help. Mark every credential-bearing option or positional argument as sensitive: true. GPC then replaces its value in command lifecycle events and webhook command metadata, including parse-failure paths.
Permissions
Third-party plugins must declare required permissions in package.json under gpc.permissions.
type PluginPermission =
| "read:config" // Reserved for a future managed capability
| "write:config" // Reserved for a future managed capability
| "read:auth" // Reserved for a future managed capability
| "api:read" // Reserved for a future managed capability
| "api:write" // Reserved for a future managed capability
| "commands:register" // Register new CLI commands
| "hooks:beforeCommand" // Hook into pre-command execution
| "hooks:afterCommand" // Hook into post-command execution
| "hooks:onError" // Hook into error handling
| "hooks:beforeRequest" // Hook into pre-transport-attempt
| "hooks:afterResponse"; // Hook into post-transport-attempt2
3
4
5
6
7
8
9
10
11
12
Trust Model
| Plugin Type | Name Pattern | Trust Level | Permission Check |
|---|---|---|---|
| First-party | Explicit GPC allowlist | Trusted after exact package-manifest identity verification | No permission checks |
| Third-party | gpc-plugin-* | Untrusted | Validated against manifest |
Third-party plugins already approved before permission metadata was introduced continue to load with broad hook and command permissions. The CLI emits a deprecation warning for this compatibility path. New approvals require an explicit permission list, and new plugins should declare the smallest set they need.
When PluginManager is used directly with an explicit untrusted manifest, a missing permission list produces PLUGIN_PERMISSIONS_REQUIRED. Unknown permission names produce PLUGIN_INVALID_PERMISSION. Both errors use exit code 10. During normal CLI discovery, an invalid declaration is rejected before the plugin module is imported, and a plugin configuration problem cannot block unrelated commands.
Manifest Declaration
{
"name": "gpc-plugin-my-plugin",
"version": "0.1.0",
"gpc": {
"permissions": ["hooks:beforeCommand", "hooks:afterCommand"]
}
}2
3
4
5
6
7
GPC reads this metadata from the package that supplied the configured module. First-party trust is limited to GPC's explicit package allowlist (currently @gpc-cli/plugin-ci) and requires a matching package manifest name, so namespace lookalikes, exported-name spoofing, and npm aliases cannot impersonate a first-party plugin.
Plugin permissions control registration of GPC-managed hooks and commands. They do not sandbox JavaScript or restrict ordinary Node.js APIs. Approval is therefore a code-trust decision: only approve packages whose source and publisher you trust.
Plugin Discovery
Plugins are loaded from the explicit plugins list in GPC configuration. Third-party entries must also be approved before their module code is imported.
Config File
{
"plugins": ["@gpc-cli/plugin-ci", "gpc-plugin-slack", "./plugins/custom.js"]
}2
3
Relative paths resolve from the directory containing the discovered project config (or an explicit cwd supplied to the Core discovery API), so invoking GPC from a nested directory does not change plugin identity. Without a project config, the current directory is used. GPC does not scan node_modules automatically.
During the one-time permission-policy migration, old relative-path approvals are removed because the earlier format did not record which project they belonged to. Reapprove the local path from its intended project; GPC then stores an absolute file identity. Package-name and already-absolute approvals are migrated automatically.
Module Resolution
Plugins are loaded via dynamic import(). The resolver checks for:
- Default export implementing
GpcPlugin - Named
pluginexport implementingGpcPlugin - Module itself -- duck-typed check for
name,version,register
PluginManager
The PluginManager class in @gpc-cli/core orchestrates the full plugin lifecycle.
class PluginManager {
load(plugin: GpcPlugin, manifest?: PluginManifest): Promise<void>;
runBeforeCommand(event: CommandEvent): Promise<void>;
runAfterCommand(event: CommandEvent, result: CommandResult): Promise<void>;
runOnError(event: CommandEvent, error: PluginError): Promise<void>;
runBeforeRequest(event: RequestEvent): Promise<void>;
runAfterResponse(event: RequestEvent, response: ResponseEvent): Promise<void>;
hasRequestHooks(): boolean;
getRegisteredCommands(): PluginCommand[];
getLoadedPlugins(): LoadedPlugin[];
reset(): void;
}2
3
4
5
6
7
8
9
10
11
12
Key behaviors:
- Completion, error, and request observer failures are swallowed to prevent cascading failures
- Hooks run sequentially in registration order
reset()clears all state (used in tests)
@gpc-cli/plugin-ci
The built-in CI/CD plugin. Detects CI environments and writes GitHub Actions step summaries.
CI Detection
| Provider | Detection | Build ID | Branch | Step Summary |
|---|---|---|---|---|
| GitHub Actions | GITHUB_ACTIONS=true | GITHUB_RUN_ID | GITHUB_REF_NAME | Yes |
| GitLab CI | GITLAB_CI=true | CI_JOB_ID | CI_COMMIT_BRANCH | No |
| Jenkins | JENKINS_URL set | BUILD_NUMBER | BRANCH_NAME | No |
| CircleCI | CIRCLECI=true | CIRCLE_BUILD_NUM | CIRCLE_BRANCH | No |
| Bitrise | BITRISE_IO=true | BITRISE_BUILD_NUMBER | BITRISE_GIT_BRANCH | No |
| Generic | CI=true | -- | -- | No |
GitHub Actions Step Summary
When running in GitHub Actions with $GITHUB_STEP_SUMMARY available, the plugin:
- Writes a markdown table after each command (app, duration, exit code)
- Writes error details on command failure (error code, message)
Example: Slack Notification Plugin
A complete example plugin that sends Slack notifications on release commands.
// gpc-plugin-slack/src/index.ts
import { definePlugin } from "@gpc-cli/plugin-sdk";
export const plugin = definePlugin({
name: "gpc-plugin-slack",
version: "1.0.0",
register(hooks) {
// Notify on successful releases
hooks.afterCommand(async (event, result) => {
if (!event.command.startsWith("releases") || !result.success) {
return;
}
const webhook = process.env.SLACK_WEBHOOK_URL;
if (!webhook) {
return;
}
await fetch(webhook, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
text: `Released ${event.app} via \`gpc ${event.command}\` (${result.durationMs}ms)`,
}),
});
});
// Alert on errors
hooks.onError(async (event, error) => {
const webhook = process.env.SLACK_WEBHOOK_URL;
if (!webhook) {
return;
}
await fetch(webhook, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
text: `GPC error in \`gpc ${event.command}\`: ${error.code} - ${error.message}`,
}),
});
});
},
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
Manifest for the Slack Plugin
{
"name": "gpc-plugin-slack",
"version": "1.0.0",
"gpc": {
"permissions": ["hooks:afterCommand", "hooks:onError"]
}
}2
3
4
5
6
7
Example: Audit Log Plugin
A plugin that logs all command executions to a file.
import { definePlugin } from "@gpc-cli/plugin-sdk";
import { appendFileSync } from "node:fs";
export const plugin = definePlugin({
name: "gpc-plugin-audit",
version: "1.0.0",
register(hooks) {
const logFile = process.env.GPC_AUDIT_LOG || "gpc-audit.jsonl";
hooks.afterCommand(async (event, result) => {
const entry = {
timestamp: new Date().toISOString(),
command: event.command,
app: event.app,
success: result.success,
exitCode: result.exitCode,
durationMs: result.durationMs,
};
appendFileSync(logFile, JSON.stringify(entry) + "\n");
});
hooks.onError(async (event, error) => {
const entry = {
timestamp: new Date().toISOString(),
command: event.command,
app: event.app,
error: error.code,
message: error.message,
};
appendFileSync(logFile, JSON.stringify(entry) + "\n");
});
},
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
Plugin SDK Exports
Everything you need to build a plugin is exported from @gpc-cli/plugin-sdk:
// Core interfaces
export type { GpcPlugin, PluginHooks, PluginManifest, PluginPermission };
// Hook handler types
export type { BeforeCommandHandler, AfterCommandHandler, ErrorHandler, CommandRegistrar };
export type { BeforeRequestHandler, AfterResponseHandler };
// Event types
export type { CommandEvent, CommandResult, PluginError, RequestEvent, ResponseEvent };
// Command types
export type { CommandRegistry, PluginCommand, PluginCommandOption, PluginCommandArgument };
// Helpers
export { definePlugin }; // Type-safe plugin factory2
3
4
5
6
7
8
9
10
11
12
13
14
15
Scaffolding a New Plugin
Use the built-in generator to create a plugin project:
gpc plugins init my-pluginThis creates a directory with:
package.jsonwith@gpc-cli/plugin-sdkpeer dependencytsconfig.jsonconfigured for ESMsrc/index.tswith a plugin skeleton usingdefinePlugin()- Basic test file
Plugin CLI Commands
gpc plugins list # Show loaded plugins and their status
gpc plugins init <name> # Scaffold a new plugin project
gpc plugins approve <name> # Approve a third-party plugin (first-run prompt)
gpc plugins revoke <name> # Revoke plugin approval2
3
4
