Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Custom tools

Your own tools for the model, declared in ~/.wisp/config.json: a command line with {placeholders} and the typed arguments that fill them. The model sees an ordinary tool; a call substitutes the values and runs the line exactly as run_command would, through the policy, classifier, approval, sandbox, and audit. Only your own config can define them; a project cannot. Decided in ADR 0036.

{
  "tools": {
    "disabled": ["notify"],
    "custom": [
      {
        "name": "issue",
        "description": "Shows a GitHub issue of the current repository.",
        "arguments": { "number": { "type": "integer", "description": "The issue number" } },
        "command": "gh issue view {number}"
      },
      {
        "name": "line_head",
        "description": "Shows the first lines of a file.",
        "arguments": {
          "path": { "type": "string", "description": "The file" },
          "lines": { "type": "integer", "description": "How many lines", "default": 5 }
        },
        "command": "head -n {lines} {path}",
        "timeoutSeconds": 10
      }
    ]
  }
}

Fields

FieldRequiredMeaning
nameyesWhat the model calls it: snake_case, up to 40 characters, not a built-in’s name, not repeated.
descriptionyesWhat the model is told it does, 1 to 300 characters. Write it for the model, as a sentence.
argumentsnoBy name (snake_case): type (string, integer, number, boolean), description, enum (strings only), default (makes it optional).
commandyesRun with /bin/sh -c. Every {name} must be a declared argument, and every argument must appear.
workingDirectorynoWhere it runs; ~ expands. Default: where wisp runs.
timeoutSecondsnoDefault: commandTimeoutSeconds.

tools.disabled lists built-in tools to leave out: current_date, run_command, read_file, edit_file, inspect, notify, system_info.

How a call runs

Values are substituted into the command: strings single-quoted, so a value cannot break out of its argument (it's becomes 'it'\''s'), and checked against the enum; integers, numbers, and booleans as written. Substitution is one pass over the template, so a value that holds another placeholder ({b}) stays text and is never filled in itself (before 0.21.1 it was, which let one value unquote another). A missing required value or one of the wrong type comes back to the model as error: … without running anything. The substituted line then goes through run_command’s runner: the policy’s deny patterns, the risk classifier and approval (judged on the substituted line, so a template that writes, installs, or reaches the network is asked about as the same command would be), the sandbox, the timeout, and the output bound. The result is run_command’s: the exit status and bounded output. Every call is audited as tool.call, and its command as classifier.verdict and command.outcome (logging.md).

Checking

A bad definition makes the config malformed, and every command says which tool and why:

Error: malformed ~/.wisp/config.json: custom tool 'issue': the command uses {num}, which is not a declared argument

wisp tools lists the tools a session gets, custom ones last; wisp tools --markdown shows each one’s limits and an example prompt, as the MCP resource wisp://tools.md does.