---
title: Tools
description: One tool module runs on every agent harness. beeuni.yaml lists the Beeuni tools and the project's own tools, and each agent's tools list decides who may call them.
---

A tool is a function the model calls during a turn. A
[session](/docs/work/sessions) has three kinds, on both
[harnesses](/docs/work/harnesses), default runtime and experimental runtime:

| Kind | Names | Implemented by |
| --- | --- | --- |
| Harness tools | `bash`, `read`, `write`, `edit`, `glob`, `grep`, `task`, `question`, … | The harness. |
| Beeuni tools | `web_search`, `image_search`, `scrape_webpage`, `memory`, `show` | Beeuni. `beeuni.yaml` lists each one as `<name>: beeuni:<name>`. A project with no `tools:` key gets all five. |
| Project tools | Any name you declare | Your project: one module, declared under `tools:` in `beeuni.yaml`. |

Beeuni tools and project tools are harness-neutral. The sandbox daemon loads
one module per tool and runs it for either harness, with the same name,
arguments, and output. The experimental runtime calls the module in the daemon's process. The default runtime
calls it through a plugin the daemon writes, which forwards each call to the
daemon. A module never imports a harness package.

## Beeuni tools in beeuni.yaml

A new project lists the five Beeuni tools under `tools:`:

```yaml
tools:
  web_search: beeuni:web_search      # maintained by Beeuni, updated automatically
  image_search: beeuni:image_search
  scrape_webpage: beeuni:scrape_webpage
  memory: beeuni:memory
  show: beeuni:show
  # lookup_order: tools/lookup_order.ts   # your own tool: a module in your repo
```

`beeuni:<name>` means: the tool Beeuni maintains. A fix to it reaches every
project that lists it, with no change in the project.

### Which Beeuni tools a session gets

