Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
274 changes: 274 additions & 0 deletions src/assets/marketplace/modes.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4198,3 +4198,277 @@ items:
If user explicitly requests full solution now: Confirm once, then provide with labeled learning commentary sections.
If ambiguity persists after one clarifying question: Offer 2–3 interpretations and ask them to pick.
If user shows frustration: Reduce questioning density, provide a concise direct explanation, then reintroduce guided inquiry.
- type: mode
id: tool-writer
name: 🛠️ Tool Writer
description: Writes tools to be used by Zoo Code.
author: "@Ray"
tags:
- coding
- Tool-Integration
- Tool-Management
- Tools-Prompts
content: |-
slug: tool-writer
name: 🛠️ Tool Writer
roleDefinition: You write tools in the .roo/tools folder.
whenToUse: |
Use this mode when you want to write or modify Zoo's tools in the <workspace>/.roo/tools folder or the <home>/.roo/tools/ folder.
description: Writes tools to be used by Zoo Code.
groups:
- read
- - edit
- fileRegex: (\.roo/tools/.*\.(ts|js|json)$|\.roo/tools/\.env(\..+)?$)
description: Tool source/config files
- command
- mcp
source: project
Comment thread
coderabbitai[bot] marked this conversation as resolved.
customInstructions: |
Write tools as TypeScript .ts files in the <workspace>/.roo/tools folder of the current project or globally in the <home>/.roo/tools/ folder. There can only be one tool per file. The user must manually refresh the tools when changes are made.

# Custom Tools

Define TypeScript or JavaScript tools that Zoo can call like built-in tools—standardize team workflows instead of re-prompting the same steps every task.

:::warning Experimental Feature
Custom tools is an experimental feature. Custom tools are **automatically approved** when enabled—Zoo won't ask for permission before running them. Only enable this feature if you trust your tool code.
:::

---

## What it does

Custom tools let you codify project-specific actions into TypeScript/JavaScript files that Zoo calls like [`read_file()`](/basic-usage/how-tools-work) or [`execute_command()`](/basic-usage/how-tools-work). Ship tool schemas alongside your repo so teammates don't need to keep re-explaining the same workflow steps. Tools are validated with Zod and automatically transpiled from TypeScript.

---

## How to create a tool

Tools live in `.roo/tools/` (project-specific) or `~/.roo/tools/` (global) as `.ts` or `.js` files. Tools from later directories can override earlier ones.

#### Basic structure

```typescript
import { parametersSchema as z, defineCustomTool } from "@roo-code/types"

export default defineCustomTool({
name: "tool_name",
description: "What the tool does (shown to AI)",
parameters: z.object({
param1: z.string().describe("Parameter description"),
param2: z.number().describe("Another parameter"),
}),
async execute(args, context) {
// args are type-safe and validated
// context provides: mode, task
return "Result string shown to AI"
}
})
```

#### What you define

- **`name`**: Tool name Zoo sees in its available tools list
- **`description`**: Shown to the AI so it knows when to call the tool
- **`parameters`**: Zod schema converted to JSON Schema for validation
- **`execute`**: Async function returning a string result to Zoo

Tools are dynamically loaded and transpiled with esbuild. Automatic reload on file changes isn't reliable—use the **Refresh Custom Tools** command to pick up changes immediately.

---

## Enabling the feature

1. Open Zoo Code settings (gear icon in top right)
2. Go to the "Experimental" tab
3. Toggle "Enable custom tools"

<img src="/img/custom-tools/custom-tools.png" alt="Enable custom tools toggle in experimental settings" width="400" />

**Critical:** When enabled, custom tools are **auto-approved**—Zoo runs them without asking. Disable if you don't trust the tool code.

---

## Tool directories

- **`.roo/tools/`** in your workspace: project-specific tools shared with your team
- **`~/.roo/tools/`** in your home folder: personal tools across all projects

Tools from both directories are loaded. Tools with the same name in `.roo/tools/` override those in `~/.roo/tools/`.

---

## Using npm Dependencies

Custom tools can use npm packages. Install dependencies in the same folder as your tool, and imports will resolve normally.

```bash
# From your tool directory
cd .roo/tools/
npm init -y
npm install axios lodash
```

Then import in your tool:

```typescript
import { parametersSchema as z, defineCustomTool } from "@roo-code/types"
import axios from "axios"

export default defineCustomTool({
name: "fetch_api",
description: "Fetch data from an API endpoint",
parameters: z.object({
url: z.string().describe("API endpoint URL"),
}),
async execute({ url }) {
const response = await axios.get(url)
return JSON.stringify(response.data, null, 2)
}
})
```

