# How to Extend the Secure-Design Tool System with Custom Tools

> Learn how to extend the secure-design tool system with custom tools. Implement Zod validation, secure filesystem operations, and register your new tool effectively.

- Repository: [Harold Martin/secure-design](https://github.com/hbmartin/secure-design)
- Tags: 
- Published: 2026-03-03

---

**Developers can extend the secure-design tool system by implementing a factory function that uses Zod schemas for input validation, wraps filesystem operations with security utilities from [`src/tools/tool-utils.ts`](https://github.com/hbmartin/secure-design/blob/main/src/tools/tool-utils.ts), and registers the tool in [`src/services/customAgentService.ts`](https://github.com/hbmartin/secure-design/blob/main/src/services/customAgentService.ts).**

The hbmartin/secure-design repository provides a modular AI agent framework for VS Code that enables secure workspace interactions through standardized tools. When you need to extend secure-design with custom tools, the codebase provides established patterns that enforce workspace containment, type safety via Zod, and consistent error handling through centralized utilities.

## Core Architecture Components

The secure-design tool architecture rests on four pillars that every custom tool must implement. According to the source code, these components work together to guarantee security and consistency:

- **ExecutionContext** – Defined in [`src/types/agent.ts`](https://github.com/hbmartin/secure-design/blob/main/src/types/agent.ts), this provides the `workingDirectory` and logger instance required by every tool operation.
- **Tool Utilities** – The [`src/tools/tool-utils.ts`](https://github.com/hbmartin/secure-design/blob/main/src/tools/tool-utils.ts) file exports `validateWorkspacePath`, `resolveWorkspacePath`, `createSuccessResponse`, and `handleToolError` (see lines 49‑55 for validation logic).
- **Zod Schemas** – Each tool declares its input parameters using Zod for runtime validation and automatic documentation generation.
- **AI SDK Integration** – Tools are constructed using the `tool({ description, inputSchema, execute })` factory from the `ai` SDK, which the AI agent uses to discover and invoke capabilities.

## Implementing a Custom Tool: The Copy File Example

To extend secure-design with a practical file operation, follow the pattern established in [`src/tools/write-tool.ts`](https://github.com/hbmartin/secure-design/blob/main/src/tools/write-tool.ts) and [`src/tools/read-tool.ts`](https://github.com/hbmartin/secure-design/blob/main/src/tools/read-tool.ts). Below is a complete implementation of a **copy-file tool** that demonstrates every required step.

### 1. Define the Zod Input Schema

Create a new file at [`src/tools/copy-tool.ts`](https://github.com/hbmartin/secure-design/blob/main/src/tools/copy-tool.ts) and declare the tool’s contract using Zod. This ensures the AI agent passes valid arguments before execution begins.

```typescript
import { z } from 'zod';

const copyToolSchema = z.object({
    source_path: z
        .string()
        .describe('Path of the file to copy (relative to workspace or absolute).'),
    destination_path: z
        .string()
        .describe('Target path for the copy (relative to workspace or absolute).'),
    overwrite: z
        .boolean()
        .optional()
        .default(false)
        .describe('Overwrite the destination if it already exists.'),
});

```

### 2. Implement the Tool Factory Function

Export a factory function following the `createCopyTool(context: ExecutionContext)` pattern used by built-in tools. This function returns the configured tool instance with security checks and logging.

```typescript
import { tool } from 'ai';
import * as fs from 'fs';
import * as path from 'path';
import type { ExecutionContext } from '../types/agent';
import {
    handleToolError,
    validateWorkspacePath,
    resolveWorkspacePath,
    createSuccessResponse,
    type ToolResponse,
} from './tool-utils';
import { getLogger } from 'react-vscode-webview-ipc/host';

export function createCopyTool(context: ExecutionContext) {
    const logger = getLogger('copy tool');

    return tool({
        description: 'Copy a file within the workspace, optionally overwriting the destination.',
        inputSchema: copyToolSchema,
        execute: async ({
            source_path,
            destination_path,
            overwrite = false,
        }): Promise<ToolResponse> => {
            const start = Date.now();

            try {
                // Validate both paths against workspace boundaries
                const srcErr = validateWorkspacePath(source_path, context);
                if (srcErr) return srcErr;
                const dstErr = validateWorkspacePath(destination_path, context);
                if (dstErr) return dstErr;

                const srcAbs = resolveWorkspacePath(source_path, context);
                const dstAbs = resolveWorkspacePath(destination_path, context);

                logger.info(`[copy] ${source_path} → ${destination_path}`);

                // Verify source exists
                if (!fs.existsSync(srcAbs) || !fs.statSync(srcAbs).isFile()) {
                    return handleToolError(
                        `Source file not found: ${source_path}`,
                        'Copy tool',
                        'file_not_found',
                    );
                }

                // Check destination collision
                if (fs.existsSync(dstAbs) && !overwrite) {
                    return handleToolError(
                        `Destination already exists (set overwrite=true to replace): ${destination_path}`,
                        'Copy tool',
                        'validation',
                    );
                }

                // Ensure parent directories exist
                const parentDir = path.dirname(dstAbs);
                if (!fs.existsSync(parentDir)) {
                    fs.mkdirSync(parentDir, { recursive: true });
                    logger.info(`[copy] Created parent directories for ${destination_path}`);
                }

                // Execute the copy operation
                fs.copyFileSync(srcAbs, dstAbs);

                const duration = Date.now() - start;
                logger.info(`[copy] Completed in ${duration}ms`);

                return createSuccessResponse({
                    source_path,
                    destination_path,
                    overwritten: overwrite && fs.existsSync(dstAbs),
                    bytes_copied: fs.statSync(srcAbs).size,
                    duration_ms: duration,
                });
            } catch (error) {
                return handleToolError(error, 'Copy tool execution', 'execution');
            }
        },
    });
}

```

### 3. Register the Tool in the Agent Service

All tool factories are instantiated in [`src/services/customAgentService.ts`](https://github.com/hbmartin/secure-design/blob/main/src/services/customAgentService.ts). Import your factory and add it to the tools array to make it available to the AI agent:

```typescript
import { createCopyTool } from '../tools/copy-tool';
// ...
this.tools = [
    // existing tools...
    createCopyTool(this.context),
];

```

## Security Requirements and Best Practices

When you extend secure-design with custom tools, you must adhere to the following security protocol enforced by the existing codebase:

- **Path Validation** – Always call `validateWorkspacePath(filePath, context)` (as implemented in [`src/tools/tool-utils.ts`](https://github.com/hbmartin/secure-design/blob/main/src/tools/tool-utils.ts) lines 49‑55) before any filesystem operation to prevent directory traversal attacks.
- **Absolute Resolution** – Use `resolveWorkspacePath(filePath, context)` to convert relative inputs into absolute paths anchored to the workspace root.
- **Error Handling** – Wrap the entire `execute` body in a `try / catch` block and return `handleToolError(error, 'Tool Name', 'execution')` for unexpected exceptions.
- **Response Consistency** – Return `createSuccessResponse({ ... })` with typed metadata to match the `ToolSuccessResponse` shape defined in [`tool-utils.ts`](https://github.com/hbmartin/secure-design/blob/main/tool-utils.ts) lines 24‑27.
- **Logging** – Initialize a logger using `getLogger('tool name')` from `react-vscode-webview-ipc/host` and emit info-level logs for operation start and completion.
- **Size Limits** – For tools handling large payloads, enforce maximum size constraints (e.g., 10 MiB) as demonstrated in [`src/tools/read-tool.ts`](https://github.com/hbmartin/secure-design/blob/main/src/tools/read-tool.ts) lines 73‑80.

## Testing Your Custom Tool

Validate your implementation using the extension’s testing infrastructure:

1. **Unit Tests** – Create test files under `src/test/tools/` using the VS Code extension test harness to verify schema validation and error paths.
2. **Integration Tests** – Run `npm run test:tools` to execute the suite in a headless VS Code instance, ensuring your tool integrates with the agent service.
3. **Manual Verification** – Open the extension’s Chat panel and invoke your tool with JSON arguments:

```json
{
  "tool": "copy",
  "arguments": {
    "source_path": "README.md",
    "destination_path": ".superdesign/README-copy.md",
    "overwrite": false
  }
}

```

The response should return a JSON payload with `success: true` and the metadata fields defined in your `createSuccessResponse` call.

## Minimal Example: Hello World Tool

For simple utilities that do not touch the filesystem, follow this minimal scaffold from [`src/tools/hello-tool.ts`](https://github.com/hbmartin/secure-design/blob/main/src/tools/hello-tool.ts):

```typescript
import { z } from 'zod';
import { tool } from 'ai';
import type { ExecutionContext } from '../types/agent';
import { createSuccessResponse, type ToolResponse } from './tool-utils';
import { getLogger } from 'react-vscode-webview-ipc/host';

const helloSchema = z.object({
    name: z.string().optional().default('Developer').describe('Who to greet'),
});

export function createHelloTool(context: ExecutionContext) {
    const logger = getLogger('hello tool');
    return tool({
        description: 'Return a friendly greeting.',
        inputSchema: helloSchema,
        execute: async ({ name }): Promise<ToolResponse> => {
            logger.info(`[hello] greeting ${name}`);
            return createSuccessResponse({ greeting: `Hello, ${name}!` });
        },
    });
}

```

## Summary

Extending the secure-design tool system requires adherence to the established factory pattern and security protocols:

- **Use Zod schemas** to define and validate input parameters with type safety.
- **Leverage [`tool-utils.ts`](https://github.com/hbmartin/secure-design/blob/main/tool-utils.ts)** for path validation (`validateWorkspacePath`), absolute resolution (`resolveWorkspacePath`), and standardized responses (`createSuccessResponse`, `handleToolError`).
- **Implement factory functions** like `createCopyTool` that accept `ExecutionContext` and return `tool` instances from the `ai` SDK.
- **Register tools** in [`src/services/customAgentService.ts`](https://github.com/hbmartin/secure-design/blob/main/src/services/customAgentService.ts) to expose them to the AI agent.
- **Maintain security** by validating all paths against workspace boundaries and wrapping execution in try/catch blocks.
- **Log operations** using the `getLogger` utility for traceability.

## Frequently Asked Questions

### How do I prevent my custom tool from accessing files outside the workspace?

Always pass file paths through `validateWorkspacePath(filePath, context)` before any filesystem operation. This utility, defined in [`src/tools/tool-utils.ts`](https://github.com/hbmartin/secure-design/blob/main/src/tools/tool-utils.ts) lines 49‑55, checks that the resolved absolute path remains within the workspace directory tree, preventing directory traversal attacks.

### What is the purpose of the Zod schema in secure-design tools?

The Zod schema serves as both a **runtime validator** and a **documentation generator** for the AI agent. It ensures that arguments passed to your tool conform to expected types (strings, booleans, etc.) before the `execute` function runs, and it provides the AI with structured descriptions of each parameter to improve tool selection accuracy.

### Where should I register a new tool to make it available to the AI agent?

Register your tool factory in [`src/services/customAgentService.ts`](https://github.com/hbmartin/secure-design/blob/main/src/services/customAgentService.ts) by importing the factory function (e.g., `createCopyTool`) and adding it to the `this.tools` array instantiation. This injects your tool into the agent’s tool registry, allowing the AI to discover and invoke it alongside built-in tools like `read` and `write`.

### How do I handle errors consistently across custom tools?

Import `handleToolError` from [`src/tools/tool-utils.ts`](https://github.com/hbmartin/secure-design/blob/main/src/tools/tool-utils.ts) and use it within a `try / catch` block wrapping your entire `execute` function. This ensures all unexpected exceptions return a standardized `ToolErrorResponse` with appropriate error codes and messages, maintaining consistency with the built-in tool error handling strategy.