Claude Code Plugin Architecture and Extension Development

Claude Code provides a flexible plugin architecture that enables developers to extend its functionality through hooks, MCP servers, and custom slash commands. This guide covers the complete architecture and practical patterns for building extensions.

Architecture Overview

Claude Code’s extensibility is built on three core systems:

  1. Hook System - Event-driven extension points for injecting custom logic
  2. MCP (Managed Code Process) System - Integration framework for external tools and services
  3. Custom Slash Commands - User-defined commands stored as Markdown templates

Hook System

Available Hooks

Hooks allow you to intercept and modify Claude Code’s behavior at specific points:

UserPromptSubmit

Triggered when a user submits a prompt. Perfect for:

  • Input validation and sanitization
  • Context enrichment
  • Custom preprocessing
interface UserPromptSubmitHook {
  event: "UserPromptSubmit";
  data: {
    prompt: string;
    cwd: string;
  };
}

PreToolUse

Validates and controls tool execution before Claude Code uses them:

  • Security checks
  • Rate limiting
  • Tool parameter validation
interface PreToolUseHook {
  event: "PreToolUse";
  data: {
    tool: string;
    parameters: Record<string, any>;
  };
}

PreCompact

Manages conversation cleanup when context limits approach:

  • Custom compaction strategies
  • Context preservation logic
  • Memory optimization

Stop/SubagentStop

Handles process completion and transcript processing:

  • Result processing
  • Cleanup operations
  • Analytics collection

Implementing Custom Hooks

Here’s a practical example of implementing a security validation hook:

// security-hook.ts
import { Hook, HookResult } from '@claude-code/hooks';
 
export class SecurityValidationHook implements Hook {
  async handle(event: HookEvent): Promise<HookResult> {
    if (event.type === 'PreToolUse') {
      const { tool, parameters } = event.data;
      
      // Validate dangerous operations
      if (tool === 'Bash' && parameters.command.includes('rm -rf')) {
        return {
          action: 'block',
          message: 'Dangerous command detected and blocked'
        };
      }
      
      // Log all tool usage for audit
      await this.logToolUsage(tool, parameters);
    }
    
    return { action: 'continue' };
  }
  
  private async logToolUsage(tool: string, params: any) {
    // Implementation for logging
  }
}

MCP (Managed Code Process) System

MCP Architecture

MCP servers enable integration with external services through standardized protocols:

graph LR
    A[Claude Code] -->|stdio/HTTP/SSE| B[MCP Server]
    B --> C[External Service]
    B --> D[Custom Tools]
    B --> E[Database]

Transport Protocols

  1. stdio - Direct process communication
  2. HTTP - RESTful API integration
  3. SSE (Server-Sent Events) - Real-time streaming

Creating an MCP Server

Here’s a complete example of an MCP server for database operations:

// database-mcp-server.ts
import { MCPServer, Tool, Transport } from '@claude-code/mcp';
 
class DatabaseMCPServer extends MCPServer {
  private db: DatabaseConnection;
  
  constructor() {
    super({
      name: 'database-tools',
      version: '1.0.0',
      transport: Transport.STDIO
    });
    
    this.registerTools();
  }
  
  private registerTools() {
    this.addTool({
      name: 'query_database',
      description: 'Execute a database query',
      parameters: {
        query: { type: 'string', required: true },
        database: { type: 'string', default: 'main' }
      },
      handler: this.queryDatabase.bind(this)
    });
    
    this.addTool({
      name: 'schema_info',
      description: 'Get database schema information',
      parameters: {
        table: { type: 'string' }
      },
      handler: this.getSchemaInfo.bind(this)
    });
  }
  
  async queryDatabase(params: any) {
    // Validate and sanitize query
    const sanitized = this.sanitizeQuery(params.query);
    const results = await this.db.execute(sanitized);
    
    return {
      success: true,
      rows: results.rows,
      count: results.count
    };
  }
  
  async getSchemaInfo(params: any) {
    // Implementation
  }
}
 
// Start the server
const server = new DatabaseMCPServer();
server.start();

MCP Configuration

Configure MCP servers in your project:

// .mcp.json
{
  "servers": {
    "database": {
      "command": "node",
      "args": ["./mcp-servers/database-mcp-server.js"],
      "transport": "stdio",
      "scope": "project"
    },
    "github": {
      "url": "https://api.mycompany.com/mcp/github",
      "transport": "http",
      "auth": {
        "type": "oauth",
        "provider": "github"
      },
      "scope": "user"
    }
  }
}

Scopes

  • project: Shared within repository
  • local: Local development only
  • user: Global user configuration

Custom Slash Commands

Creating Commands

Place Markdown files in .claude/commands/:

<!-- .claude/commands/generate-component.md -->
---
title: Generate React Component
description: Creates a new React component with TypeScript
args:
  name: Component name
  type: Component type (functional/class)
---
 
Generate a new React component named {{name}} of type {{type}}.
 
Requirements:
- Use TypeScript
- Include proper props interface
- Add JSDoc comments
- Follow project conventions in src/components/
 