| `beeuni.yaml` | The session gets |
| --- | --- |
| No `tools:` key, in the root file or in any [imported](/docs/project/manifest#imports) file | All five Beeuni tools. A project created before this list existed keeps them all. |
| A `tools:` key, even an empty one (`tools: {}`, or `tools:` with every line deleted) | Only the Beeuni tools it lists, as `beeuni:<name>` or as a module that replaces the tool. |

The project's own tools load in both cases.

### See what a session gets

`beeuni tools ls` reads `beeuni.yaml` and its imports and prints each tool a
session gets, then the Beeuni tools the project removed:

```text
$ beeuni tools ls
NAME            KIND                 SOURCE
web_search      beeuni (overridden)  tools/web_search.ts
scrape_webpage  beeuni               beeuni:scrape_webpage
memory          beeuni               beeuni:memory
show            beeuni               beeuni:show
lookup_order    project              tools/lookup_order.ts

Removed Beeuni tools: image_search
Add `<name>: beeuni:<name>` under `tools:` to get one back.
```

| Kind | Meaning |
| --- | --- |
| `beeuni` | A Beeuni tool, listed as `beeuni:<name>` (or every one, with no `tools:` key). |
| `beeuni (overridden)` | A Beeuni tool name with a module path: the project's module runs instead. |
| `project` | A tool of the project's own. |

`beeuni tools ls --json` prints `{ manifest, tools_key, tools: [{ name, kind, source }], removed }`.

### Remove a Beeuni tool

Delete its line under `tools:`. Every agent of the project loses the tool,
on both harnesses, in every session that starts after the change request
merges. To keep a tool from one agent only, use
[`agents.<name>.tools`](#who-may-use-which-tool) instead.

A session of a project that removed a tool:

- **default runtime** does not offer the tool to the model.
- **experimental runtime** does not offer the tool. A call to it fails with `Tool show not found`
  (for `show`).
- **The daemon** answers `POST /beeuni/tools/<name>` with `404` and
  `no tool named <name> is loaded`.
- **A skill that names the tool still loads.** For example, `beeuni-memory`
  names `memory`. The model reads the skill, finds no such tool, and uses
  other tools: without `memory` it reads and edits the files under `memory/`
  with the file tools.

### Change a Beeuni tool: eject

`beeuni tools eject <name>` gives the project its own copy of a Beeuni tool:

```text
$ beeuni tools eject web_search
  ✓  Ejected web_search
  wrote   tools/web_search.ts (the Beeuni source, now the project's to change)
  set     beeuni.yaml: tools.web_search: tools/web_search.ts

Next:
  1. Edit tools/web_search.ts. Keep the default export: { description, parameters, execute(args, context) }.
  2. beeuni validate
  3. git add tools/web_search.ts beeuni.yaml && git commit -m "Own the web_search tool"
  4. Open a change request: git push origin HEAD && beeuni cr open --title "Own the web_search tool"
Sessions load the copy after the change request merges.
```

- It writes the Beeuni source, the same bytes the sandbox runs, to
  `tools/<name>.ts`. The file imports only `node:*` modules and reads
  `BEEUNI_API_URL`, `BEEUNI_TOKEN`, and provider keys from `context.env`, so it
  runs as it is.
- It sets `<name>: tools/<name>.ts` in `beeuni.yaml`, in the file that
  declares the entry (the root file or an imported file). Every other line and
  comment stays. The changed line ends with `# ejected from beeuni:<name>`.
- With no `tools:` key, it creates one and lists the other four Beeuni tools
  as `beeuni:<name>`, so the eject removes nothing.
- It refuses when `tools/<name>.ts` exists, or when the tool already points at
  another module. `--force` overwrites.
- It refuses a name that is not a Beeuni tool.

The copy is yours. Beeuni no longer updates it. To return to the maintained
tool, set the line back to `<name>: beeuni:<name>` and delete the file.

### Override without eject

A Beeuni tool name with a module path replaces the Beeuni tool, on every
harness:

```yaml
tools:
  web_search: integrations/search/web_search.ts
```

### The web UI views

The web UI has a view for each Beeuni tool, picked by the tool name. The
`show` and `memory` views read the call's arguments (`type`, `path`, `url`,
`content`, `items`; `command`, `path`). The `web_search`, `image_search`, and
`scrape_webpage` views read the output JSON. A changed copy that keeps the
name must keep those arguments and that output shape, or the UI shows the raw
output.

### Validation

`beeuni validate` checks the list:

- `<name>` in `beeuni:<name>` must be a Beeuni tool, and the key must equal
  it. `web_search: beeuni:image_search` is an error:
  ``must be `beeuni:web_search` (the Beeuni tool) or a repo-relative path to a .ts or .js module (e.g. `tools/web_search.ts`) that replaces it.``
- A `tools:` key that lists no Beeuni tool is a warning:
  ``Sessions of this project get no Beeuni tool (web_search, image_search, scrape_webpage, memory, show). Add `<name>: beeuni:<name>` to keep one.``
  It catches a project that adds its first own tool and drops the five by
  accident.
- An agent whose `tools` names a Beeuni tool the project does not load is a
  warning: ``"show" is a Beeuni tool this project does not load: add `show: beeuni:show` under the top-level `tools`, or remove it here.``

The [manifest reference](/docs/project/manifest#tools) has every rule.

## Write a project tool

```ts
// tools/lookup_order.ts
export default {
  description: 'Look up an order by id. Returns its status and carrier.',
  parameters: {
    type: 'object',
    properties: {
      id: { type: 'string', description: 'The order id, e.g. "A-1042"' },
      include_items: { type: 'boolean', description: 'Also list the items' },
    },
    required: ['id'],
  },
  async execute(args, context) {
    const response = await fetch(`https://orders.example.com/v1/orders/${args.id}`, {
      headers: { authorization: `Bearer ${context.env.ORDERS_API_KEY}` },
      signal: context.signal,
    })
    if (!response.ok) throw new Error(`orders API answered ${response.status}`)
    return await response.json()
  },
}
```

```yaml
# beeuni.yaml
tools:
  web_search: beeuni:web_search
  # … the other Beeuni tools you keep
  lookup_order: tools/lookup_order.ts
