Files
2025-11-30 08:39:56 +08:00

17 KiB

Claude Code Plugin Development Best Practices

This guide covers best practices for developing high-quality, maintainable, and secure Claude Code plugins.

Code Quality Standards

TypeScript Best Practices

Configuration

{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "noImplicitReturns": true,
    "noImplicitThis": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true
  }
}

Interface Design

// Use clear interfaces
interface PluginConfig {
  name: string;
  version: string;
  settings: PluginSettings;
}

interface PluginSettings {
  enabled: boolean;
  autoUpdate: boolean;
  customOptions: Record<string, unknown>;
}

// Use generic types for flexibility
class PluginManager<T extends PluginConfig> {
  private plugins: Map<string, T> = new Map();

  register(plugin: T): void {
    this.plugins.set(plugin.name, plugin);
  }

  get<K extends keyof T>(name: string, key: K): T[K] | undefined {
    const plugin = this.plugins.get(name);
    return plugin?.[key];
  }
}

Error Handling

// Implement proper error handling
class PluginError extends Error {
  constructor(
    message: string,
    public readonly code: string,
    public readonly pluginName: string
  ) {
    super(message);
    this.name = 'PluginError';
  }
}

// Use async/await for asynchronous operations
async function loadPlugin(pluginPath: string): Promise<Plugin> {
  try {
    const manifest = await loadManifest(pluginPath);
    const plugin = await import(pluginPath);
    return new plugin.default(manifest);
  } catch (error) {
    throw new PluginError(`Failed to load plugin from ${pluginPath}`, 'LOAD_ERROR', pluginPath);
  }
}

Code Organization

Directory Structure

src/
├── types/
│   ├── plugin.ts
│   ├── command.ts
│   └── skill.ts
├── core/
│   ├── plugin-manager.ts
│   ├── command-registry.ts
│   └── skill-loader.ts
├── utils/
│   ├── file-utils.ts
│   ├── validation.ts
│   └── logger.ts
├── commands/
│   ├── base-command.ts
│   └── implementations/
├── skills/
│   ├── base-skill.ts
│   └── implementations/
└── index.ts

Naming Conventions

// Use descriptive names
class PluginConfigurationValidator {} // Good
class PCV {} // Bad

// Use consistent patterns
interface PluginManifest {} // Interface
class PluginLoader {} // Class
const DEFAULT_PLUGIN_PATH = '/plugins'; // Constant
function validatePluginManifest() {} // Function

// File naming
plugin - manager.ts; // kebab-case for files
PluginManager; // PascalCase for classes
validatePlugin(); // camelCase for functions

Performance Optimization

Lazy Loading

class PluginRegistry {
  private plugins = new Map<string, () => Promise<Plugin>>();
  private loadedPlugins = new Map<string, Plugin>();

  async get(name: string): Promise<Plugin> {
    // Check if already loaded
    if (this.loadedPlugins.has(name)) {
      return this.loadedPlugins.get(name)!;
    }

    // Load plugin on demand
    const loader = this.plugins.get(name);
    if (!loader) {
      throw new Error(`Plugin not found: ${name}`);
    }

    const plugin = await loader();
    this.loadedPlugins.set(name, plugin);
    return plugin;
  }
}

Caching Strategies

interface CacheEntry<T> {
  value: T;
  timestamp: number;
  ttl: number;
}

class Cache<T> {
  private cache = new Map<string, CacheEntry<T>>();

  set(key: string, value: T, ttl: number = 300000): void {
    // 5 minutes default
    this.cache.set(key, {
      value,
      timestamp: Date.now(),
      ttl,
    });
  }

  get(key: string): T | undefined {
    const entry = this.cache.get(key);
    if (!entry) {
      return undefined;
    }

    if (Date.now() - entry.timestamp > entry.ttl) {
      this.cache.delete(key);
      return undefined;
    }

    return entry.value;
  }

  // Cleanup expired entries
  cleanup(): void {
    const now = Date.now();
    for (const [key, entry] of this.cache.entries()) {
      if (now - entry.timestamp > entry.ttl) {
        this.cache.delete(key);
      }
    }
  }
}

Resource Management

class ResourceManager {
  private resources = new Set<() => Promise<void>>();

