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

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, and registers the tool in 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, this provides the workingDirectory and logger instance required by every tool operation.
  • Tool Utilities – The 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 and 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 and declare the tool’s contract using Zod. This ensures the AI agent passes valid arguments before execution begins.

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.

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. Import your factory and add it to the tools array to make it available to the AI agent:

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 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 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 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:
{
  "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:

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 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 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 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 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 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →