> ## Documentation Index
> Fetch the complete documentation index at: https://docs.symbioticsec.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom tools

> Create functions that the LLM can call during conversations

Custom tools are functions you create that the LLM can call during conversations. They work alongside Symbiotic Code's built-in tools like `read`, `write`, and `bash`.

## Creating a tool

Tools are defined as **TypeScript** or **JavaScript** files. However, the tool definition can invoke scripts written in **any language** — TypeScript or JavaScript is only used for the tool definition itself.

### Location

They can be defined:

* Locally by placing them in the `.symbiotic/tools/` directory of your project.
* Or globally, by placing them in `~/.config/symbiotic/tools/`.

Only `.ts` and `.js` files placed directly in these folders are loaded (subfolders are not scanned). The singular folder name `tool/` also works.

### Structure

The easiest way to create tools is using the `tool()` helper from the `@symbioticsec/plugin` package, which provides type-safety and validation.

```ts title=".symbiotic/tools/database.ts" {1} theme={null}
import { tool } from "@symbioticsec/plugin"

export default tool({
  description: "Query the project database",
  args: {
    query: tool.schema.string().describe("SQL query to execute"),
  },
  async execute(args) {
    // Your database logic here
    return `Executed query: ${args.query}`
  },
})
```

The **filename** becomes the **tool name**. The above creates a `database` tool.

`execute` must return a **string**: it is the result sent back to the model. Long results are truncated (2,000 lines or 50 KB) and the full output is saved to a file the agent can read.

#### Dependencies

You don't need to install `@symbioticsec/plugin` yourself: at startup, Symbiotic Code creates a `package.json` in the config folder (`.symbiotic/` or `~/.config/symbiotic/`) with `@symbioticsec/plugin` as a dependency and installs it with Bun. It also adds a `.gitignore` for these generated files.

To use other npm packages in your tools, add them to the `dependencies` of that same `package.json`. Package install scripts are not run.

#### Multiple tools per file

You can also export multiple tools from a single file. Each export becomes **a separate tool** with the name **`<filename>_<exportname>`**:

```ts title=".symbiotic/tools/math.ts" theme={null}
import { tool } from "@symbioticsec/plugin"

export const add = tool({
  description: "Add two numbers",
  args: {
    a: tool.schema.number().describe("First number"),
    b: tool.schema.number().describe("Second number"),
  },
  async execute(args) {
    return String(args.a + args.b)
  },
})

export const multiply = tool({
  description: "Multiply two numbers",
  args: {
    a: tool.schema.number().describe("First number"),
    b: tool.schema.number().describe("Second number"),
  },
  async execute(args) {
    return String(args.a * args.b)
  },
})
```

This creates two tools: `math_add` and `math_multiply`.

<Warning>
  Every export of a tool file is loaded as a tool. Don't export helper functions or constants from these files: move them to a separate file outside the `tools/` folder.
</Warning>

#### Name collisions with built-in tools

Custom tools are keyed by tool name. If a custom tool uses the same name as a built-in tool, the custom tool takes precedence.

For example, this file replaces the built-in `bash` tool:

```ts title=".symbiotic/tools/bash.ts" theme={null}
import { tool } from "@symbioticsec/plugin"

export default tool({
  description: "Restricted bash wrapper",
  args: {
    command: tool.schema.string(),
  },
  async execute(args) {
    return `blocked: ${args.command}`
  },
})
```

<Info>
  Prefer unique names unless you intentionally want to replace a built-in tool. If you want to disable a built-in tool but not override it, use [tool permissions](/code/security/tool_permissions).
</Info>

### Arguments

You can use `tool.schema`, which is just [Zod](https://zod.dev), to define argument types.

```ts "tool.schema" theme={null}
args: {
  query: tool.schema.string().describe("SQL query to execute")
}
```

You can also import [Zod](https://zod.dev) directly and export a plain object:

```ts {6} theme={null}
import { z } from "zod"

export default {
  description: "Tool description",
  args: {
    param: z.string().describe("Parameter description"),
  },
  async execute(args, context) {
    // Tool implementation
    return "result"
  },
}
```

### Context

Tools receive context about the current session:

```ts title=".symbiotic/tools/project.ts" {8} theme={null}
import { tool } from "@symbioticsec/plugin"

export default tool({
  description: "Get project information",
  args: {},
  async execute(args, context) {
    // Access context information
    const { agent, sessionID, messageID, directory, worktree } = context
    return `Agent: ${agent}, Session: ${sessionID}, Message: ${messageID}, Directory: ${directory}, Worktree: ${worktree}`
  },
})
```

| Property                 | Description                                                     |
| ------------------------ | --------------------------------------------------------------- |
| `directory`              | The session working directory                                   |
| `worktree`               | The git worktree root                                           |
| `agent`                  | Name of the agent calling the tool                              |
| `sessionID`, `messageID` | Identifiers of the current session and message                  |
| `abort`                  | An `AbortSignal` triggered when the user interrupts the session |
| `ask(...)`               | Request a permission from the user, see below                   |

### Permissions

Custom tools follow [tool permissions](/code/security/tool_permissions) using their tool name as the permission key. Setting a tool to `"deny"` (globally or for an [agent](/code/customize/custom_agents)) removes it from the tools sent to the model:

```json symbiotic.json theme={null}
{
  "permission": {
    "database": "deny"
  }
}
```

`"ask"` doesn't prompt the user automatically: a custom tool runs without confirmation unless it calls `context.ask()` itself. The call resolves when the action is allowed (by a rule or by the user) and throws when it is denied:

```ts title=".symbiotic/tools/deploy.ts" theme={null}
import { tool } from "@symbioticsec/plugin"

export default tool({
  description: "Deploy the application to an environment",
  args: {
    env: tool.schema.enum(["staging", "production"]),
  },
  async execute(args, context) {
    await context.ask({
      permission: "deploy",
      patterns: [args.env],
      always: [args.env],
      metadata: { env: args.env },
    })
    // Deployment logic here
    return `Deployed to ${args.env}`
  },
})
```

With this tool, `"deploy": { "*": "ask", "staging": "allow" }` deploys to staging without asking and asks before deploying to production. `always` lists the patterns saved when the user chooses to always allow the request.

<Warning>
  Custom tools run on your machine with your user's privileges, outside the [agents container](/code/security/containers). Only add tools from sources you trust, and validate arguments before passing them to shell commands or queries.
</Warning>

## Examples

### Write a tool in Python

You can write your tools in any language you want. Here's an example that adds two numbers using Python.

First, create the tool as a Python script:

```python title=".symbiotic/tools/add.py" theme={null}
import sys

a = int(sys.argv[1])
b = int(sys.argv[2])
print(a + b)
```

Then create the tool definition that invokes it:

```ts title=".symbiotic/tools/python-add.ts" {10} theme={null}
import { tool } from "@symbioticsec/plugin"
import path from "path"

export default tool({
  description: "Add two numbers using Python",
  args: {
    a: tool.schema.number().describe("First number"),
    b: tool.schema.number().describe("Second number"),
  },
  async execute(args, context) {
    const script = path.join(context.worktree, ".symbiotic/tools/add.py")
    const result = await Bun.$`python3 ${script} ${args.a} ${args.b}`.text()
    return result.trim()
  },
})
```

Here we are using the [`Bun.$`](https://bun.com/docs/runtime/shell) utility to run the Python script. Interpolated values are escaped by `Bun.$`, so arguments can't inject extra shell commands.

## Tools from plugins

[Plugins](/code/customize/plugins) can also provide tools with their `tool` hook, using the same `tool()` helper. Plugin tools are named after their key in the `tool` object, without a file name prefix.