  register(cleanup: () => Promise<void>): void {
    this.resources.add(cleanup);
  }

  async cleanup(): Promise<void> {
    const cleanupPromises = Array.from(this.resources).map(async cleanup => {
      try {
        await cleanup();
      } catch (error) {
        console.error('Cleanup error:', error);
      }
    });

    await Promise.allSettled(cleanupPromises);
    this.resources.clear();
  }
}

// Usage with automatic cleanup
class Plugin {
  private resourceManager = new ResourceManager();

  async initialize(): Promise<void> {
    // Register cleanup functions
    this.resourceManager.register(async () => {
      await this.closeConnections();
    });

    this.resourceManager.register(async () => {
      await this.cleanupTempFiles();
    });
  }

  async destroy(): Promise<void> {
    await this.resourceManager.cleanup();
  }
}

Security Considerations

Input Validation

import Joi from 'joi';

const pluginConfigSchema = Joi.object({
  name: Joi.string().alphanum().min(1).max(50).required(),
  version: Joi.string()
    .pattern(/^\d+\.\d+\.\d+$/)
    .required(),
  description: Joi.string().max(500).optional(),
  permissions: Joi.array().items(Joi.string()).optional(),
});

class PluginValidator {
  static validateConfig(config: unknown): PluginConfig {
    const { error, value } = pluginConfigSchema.validate(config);
    if (error) {
      throw new ValidationError(`Invalid plugin configuration: ${error.message}`);
    }
    return value;
  }

  static sanitizeInput(input: string): string {
    return input
      .replace(/[<>]/g, '') // Remove HTML tags
      .replace(/javascript:/gi, '') // Remove javascript protocol
      .trim()
      .substring(0, 1000); // Limit length
  }
}

Permission Management

enum Permission {
  FILE_READ = 'file:read',
  FILE_WRITE = 'file:write',
  NETWORK_REQUEST = 'network:request',
  SYSTEM_EXEC = 'system:exec',
  ENV_READ = 'env:read',
}

class PermissionManager {
  private permissions = new Set<Permission>();

  constructor(permissions: Permission[]) {
    this.permissions = new Set(permissions);
  }

  has(permission: Permission): boolean {
    return this.permissions.has(permission);
  }

  require(permission: Permission): void {
    if (!this.has(permission)) {
      throw new SecurityError(`Permission required: ${permission}`);
    }
  }

  checkFileAccess(path: string, mode: 'read' | 'write'): void {
    const permission = mode === 'read' ? Permission.FILE_READ : Permission.FILE_WRITE;
    this.require(permission);

    // Additional path validation
    if (path.includes('..')) {
      throw new SecurityError('Path traversal detected');
    }

    if (path.startsWith('/etc/') || path.startsWith('/sys/')) {
      throw new SecurityError('Access to system directories denied');
    }
  }
}

Secure Plugin Execution

interface SecureExecutionContext {
  permissions: Permission[];
  timeout: number;
  memoryLimit: number;
}

class SecurePluginRunner {
  async executePlugin(plugin: Plugin, context: SecureExecutionContext): Promise<unknown> {
    const permissionManager = new PermissionManager(context.permissions);
    const monitor = new ResourceMonitor(context.memoryLimit);

    try {
      // Set up timeout
      const timeoutPromise = new Promise((_, reject) => {
        setTimeout(() => reject(new Error('Plugin execution timeout')), context.timeout);
      });

      // Execute plugin with monitoring
      const executionPromise = this.executeWithMonitoring(plugin, permissionManager, monitor);

      const result = await Promise.race([executionPromise, timeoutPromise]);
      return result;
    } finally {
      monitor.stop();
    }
  }
}

Error Handling and Logging

Structured Error Handling

abstract class PluginError extends Error {
  abstract readonly code: string;
  abstract readonly category: ErrorCategory;

  constructor(
    message: string,
    public readonly context?: Record<string, unknown>
  ) {
    super(message);
    this.name = this.constructor.name;
  }

  toJSON(): ErrorRecord {
    return {
      name: this.name,
      message: this.message,
      code: this.code,
      category: this.category,
      context: this.context,
      stack: this.stack,
      timestamp: new Date().toISOString(),
    };
  }
}

