
Gpc Plugin Development
- 21 installs
- 1 repo stars
- Updated August 1, 2026
- yasserstudio/gpc-skills
Helps with ai & agent building tasks.
About
gpc-plugin-development is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- gpc-plugin-development
- AI & Agent Building
- AI-coding skill
Gpc Plugin Development by the numbers
- 21 all-time installs (skills.sh)
- Ranked #10,307 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/yasserstudio/gpc-skills --skill gpc-plugin-developmentAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 21 |
|---|---|
| repo stars | ★ 1 |
| Last updated | August 1, 2026 |
| Repository | yasserstudio/gpc-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
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.tsOr 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 vitest1. 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 |
First-party plugins (@gpc-cli/*) are auto-trusted — no permissions needed.
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 withouthooks: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 run5. Install and configure
# Install locally
npm install ./gpc-plugin-my-notifier
# Or from npm
npm install -g @gpc-cli/cli-plugin-my-notifierAdd 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 publishNaming 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
{
"skill_name": "gpc-plugin-development",
"evals": [
{
"id": 1,
"prompt": "I want to build a GPC plugin that sends a Slack message whenever a release upload succeeds or fails. It should include the track name, duration, and error details if it failed. How do I get started?",
"expected_output": "Scaffolds a plugin with afterCommand and onError hooks that send Slack notifications",
"files": [],
"expectations": [
"Shows gpc plugins init or manual setup with @gpc-cli/plugin-sdk",
"Implements afterCommand hook to send success notifications",
"Implements onError hook to send failure notifications with error.code and error.message",
"Accesses event.command and result.durationMs for the message",
"Shows the package.json permissions declaration for a third-party plugin"
]
},
{
"id": 2,
"prompt": "I built a plugin but it's not loading. I added it to the plugins array in .gpcrc.json but gpc plugins list doesn't show it. The plugin is called gpc-plugin-metrics and it's installed in node_modules. What's wrong?",
"expected_output": "Diagnoses the missing approvedPlugins entry for third-party plugins",
"files": [],
"expectations": [
"Identifies that third-party plugins need to be in approvedPlugins config",
"Shows the correct .gpcrc.json with both plugins and approvedPlugins arrays",
"Mentions the @gpc-cli/* vs gpc-plugin-* trust model difference",
"Suggests checking that the plugin exports a valid GpcPlugin object",
"Mentions that standalone binary mode disables plugins"
]
},
{
"id": 3,
"prompt": "I want to add a custom gpc command called 'gpc deploy' that uploads to internal, waits, checks vitals, and then promotes to beta automatically. Can I do this with a plugin?",
"expected_output": "Shows how to use registerCommands hook to add a custom command",
"files": [],
"expectations": [
"Uses hooks.registerCommands with registry.add to create the command",
"Shows the command action function with the deploy workflow logic",
"Includes commands:register in the permissions declaration",
"Shows the plugin interface with name, version, and register method",
"Mentions that custom commands are registered during plugin load"
]
}
]
}
Plugin Hooks Reference
Complete type signatures and behavior for all 6 GPC plugin hooks.
Hook execution order
Command invoked
│
├─ beforeCommand(event)
│
├─ For each API call:
│ ├─ beforeRequest(requestEvent)
│ ├─ [HTTP request]
│ └─ afterResponse(requestEvent, responseEvent)
│
├─ On success: afterCommand(event, result)
│ On failure: onError(event, error)
│
└─ DoneHooks run sequentially in registration order across plugins.
1. beforeCommand
Runs before any GPC command executes.
hooks.beforeCommand(async (event: CommandEvent) => {
// event.command — "releases upload", "vitals crashes", etc.
// event.args — resolved arguments { track: "beta", app: "com.example.app" }
// event.app — package name (if available)
// event.startedAt — Date when command started
});Behavior: Errors in this hook propagate and can prevent the command from running.
Use cases: Validation, prerequisite checks, command logging.
2. afterCommand
Runs after a command completes successfully.
hooks.afterCommand(async (event: CommandEvent, result: CommandResult) => {
// result.success — always true in this hook
// result.data — command output data (varies by command)
// result.durationMs — execution time in milliseconds
// result.exitCode — 0 for success
});Behavior: Errors in this hook are logged but don't change the command's exit code.
Use cases: Notifications (Slack, email), metrics, audit logging, CI step summaries.
3. onError
Runs when a command fails.
hooks.onError(async (event: CommandEvent, error: PluginError) => {
// error.code — "API_FORBIDDEN", "AUTH_FAILED", etc.
// error.message — human-readable error description
// error.exitCode — 1-10 (see exit code reference)
// error.cause — original Error object (if available)
});Behavior: Errors in this handler are swallowed to prevent cascading failures.
Use cases: Error reporting, alerting, logging failures to external systems.
4. beforeRequest
Runs before each HTTP request to the Google Play API.
hooks.beforeRequest(async (event: RequestEvent) => {
// event.method — "GET", "POST", "PUT", "PATCH", "DELETE"
// event.path — API path (relative to base URL)
// event.startedAt — Date
});Behavior: Errors are swallowed — never blocks API calls.
Use cases: Request tracing, custom headers, API call logging.
5. afterResponse
Runs after each HTTP response from the Google Play API.
hooks.afterResponse(async (event: RequestEvent, response: ResponseEvent) => {
// response.status — HTTP status code (200, 403, 404, etc.)
// response.durationMs — request duration
// response.ok — true if 2xx status
});Behavior: Errors are swallowed.
Use cases: Performance monitoring, rate-limit tracking, API metrics.
6. registerCommands
Register custom CLI commands from your plugin.
hooks.registerCommands((registry: CommandRegistry) => {
registry.add({
name: "my-command",
description: "Does something custom",
options: [
{ flags: "--format <type>", description: "Output format" },
],
action: async (args) => {
console.log("Running custom command with:", args);
},
});
});Behavior: Commands are registered synchronously during plugin load.
Use cases: Domain-specific tools, workflow shortcuts, report generators.
Error handling summary
| Hook | Error behavior |
|---|---|
beforeCommand | Propagates — can block the command |
afterCommand | Logged, swallowed |
onError | Swallowed (prevents cascading) |
beforeRequest | Swallowed (never blocks API calls) |
afterResponse | Swallowed |
registerCommands | Propagates during load |
Example: Slack notification plugin
import type { GpcPlugin } from "@gpc-cli/plugin-sdk";
export const plugin: GpcPlugin = {
name: "gpc-plugin-slack",
version: "1.0.0",
register(hooks) {
hooks.afterCommand(async (event, result) => {
if (event.command.startsWith("releases")) {
await fetch(process.env.SLACK_WEBHOOK!, {
method: "POST",
body: JSON.stringify({
text: `✓ gpc ${event.command} completed in ${result.durationMs}ms`,
}),
});
}
});
hooks.onError(async (event, error) => {
await fetch(process.env.SLACK_WEBHOOK!, {
method: "POST",
body: JSON.stringify({
text: `✗ gpc ${event.command} failed: ${error.code} — ${error.message}`,
}),
});
});
},
};Plugin Permission System
How GPC manages plugin trust and permissions.
Trust model
First-party plugins (@gpc-cli/*)
- Auto-trusted — no permission validation
- Loaded automatically if installed
- Full access to all hooks and APIs
- Example:
@gpc-cli/plugin-ci
Third-party plugins (gpc-plugin-*)
- Must declare permissions in
package.json - Must be listed in
approvedPluginsconfig - Permission violations throw
PLUGIN_INVALID_PERMISSION(exit code 10) - Users must explicitly approve each plugin
Declaring permissions
In your plugin's package.json:
{
"name": "gpc-plugin-slack-notifier",
"version": "1.0.0",
"gpc": {
"permissions": [
"hooks:afterCommand",
"hooks:onError"
]
}
}All permissions
| Permission | Description | Required for |
|---|---|---|
read:config | Read .gpcrc.json values | Accessing user config |
write:config | Modify .gpcrc.json | Changing config programmatically |
read:auth | Access credentials | Custom API calls with user's auth |
api:read | Make read API calls | Fetching data from Google Play |
api:write | Make write API calls | Uploading, modifying releases |
commands:register | Register CLI commands | Adding custom commands |
hooks:beforeCommand | Hook before commands | Pre-command validation |
hooks:afterCommand | Hook after commands | Post-command notifications |
hooks:onError | Hook on errors | Error reporting |
hooks:beforeRequest | Hook before API requests | Request inspection |
hooks:afterResponse | Hook after API responses | Response monitoring |
Approving third-party plugins
In .gpcrc.json:
{
"plugins": [
"@gpc-cli/plugin-ci",
"gpc-plugin-slack-notifier"
],
"approvedPlugins": [
"gpc-plugin-slack-notifier"
]
}plugins— list of plugins to loadapprovedPlugins— third-party plugins the user has explicitly approved- First-party plugins don't need to be in
approvedPlugins
Permission validation flow
The trust check runs before import() is called on any plugin specifier. This prevents untrusted top-level module code from executing during discovery.
discoverPlugins() resolves specifier
│
├─ Is @gpc-cli/* prefix?
│ └─ Yes → isPluginTrusted() = true → import() → Auto-trusted, skip permission validation
│
├─ Is in approvedPlugins?
│ └─ No → isPluginTrusted() = false → Skip (silent, no import())
│
└─ Yes → isPluginTrusted() = true → import()
│
├─ Has gpc.permissions in package.json?
│ └─ No → Reject with PLUGIN_INVALID_PERMISSION
│
├─ All permissions recognized?
│ └─ No → Reject with PLUGIN_INVALID_PERMISSION
│
└─ Enforce permissionsSecurity note: Prior to v0.9.74, GPC imported the plugin module first and checked approval afterward. This allowed untrusted top-level module code (side effects onimport()) to run during discovery -- an RCE risk. The new model gatesimport()behindisPluginTrusted(), so unapproved plugins never execute any code.
Common permission patterns
Notification plugin (read-only)
{
"gpc": {
"permissions": ["hooks:afterCommand", "hooks:onError"]
}
}Metrics plugin (observability)
{
"gpc": {
"permissions": [
"hooks:beforeCommand",
"hooks:afterCommand",
"hooks:onError",
"hooks:beforeRequest",
"hooks:afterResponse"
]
}
}Custom command plugin
{
"gpc": {
"permissions": [
"commands:register",
"api:read",
"read:config"
]
}
}Full-access plugin
{
"gpc": {
"permissions": [
"read:config", "write:config",
"read:auth",
"api:read", "api:write",
"commands:register",
"hooks:beforeCommand", "hooks:afterCommand", "hooks:onError",
"hooks:beforeRequest", "hooks:afterResponse"
]
}
}Standalone binary
Plugins are disabled when running GPC as a standalone binary (__GPC_BINARY=1). Use the npm-installed version for plugin support.
#!/usr/bin/env node
/**
* Detection script for GPC CLI.
* Returns JSON with installation status, version, auth state, and config.
* Used by Claude Code skill system for deterministic environment detection.
*
* Exit codes:
* 0 — GPC detected (may or may not be authenticated)
* 1 — GPC not found
*/
import { execSync } from "node:child_process";
import { existsSync } from "node:fs";
import { join } from "node:path";
function run(cmd) {
try {
return execSync(cmd, { encoding: "utf-8", timeout: 10000 }).trim();
} catch {
return null;
}
}
const result = {
installed: false,
version: null,
installMethod: null,
authStatus: null,
authMethod: null,
profile: null,
envAuth: false,
defaultApp: null,
configFile: null,
nodeVersion: process.version,
};
// Check if gpc is installed globally
const versionOutput = run("gpc --version");
if (!versionOutput) {
// Try npx
const npxVersion = run("npx gpc --version 2>/dev/null");
if (!npxVersion) {
console.log(JSON.stringify(result, null, 2));
process.exit(1);
}
result.version = npxVersion;
result.installed = true;
result.installMethod = "npx";
} else {
result.version = versionOutput;
result.installed = true;
result.installMethod = "global";
}
// Check auth status
const authOutput = run("gpc auth status --json 2>/dev/null");
if (authOutput) {
try {
const auth = JSON.parse(authOutput);
result.authStatus = auth.status || "unknown";
result.authMethod = auth.method || null;
result.profile = auth.profile || null;
} catch {
result.authStatus = "parse_error";
}
}
// Check for env-based auth
if (process.env.GPC_SERVICE_ACCOUNT) {
result.envAuth = true;
}
// Check default app
const configOutput = run("gpc config get app --json 2>/dev/null");
if (configOutput) {
try {
const config = JSON.parse(configOutput);
result.defaultApp = config.value || config.app || null;
} catch {
result.defaultApp = configOutput || null;
}
}
// Check for .gpcrc.json in current directory
const rcPath = join(process.cwd(), ".gpcrc.json");
if (existsSync(rcPath)) {
result.configFile = rcPath;
}
console.log(JSON.stringify(result, null, 2));
process.exit(0);