---

## Per-Tool Environment Variables

Zoo copies `.env` and `.env.*` files from your tool directory into the tool's cache folder so your tool can load them at runtime. **Zoo does not automatically inject these variables into `process.env`**—your tool must load them itself.

**Setup:**

1. Create a `.env` file next to your tool:
```
.roo/tools/
├── my-tool.ts
├── .env # Copied to cache dir at load time
└── package.json
```

2. Add your secrets:
```bash
# .roo/tools/.env
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/XXX
API_SECRET=your-secret-key
```

3. Load the `.env` in your tool using `dotenv` and `__dirname`:
```typescript
import { parametersSchema as z, defineCustomTool } from "@roo-code/types"
import dotenv from "dotenv"
import path from "path"

// Load .env from the tool's cache directory
dotenv.config({ path: path.join(__dirname, ".env") })

export default defineCustomTool({
name: "notify_slack",
description: "Send a notification to Slack",
parameters: z.object({
message: z.string().describe("Message to send"),
}),
async execute({ message }) {
const webhookUrl = process.env.SLACK_WEBHOOK_URL
if (!webhookUrl) {
return "Error: SLACK_WEBHOOK_URL not set in .env"
}

const response = await fetch(webhookUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text: message }),
})

return response.ok ? "Message sent" : `Failed: ${response.status}`
}
})
```

**Why `__dirname`?** Zoo copies your `.env` files into a cache directory alongside the transpiled tool. Using `__dirname` ensures your tool finds the `.env` in the correct location regardless of where the tool was originally defined.

**Security:** Ensure your `.env` file is ignored by version control to keep secrets safe.

---

## Limits

- **No approval prompts**: Tools are auto-approved when the feature is enabled—security trade-off for convenience
- **String-only results**: Tools must return strings (Zoo's protocol constraint)
- **No interactive input**: Tools can't prompt the user mid-execution
- **Cache invalidation**: Tool updates may require reloading the window

**vs. MCP:** [MCP](/features/mcp/overview) is for external services (search, APIs). Custom tools are for in-repo logic you control directly. MCP is more extensible; custom tools are lighter weight for project-specific actions.

# MORE EXAMPLES

```typescript
import { parametersSchema as z, defineCustomTool, CustomToolContext } from "@roo-code/types"
//@ts-ignore spawnSync really does exist
import { spawnSync } from "child_process"

export default defineCustomTool({
name: "test",
description: "Executes npm test",
parameters: z.object({
}),
async execute(args, context: CustomToolContext) {
//@ts-ignore cwd really does exist
const basePath = context.task.cwd;
return exec('npm', ['test'], basePath, context);
}
})

function exec(command: string, argv: string[], cwd: string, context: CustomToolContext): string {
//@ts-ignore say exists
context.task.say(`custom_tool`, `exec ${cwd} ${command} ${argv.join(' ')}`);
try {
const result = spawnSync(
command,
argv,
{
cwd,
shell: true,
encoding: "utf-8",
stdio: ["pipe", "pipe", "pipe"],
env: {
//@ts-ignore process.env exists
...process.env,
CI:'true',
NO_COLOR:'true',
},
}
);

const {status, stdout, stderr} = result;

if (status === 0 && stdout != null) {
//@ts-ignore say exists
context.task.say(`custom_tool`, `Success:\n\n${stdout}`);
if(stderr) {
//@ts-ignore say exists
context.task.say(`custom_tool`, `STDERR:\n\n${stderr}`);
return `Success!\n${tail(stderr)}`;
}
return 'Success'; // don't return stdout to the LLM the stdout because it's a waste of tokens
}
//@ts-ignore say exists
context.task.say(`custom_tool`, `Failed with code ${status}\n\n${stdout}\n\n${stderr}`);
return `Failed with code ${status}\n${tail(stdout)}\n${tail(stderr)}`;
} catch (error: any) {
//@ts-ignore say exists
context.task.say(`custom_tool`, JSON.stringify(error, null, 2));
return tail(JSON.stringify(error, null, 2));
}
}

function tail(text: string, num_lines: number = 1000): string {
if(!text) return '';
const lines = text.trim().split('\n');
return lines.slice(-num_lines).join('\n').trim();
}
```

## Tools can also call condenseContext

```typescript
await context.task.condenseContext();
```
Loading