enum ErrorCategory {
  CONFIGURATION = 'configuration',
  EXECUTION = 'execution',
  VALIDATION = 'validation',
  NETWORK = 'network',
  FILESYSTEM = 'filesystem',
  SECURITY = 'security',
}

Comprehensive Logging

interface LogEntry {
  level: LogLevel;
  message: string;
  timestamp: string;
  context?: Record<string, unknown>;
  error?: ErrorRecord;
  plugin?: string;
  operation?: string;
}

class Logger {
  private transports: LogTransport[] = [];

  constructor(private minLevel: LogLevel = LogLevel.INFO) {}

  addTransport(transport: LogTransport): void {
    this.transports.push(transport);
  }

  debug(message: string, context?: Record<string, unknown>): void {
    this.log(LogLevel.DEBUG, message, context);
  }

  info(message: string, context?: Record<string, unknown>): void {
    this.log(LogLevel.INFO, message, context);
  }

  warn(message: string, context?: Record<string, unknown>): void {
    this.log(LogLevel.WARN, message, context);
  }

  error(message: string, error?: Error, context?: Record<string, unknown>): void {
    const errorRecord = error
      ? {
          name: error.name,
          message: error.message,
          stack: error.stack,
        }
      : undefined;

    this.log(LogLevel.ERROR, message, context, errorRecord);
  }

  private log(
    level: LogLevel,
    message: string,
    context?: Record<string, unknown>,
    error?: ErrorRecord
  ): void {
    if (level < this.minLevel) {
      return;
    }

    const entry: LogEntry = {
      level,
      message,
      timestamp: new Date().toISOString(),
      context,
      error,
      plugin: context?.plugin as string,
      operation: context?.operation as string,
    };

    this.transports.forEach(transport => {
      try {
        transport.log(entry);
      } catch (transportError) {
        console.error('Transport error:', transportError);
      }
    });
  }
}

Testing Strategies

Unit Testing

import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
import { PluginManager } from '../src/plugin-manager';
import { MockPlugin } from './mocks/mock-plugin';

describe('PluginManager', () => {
  let pluginManager: PluginManager;

  beforeEach(() => {
    pluginManager = new PluginManager();
  });

  afterEach(() => {
    pluginManager.cleanup();
  });

  describe('registerPlugin', () => {
    it('should register a valid plugin', () => {
      const plugin = new MockPlugin('test-plugin', '1.0.0');

      expect(() => pluginManager.register(plugin)).not.toThrow();
      expect(pluginManager.isRegistered('test-plugin')).toBe(true);
    });

    it('should reject plugin with invalid name', () => {
      const plugin = new MockPlugin('', '1.0.0');

      expect(() => pluginManager.register(plugin)).toThrow(
        'Plugin name must be a non-empty string'
      );
    });
  });
});

Integration Testing

describe('Plugin Integration', () => {
  let client: ClaudeCodeClient;
  let server: TestServer;

  beforeAll(async () => {
    server = new TestServer();
    await server.start();

    client = new ClaudeCodeClient({
      endpoint: server.getUrl(),
      timeout: 5000,
    });
  });

  afterAll(async () => {
    await server.stop();
  });

  it('should install and execute plugin end-to-end', async () => {
    // Install plugin
    const installResult = await client.installPlugin({
      name: 'integration-test-plugin',
      version: '1.0.0',
      source: './test-fixtures/integration-plugin',
    });

    expect(installResult.success).toBe(true);

    // Execute command
    const commandResult = await client.executeCommand('/integration-test', {
      input: 'test data',
    });

    expect(commandResult.success).toBe(true);
    expect(commandResult.output).toContain('processed: test data');
  });
});

Performance Testing

describe('Performance Tests', () => {
  it('should handle high load without memory leaks', async () => {
    const monitor = new PerformanceMonitor();
    const plugin = new TestPlugin();

    const initialMemory = process.memoryUsage().heapUsed;
    const iterations = 1000;

    for (let i = 0; i < iterations; i++) {
      await monitor.measure(() => plugin.process(`test-data-${i}`), 'process-operation');
    }

    const finalMemory = process.memoryUsage().heapUsed;
    const memoryIncrease = finalMemory - initialMemory;

    // Memory increase should be reasonable (less than 10MB)
    expect(memoryIncrease).toBeLessThan(10 * 1024 * 1024);
  });
});