```

The default export is a plain object:

| Field | Rule |
| --- | --- |
| `description` | Non-empty string. The model reads it to decide when to call the tool. |
| `parameters` | [JSON Schema](https://json-schema.org) of the arguments, `type: 'object'`. The model receives it as it is, so list required arguments in `required`. Beeuni does not validate the arguments against it. |
| `execute(args, context)` | Returns a string, or any JSON value (sent as indented JSON). May be `async`. A thrown error is the tool's error, and the model reads its message. |

`context` carries:

| Field | What it is |
| --- | --- |
| `sessionId` | The Beeuni session the call belongs to. |
| `agent` | The agent that made the call. |
| `directory` | The project checkout, `/workspace`. Resolve a relative path against it. |
| `env` | The box environment plus the [secrets](/docs/project/secrets) this agent receives, as they are now. Read a secret as `context.env.NAME`: a secret changed after the box booted is in `context.env`, not in `process.env`. |
| `signal` | An `AbortSignal` that aborts when the user stops the turn. Pass it to `fetch`. |

- **Output size.** An output over 50 KB or 2,000 lines is saved whole to a
  file in the sandbox's temp directory. The model receives the head, the size,
  and the file path. Return what the model needs, not a raw dump.
- **Errors.** A thrown error ends the call, and the model reads the error
  message. Throw a message that says what to change.
- **Imports.** A module may import `node:*` modules, Bun APIs, and relative
  files. A package resolves from a `node_modules` next to the module or above
  it. The sandbox installs no package for a tool, and a
  [config release](/docs/work/runtime#config-convergence) holds only the files
  Git tracks. Use `fetch` for HTTP APIs. TypeScript runs without a build step.
- **Failures.** A module that does not load (a missing file, no default
  export, a `parameters` that is not an object schema, an error at import, a
  package that does not resolve) is left out, and the other tools still load.
  The daemon logs `[tools] project tools that did not load` with the reason
  per tool.

### `parameters` with zod

zod 4 writes the same JSON Schema:

```ts
import { z } from 'zod'

export default {
  description: 'Look up an order by id.',
  parameters: z.toJSONSchema(z.object({ id: z.string().describe('The order id') })),
  async execute(args) {
    return `order ${args.id}: shipped`
  },
}
```

This module loads only where `zod` resolves from a `node_modules` in the
checkout the session runs. Otherwise it fails with `Cannot find package 'zod'`.
To write the schema with zod and ship plain JSON Schema, print it once and
paste the output into `parameters`:

```bash
bun -e "import { z } from 'zod'; console.log(JSON.stringify(z.toJSONSchema(z.object({ id: z.string() })), null, 2))"
```

### Example: call an HTTP API with a project secret

Store the key as a project secret. The command reads the value from stdin:

```bash
beeuni secrets set WEATHER_API_KEY=-
```

Grant the secret to the agents that call the tool. The starter's `beeuni`
agent has `secrets: all`. A scoped agent lists it: `secrets: [WEATHER_API_KEY]`.

Write the module:

```ts
// tools/weather_now.ts
export default {
  description: 'Current weather for a city: temperature in °C and conditions.',
  parameters: {
    type: 'object',
    properties: { city: { type: 'string', description: 'City name, e.g. "Berlin"' } },
    required: ['city'],
  },
  async execute(args, context) {
    const key = context.env.WEATHER_API_KEY
    if (!key) throw new Error('WEATHER_API_KEY is not set: run `beeuni secrets set WEATHER_API_KEY=-` and grant it to this agent.')
    const url = `https://api.weather.example.com/v1/current?city=${encodeURIComponent(args.city)}`
    const response = await fetch(url, { headers: { authorization: `Bearer ${key}` }, signal: context.signal })
    if (!response.ok) throw new Error(`weather API answered ${response.status}: ${await response.text()}`)
    const data = await response.json()
    return { city: args.city, temperature_c: data.temp_c, conditions: data.conditions }
  },
}
```

Declare it:

```yaml
tools:
  # … the Beeuni tools you keep
  weather_now: tools/weather_now.ts
```

### Example: override `web_search`

`beeuni tools eject web_search` writes `tools/web_search.ts` and sets
`web_search: tools/web_search.ts`. Then change the copy. For example, to keep
results from one site only, add the site to each query in `execute`, before
the request:

```ts
// tools/web_search.ts, inside execute()
const queries = String(args.query ?? '').split('|||').map((query) => query.trim()).filter(Boolean)
  .map((query) => `site:docs.example.com ${query}`)