```bash
# Check existing component patterns
ls -la src/components/

@src/components/Button.tsx - Use this as a reference


### Advanced Command Features

#### Namespacing
Create subdirectories for command organization:

.claude/commands/ ├── frontend/ │ ├── component.md # /frontend:component │ └── hook.md # /frontend:hook └── backend/ ├── api.md # /backend:api └── migration.md # /backend:migration


#### Dynamic Arguments

```markdown
---
args:
  table: Database table name
  fields: Comma-separated field names
---

Create a CRUD API for {{table}} with fields: {{fields}}

Best Practices

1. Hook Development

  • Keep hooks lightweight - Don’t block the main thread
  • Handle errors gracefully - Always return a valid response
  • Use async operations - Leverage promises for I/O
  • Implement timeout handling - Prevent hanging operations

2. MCP Server Guidelines

  • Validate all inputs - Never trust external data
  • Implement rate limiting - Protect against abuse
  • Use connection pooling - Optimize resource usage
  • Provide clear error messages - Help with debugging

3. Command Design

  • Make commands discoverable - Use clear names and descriptions
  • Provide examples - Show expected usage
  • Handle edge cases - Validate arguments
  • Follow conventions - Match project patterns

Security Considerations

Input Validation

Always validate and sanitize inputs:

function validateInput(input: string): string {
  // Remove potential command injection
  const sanitized = input.replace(/[;&|`$]/g, '');
  
  // Validate against whitelist
  if (!ALLOWED_PATTERNS.test(sanitized)) {
    throw new Error('Invalid input pattern');
  }
  
  return sanitized;
}

Permission Management

Implement proper permission checks:

class PermissionHook implements Hook {
  async handle(event: HookEvent): Promise<HookResult> {
    if (event.type === 'PreToolUse') {
      const hasPermission = await this.checkPermission(
        event.user,
        event.data.tool
      );
      
      if (!hasPermission) {
        return {
          action: 'block',
          message: 'Insufficient permissions'
        };
      }
    }
    
    return { action: 'continue' };
  }
}

Performance Optimization

Caching Strategies

Implement intelligent caching for MCP servers:

class CachedMCPServer extends MCPServer {
  private cache = new LRUCache<string, any>({
    max: 1000,
    ttl: 1000 * 60 * 5 // 5 minutes
  });
  
  async handleRequest(request: MCPRequest) {
    const cacheKey = this.getCacheKey(request);
    
    // Check cache first
    const cached = this.cache.get(cacheKey);
    if (cached) {
      return cached;
    }
    
    // Process request
    const result = await this.processRequest(request);
    
    // Cache result
    this.cache.set(cacheKey, result);
    
    return result;
  }
}

Connection Pooling

Optimize external service connections:

class ConnectionPool {
  private pool: Connection[] = [];
  private maxConnections = 10;
  
  async getConnection(): Promise<Connection> {
    // Reuse existing connection
    const available = this.pool.find(c => !c.inUse);
    if (available) {
      available.inUse = true;
      return available;
    }
    
    // Create new if under limit
    if (this.pool.length < this.maxConnections) {
      const conn = await this.createConnection();
      this.pool.push(conn);
      return conn;
    }
    
    // Wait for available connection
    return this.waitForConnection();
  }
}

Testing Extensions

Unit Testing Hooks

import { describe, it, expect } from 'bun:test';
import { SecurityHook } from './security-hook';
 
describe('SecurityHook', () => {
  it('should block dangerous commands', async () => {
    const hook = new SecurityHook();
    const result = await hook.handle({
      type: 'PreToolUse',
      data: {
        tool: 'Bash',
        parameters: { command: 'rm -rf /' }
      }
    });
    
    expect(result.action).toBe('block');
  });
});

Integration Testing MCP Servers

describe('DatabaseMCPServer', () => {
  let server: DatabaseMCPServer;
  
  beforeEach(() => {
    server = new DatabaseMCPServer();
    server.start();
  });
  
  it('should execute queries', async () => {
    const response = await server.call('query_database', {
      query: 'SELECT * FROM users LIMIT 10'
    });
    
    expect(response.success).toBe(true);
    expect(response.rows).toHaveLength(10);
  });
});

Real-World Examples

GitHub Integration

Example from claude-issue-triage.yml:

- name: Run Claude Code with GitHub MCP
  uses: anthropics/claude-code-action@v1
  with:
    mcp-servers: |
      github:
        url: https://api.github.com/mcp
        transport: http
        auth:
          type: oauth
          token: ${{ secrets.GITHUB_TOKEN }}

Custom Analysis Hook

class CodeAnalysisHook implements Hook {
  async handle(event: HookEvent): Promise<HookResult> {
    if (event.type === 'PreToolUse' && event.data.tool === 'Edit') {
      // Analyze code changes
      const analysis = await this.analyzeCode(
        event.data.parameters.file_path,
        event.data.parameters.new_string
      );
      
      // Add analysis to context
      return {
        action: 'continue',
        context: {
          codeAnalysis: analysis
        }
      };
    }
    
    return { action: 'continue' };
  }
}

Resources

Next Steps

  1. Start with a simple hook implementation
  2. Build an MCP server for your most common tasks
  3. Create custom slash commands for your workflow
  4. Share your extensions with the community

The plugin architecture provides powerful extension points while maintaining security and performance. By following these patterns, you can create robust extensions that enhance Claude Code for your specific needs.