Documentation Standards

Code Documentation

/**
 * Plugin manager for handling plugin lifecycle and execution.
 *
 * This class provides a centralized way to manage Claude Code plugins,
 * including registration, execution, and cleanup operations.
 *
 * @example
 * ```typescript
 * const manager = new PluginManager();
 * const plugin = new MyPlugin();
 *
 * manager.register(plugin);
 * const result = await manager.executeCommand('my-command', { param: 'value' });
 * ```
 */
export class PluginManager {
  /**
   * Registers a plugin with the manager.
   *
   * @param plugin - The plugin to register
   * @throws {ValidationError} If plugin validation fails
   * @throws {DuplicateError} If a plugin with the same name is already registered
   */
  register(plugin: Plugin): void {
    this.validatePlugin(plugin);
    this.checkDuplicate(plugin.name);
    this.plugins.set(plugin.name, plugin);
  }
}

README Template

# Plugin Name

> Brief description of what the plugin does

## Features

- Feature 1
- Feature 2
- Feature 3

## Installation

```bash
claude marketplace install plugin-name
```

Usage

Basic Usage

/command-name --param=value

Configuration

Add to your .claude/settings.json:

{
  "plugins": {
    "plugin-name": {
      "setting1": "value1",
      "setting2": "value2"
    }
  }
}

Development

Building

npm run build

Testing

npm test

License

License information.


## Version Management

### Semantic Versioning
```typescript
class VersionManager {
  static parseVersion(version: string): Version {
    const match = version.match(/^(\d+)\.(\d+)\.(\d+)(?:-(.+))?$/);
    if (!match) {
      throw new Error(`Invalid version format: ${version}`);
    }

    return {
      major: parseInt(match[1], 10),
      minor: parseInt(match[2], 10),
      patch: parseInt(match[3], 10),
      prerelease: match[4] || null,
    };
  }

  static compareVersions(v1: string, v2: string): number {
    const version1 = this.parseVersion(v1);
    const version2 = this.parseVersion(v2);

    if (version1.major !== version2.major) {
      return version1.major - version2.major;
    }
    if (version1.minor !== version2.minor) {
      return version1.minor - version2.minor;
    }
    if (version1.patch !== version2.patch) {
      return version1.patch - version2.patch;
    }

    return 0;
  }
}

Design Patterns

Plugin Factory Pattern

abstract class PluginFactory {
  abstract create(config: PluginConfig): Plugin;

  static register(type: string, factory: PluginFactory): void {
    this.factories.set(type, factory);
  }

  static create(type: string, config: PluginConfig): Plugin {
    const factory = this.factories.get(type);
    if (!factory) {
      throw new Error(`Unknown plugin type: ${type}`);
    }
    return factory.create(config);
  }

  private static factories = new Map<string, PluginFactory>();
}

Observer Pattern for Plugin Events

class PluginEventEmitter {
  private handlers = new Map<string, PluginEventHandler[]>();

  on(eventType: string, handler: PluginEventHandler): void {
    if (!this.handlers.has(eventType)) {
      this.handlers.set(eventType, []);
    }
    this.handlers.get(eventType)!.push(handler);
  }

  emit(event: PluginEvent): void {
    const handlers = this.handlers.get(event.type);
    if (handlers) {
      handlers.forEach(handler => {
        try {
          handler(event);
        } catch (error) {
          console.error(`Error in event handler for ${event.type}:`, error);
        }
      });
    }
  }
}

Quality Assurance Checklist

Before Release

  • Code follows all style guidelines
  • All tests pass successfully
  • Documentation is complete and accurate
  • Security review passed
  • Performance benchmarks met
  • Plugin tested in multiple environments
  • Error handling comprehensive
  • Dependencies validated

Code Review

  • Functions have clear single responsibilities
  • Error handling is comprehensive
  • Logging is appropriate and informative
  • Tests cover edge cases
  • Security considerations are addressed
  • Performance implications are considered

Following these best practices will help ensure your Claude Code plugins are high-quality, secure, and well-maintained.