```

Keep the name, the arguments, and the output JSON, so skills that name
`web_search` and the web UI view still work.

### Test a tool

`beeuni validate` checks the name, the path, a missing file, and the
`beeuni:` values. `beeuni tools ls` shows what a session gets.

Then commit, push, and open a change request. After it merges, start a
session and ask for one call with real arguments:

```bash
beeuni sessions new --wait --prompt "Call the weather_now tool once with city Berlin and print its output."
```

A tool that does not load is not offered to the model. A tool that throws
answers with its error message. `beeuni sessions log <id>` prints the
session's messages, with each tool call and its status.

## Declare tools

`tools:` maps each tool name to its module, or a Beeuni tool name to
`beeuni:<name>`. The folder of a module is yours to choose.

```yaml
tools:
  web_search: beeuni:web_search
  memory: beeuni:memory
  lookup_order: tools/lookup_order.ts
  crm_note: integrations/crm/note.ts
```

- A name is snake_case, at most 64 characters: `^[a-z][a-z0-9_]{0,63}$`.
- A harness tool name is an error. A Beeuni tool name takes `beeuni:<name>`
  or a module path; the module replaces the Beeuni tool, on every harness.
- A path is repo-relative and ends in `.ts`, `.js`, `.mts`, `.mjs`, `.cts`, or
  `.cjs`.
- A file listed under [`imports:`](/docs/project/manifest#imports) may declare
  tools. A name declared in two files is an error. A `tools:` key in an
  imported file counts as a `tools:` key for the selection rule.

[The manifest reference](/docs/project/manifest#tools) has the full rules.

## Who may use which tool

`agents.<name>.tools` decides which tools an agent's model sees. It names
harness, Beeuni, and project tools alike.

```yaml
agents:
  beeuni:
    tools: all                                    # every tool (the default)
  researcher:
    tools: [read, grep, web_search, lookup_order] # only these
  support:
    tools:
      exclude: [bash, write, edit]                # every tool but these
  greeter:
    tools: none                                    # no tool
```

| Value | The agent gets |
| --- | --- |
| omitted, or `all` | Every tool. |
| `none` | No tool. |
| a list | Only the tools listed. A list with `*` is `all`. |
| `{ exclude: [...] }` | Every tool except the ones listed. |
| `{ bash: false, … }` | The earlier map form: `false` removes a tool. |

- `tools` decides visibility. The agent's `permission` rules still decide
  whether a call is allowed, asked, or denied. A tool not in the list is
  hidden from the model and denied if called.
- An agent list cannot add a Beeuni tool the project does not load. List it
  under the top-level `tools:` first.
- A subagent started with the `task` tool: on the experimental runtime it gets only the tools both
  its own list and the session agent's list allow. on the default runtime it gets the
  tools of its own list: default runtime applies an agent's rules to that agent only.
  To stop an agent from delegating, leave `task` out of its list.
- [Connector](/docs/connect/connectors) tools follow `agents.<name>.connectors`,
  not this list.
- `beeuni validate` warns about a name that is not a harness, Beeuni, or
  project tool, and about a Beeuni tool the project does not load. A typo in
  an `exclude` list leaves the real tool on.

on the default runtime, `bash` also covers the terminal plugin's `pty_*` tools, and
`write`, `edit`, and `apply_patch` share one permission: listing one shows
all three, and excluding one hides all three. The experimental runtime has separate `write` and
`edit` tools.

## When a change takes effect

A session loads its tools when its runtime starts. A change applies after
the change request merges: new sessions load it, and with
[config releases](/docs/work/runtime#config-convergence) a running session
loads it at the next convergence. on the experimental runtime, a change to a project tool or to the
list of Beeuni tools restarts the runtime in place while no turn runs; the
conversation continues.

## Tools in `harnesses/opencode/tools/`

The default runtime still loads custom tools from its config directory. The experimental runtime does not.
Projects created before hosted tools carry copies of the Beeuni tools there
(`web_search.ts`, `memory.ts`, …).

- on the default runtime, a tool in that folder wins over the hosted tool of the same
  name, so a project keeps its copy until it deletes it. Delete the copies of
  the Beeuni tools: the daemon hosts the ones `beeuni.yaml` lists, on both
  harnesses.
- To make an default runtime tool run on both harnesses, move it out of the folder,
  replace `tool({ description, args, execute })` with a plain object whose
  `parameters` is JSON Schema, read secrets from `context.env`, and declare it
  under `tools:`.
