# Tools and commands

> Add tools the model can call, guard them with hooks, and add slash commands you can type.


A **tool** is a function the model can call. A **command** is a slash command you type. Both are added from a setup block, which receives the session:

```ruby
Elelem.configure do |config|
  config.setup(:wc) do |session|
    session.toolbox.add("wc",
                        description: "Count lines, words, and bytes in a file",
                        params: { path: { type: "string", description: "File to count" } },
                        required: ["path"],
                        aliases: ["count"]) do |args|
      path = args["path"]
      next { error: "not found: #{path}" } unless File.file?(path)

      text = File.read(path)
      { lines: text.lines.count, words: text.split.count, bytes: text.bytesize }
    end

    session.toolbox.before("execute") do |args|
      raise "refusing to run rm" if args["command"].match?(/\brm\b/)
    end

    session.commands.register("wc", description: "Count a file", completions: -> { Dir["*"] }) do |path|
      session.output.say(session.toolbox.run("wc", { "path" => path.to_s }).to_h.inspect)
    end
  end
end
```

Save that as `.agents/plugins/wc.rb` and restart elelem. The rest of this page explains each part. [Plugins](/docs/plugins/) covers where files go and when they load.

## Tools

### `toolbox.add`

```ruby
toolbox.add(name, description:, params: {}, required: [], aliases: [], &handler)
```

| Argument | Meaning |
|---|---|
| `name` | The name the model calls. Use a string. |
| `description:` | What the tool does. The model reads this to decide when to call it. |
| `params:` | JSON Schema `properties`: a hash of parameter name to schema. |
| `required:` | Names of required parameters, as strings. |
| `aliases:` | Other names that resolve to this tool. |
| `&handler` | Runs the tool. Receives the arguments hash; returns a hash. |

Adding a tool with a name that already exists replaces it.

The handler gets the arguments the model sent, with **string keys**: `args["path"]`, not `args[:path]`. It must return a hash. Return `{ error: "..." }` to report a failure the model should see. Anything else is a success. Raising also works: the exception message becomes the error.

### What the model sees

`toolbox.to_a` turns every tool into an OpenAI-style function definition, and the provider sends that list to the model:

```ruby
{
  type: "function",
  function: {
    name: "wc",
    description: "Count lines, words, and bytes in a file",
    parameters: {
      type: "object",
      properties: { path: { type: "string", description: "File to count" } },
      required: ["path"]
    }
  }
}
```

Aliases are not sent. They exist to catch names the model guesses. `elelem-builtins` maps `bash`, `sh`, and `exec` to `execute` this way.

### Validation

Before a tool runs, its arguments are validated against `{ type: "object", properties: params, required: required }` with [json_schemer](https://github.com/davishmcclurg/json_schemer). Any JSON Schema keyword inside a property works: `enum`, `items`, `minimum`, and so on. Extra arguments are allowed, because the schema does not set `additionalProperties`.

A call that fails validation never reaches hooks or the handler. The model gets the errors joined with commas:

```json
{"error":"value at `/path` is not a string","ok":false}
```

### Results

`toolbox.run(name, args)` returns an `Elelem::Result`. It wraps the handler's hash and adds `ok: true` or `ok: false`.

```ruby
session.toolbox.run("wc", { "path" => "README.md" }).to_h
# => {lines: 2, words: 3, bytes: 6, ok: true}

session.toolbox.run("wc", { "path" => "nope" }).to_h
# => {error: "not found: nope", ok: false}
```

When the handler or a hook raises, the result also carries `name:` and `args:`, and elelem logs a warning with the backtrace.

A tool can call another tool through `toolbox.run`. The call goes through validation and hooks like any other. `elelem-tools` builds `glob`, `grep`, and `git` on top of `execute` this way.

### Hooks

```ruby
toolbox.before(tool_name = :*) { |args| ... }
toolbox.after(tool_name = :*) { |args, result| ... }
```

Hooks run around the handler, after validation. With a tool name, a hook runs for that tool only. Without one, it runs for every tool and also receives the tool's name:

```ruby
toolbox.before { |args, tool_name:| ... }
toolbox.after { |args, result, tool_name:| ... }
```

The order for one call is:

1. Global `before` hooks, in the order added.
2. `before` hooks for this tool.
3. The handler.
4. Global `after` hooks.
5. `after` hooks for this tool.

Hook return values are ignored. To stop a call, raise in a `before` hook; the message goes back to the model as the error. `elelem-tools` builds its confirmation prompt this way: a global `before` hook that asks you and raises `User denied permission: execute` if you answer `n`.

`after` hooks receive the handler's raw hash, before `ok:` is added. They run even when that hash has an `:error` key, so check for it. Hooks are matched by the tool's real name, never an alias. A call to an unknown tool runs no hooks.

### Inspect the toolbox

| Method | Returns |
|---|---|
| `toolbox.tools` | Hash of name to `Elelem::Tool` |
| `toolbox.aliases` | Hash of alias to name |
| `toolbox.hooks` | `{ before: {name => [blocks]}, after: {name => [blocks]} }` |
| `toolbox.tool_for(name)` | The tool, following aliases. An unknown name returns a stand-in whose name ends in `?`. |

With `elelem-builtins`, `/tools` prints a table of tools and `/tools NAME` prints one tool's parameters.

## Slash commands

### `commands.register`

```ruby
commands.register(name, description: "", completions: nil) { |args| ... }
```

| Argument | Meaning |
|---|---|
| `name` | The command without the slash. Use a string. |
| `description:` | Shown by `/help` in `elelem-builtins`. |
| `completions:` | An array of strings, or a lambda that returns one. Used for tab completion of the argument. |
| block | Runs the command. |

The block receives everything after the first space as one string, or `nil` if there is nothing. `/context 3` passes `"3"`. `/skills fetch example.com` passes `"fetch example.com"`. Registering the same name again replaces the command.

A command can do anything with the session: print, change the conversation, swap the provider, or run a turn. The `/compact` command in `elelem-tools` calls `session.turn` to ask for a summary, clears the conversation, and stores the summary as the new first message.

### How the REPL runs commands

- A line that starts with `/` is a command. The REPL splits off the name at the first space.
- If no command has that name, the REPL prints the names of all commands.
- If the command raises, the REPL logs a warning and prints the message as an error. The session keeps running.

### Tab completion

The default input completes:

- Command names, when the line starts with `/` and has no space yet.
- Command arguments, from `completions:`, after `/name `.
- File names everywhere else, up to 20 matches.

### Other methods

| Method | Returns |
|---|---|
| `commands.run(name, args = nil)` | `true` if the command exists and ran, `false` otherwise |
| `commands.names` | `["/exit", "/help", ...]` |
| `commands.each { \|name, description\| }` | Yields `"/name"` and its description. `Commands` is `Enumerable`. |
| `commands.completions_for(name, partial = "")` | The completions that start with `partial` |
| `commands.clear!` | Removes every command |

Full signatures are in the [Plugin API](/docs/plugin-api/).


Canonical: https://elelem.sh/docs/tools-and-commands/

