Coyote is designed to be as flexible and as customizable as possible. One of the key features that enables this flexibility is the ability to create and integrate custom tools into your Coyote setup. This document provides a guide on how to create and use custom tools within Coyote.
Supported Languages
Coyote supports custom tools written in the following programming languages:
- Python
- Bash
- TypeScript
Creating a Custom Tool
All tools are created as scripts in either Python, Bash, or TypeScript. They should be placed in the functions/tools directory.
The location of the functions directory varies between systems, so you can use the following command to locate
your functions directory:
coyote --info | grep functions_dir | awk '{print $2}'
Once you've created your custom tool, remember to add it to the visible_tools array in your global config.yaml file
to enable it globally. See the Tools documentation for more information on how Coyote utilizes the
visible_tools array.
Sandbox Support
If you run Coyote inside a Sandbox, custom tools need to declare their external binary and network
dependencies in an sbx-mixin.yaml next to the tool. Otherwise the tool fails silently inside the sandbox even though
it works on your host.
Two layouts work:
Per-tool mixin: Co-located with the tool itself, applies whenever any sandbox is launched. Use this when the tool is meaningful as a portable unit you'd share via Sharing Configurations:
<coyote-config-dir>/functions/<your-tool-dir>/
├── tools.sh
└── sbx-mixin.yaml
Global custom-tools mixin: Covers all your custom tools at once. Use this when several tools share the same binary/domain needs and you don't want to duplicate:
<coyote-config-dir>/functions/sbx-mixin.yaml
Both are auto-discovered by Coyote on every coyote --sandbox. No flags or registration required.
Example: a custom tool that uses httpie
<coyote-config-dir>/functions/my-httpie-tool/
├── tools.sh
└── sbx-mixin.yaml
sbx-mixin.yaml:
schemaVersion: "1"
kind: mixin
name: my-httpie-tool
description: Adds httpie + access to example-api.com for the my-httpie-tool custom tool
network:
allowedDomains:
- "example-api.com:443"
commands:
install:
- command: |
sudo apt-get update
sudo apt-get install -y httpie
user: "1000"
description: Install httpie for use by my-httpie-tool
See Sandboxes: Extending the Sandbox for the full discovery path table and the official sbx mixin reference for the schema.
Sandbox caveats
- Custom tools that shell out to host-installed binaries fail in the sandbox unless those binaries are also installed by the tool's mixin (or a global mixin, or the base kit).
- Custom tools that hit external APIs fail on DNS resolution if the domain isn't in any
allowedDomainsblock. The sandbox proxy denies all unlisted traffic. - Mixin install steps run as UID 1000 (the
agentuser) with passwordless sudo. Don't assume root home paths; use~/or explicit/home/agent/if you need a known location. - When sharing a custom tool, ship its
sbx-mixin.yamlalongside it. Without it, recipients will hit silent sandbox failures with no obvious cause. See Sharing Configurations: Sandbox Implications for the security implications of a shared mixin.
Environment Variables
All tools have access to the following environment variables that provide context about the current execution environment:
| Variable | Description |
|---|---|
LLM_OUTPUT |
Indicates where the output of the tool should go. In certain situations, this may be set to a temporary file instead of /dev/stdout. |
LLM_ROOT_DIR |
The root config_dir directory for Coyote (i.e. dirname $(coyote --info | grep config_file | awk '{print $2}')) |
LLM_TOOL_NAME |
The name of the tool being executed |
LLM_TOOL_CACHE_DIR |
A directory specific to the tool for storing cache or temporary files |
LLM_TOOL_RAW_JSON |
The raw JSON envelope the LLM sent for this tool call, exactly as received. See Reading values via LLM_TOOL_RAW_JSON below. |
Coyote also searches the tools directory on startup for a .env file. If found, all tools in functions/tools/ will have
the environment variables defined in the .env file available to them.
Reading values via LLM_TOOL_RAW_JSON
Coyote exports the raw JSON envelope it received from the LLM as the LLM_TOOL_RAW_JSON environment variable on every
tool invocation. Tools can use this to read option values directly from the JSON rather than going through the
argc_* variables.
When to use it
Bash tools: This is the recommended pattern for any option that may carry large multi-line content, code, file
contents, or values dense with shell-significant characters (markdown table pipes, single quotes, em-dashes, etc.).
Coyote's bash dispatcher converts JSON to shell --option=<value> flags via jq and eval-s the result; for large or
special-character values, that shell-quoting round-trip can occasionally drop characters or misalign content before it
reaches argc_*. Reading from LLM_TOOL_RAW_JSON bypasses the shell layer entirely.
main() {
argc_contents="$(jq -r '.contents' <<< "$LLM_TOOL_RAW_JSON")"
argc_path="$(jq -r '.path' <<< "$LLM_TOOL_RAW_JSON")"
# ... rest of your tool logic using $argc_contents and $argc_path
}
This is the pattern Coyote's bundled fs_write, fs_patch, execute_command, execute_sql_code, and send_mail tools
use for their large-payload options. The argc # @option --foo! directives stay in your script so Coyote can build the
JSON schema for the LLM, but your main() reads from LLM_TOOL_RAW_JSON instead of trusting argc's value capture.
Python and TypeScript tools: Coyote's Python and TypeScript dispatchers parse the JSON envelope natively (json.loads
/ JSON.parse) and pass values directly to your run() function as native types. They don't go through shell quoting,
so the LLM_*_RAW_JSON escape hatch that bash tools need doesn't affect them. Declared parameters arrive in your
function correctly without needing LLM_TOOL_RAW_JSON.
Python and TypeScript tools may still want to read LLM_TOOL_RAW_JSON for other reasons:
- Accessing fields the LLM passed that aren't declared in your
run()signature (telemetry, optional metadata). - Auditing or logging the original LLM-sent JSON verbatim.
- Debugging when a value isn't what you expected.
# Python: parse the raw JSON when you need beyond-signature access
import json, os
payload = json.loads(os.environ["LLM_TOOL_RAW_JSON"])
extra_field = payload.get("extra_field")
// TypeScript: parse the raw JSON when you need beyond-signature access
const payload = JSON.parse(process.env.LLM_TOOL_RAW_JSON!);
const extraField = (payload as Record<string, unknown>).extra_field;
Agent-local tools
For tools written under <config_dir>/agents/<agent>/tools.sh (or .py / .ts), the same value is exposed as
LLM_AGENT_RAW_JSON, the raw JSON payload for the agent function call. The semantics are identical; only the variable
name differs.
Custom Bash-Based Tools
To create a Bash-based tool, refer to the custom bash tools documentation.
Custom Python-Based Tools
Coyote supports tools written in Python.
Each Python-based tool must follow a specific structure in order for Coyote to be able to properly compile and execute it:
- The tool must be a Python script with a
.pyfile extension. - The tool must have a
def runfunction that serves as the entry point for the tool. - The
runfunction must accept parameters that define the inputs for the tool.- Always use type hints to specify the data type of each parameter.
- Use
Optional[...]to indicate optional parameters
- The
runfunction must return astr.- For Python, this is automatically written to the
LLM_OUTPUTenvironment variable, so there's no need to explicitly write to the environment variable within the function.
- For Python, this is automatically written to the
- The function must also have a docstring that describes the tool and its parameters.
- Each parameter in the
runfunction should be documented in the docstring using theArgs:section. They should use the following format:<parameter_name>: <description>Where<parameter_name>: The name of the parameter<description>: The description of the parameter
- These are very important because these descriptions are what's passed to the LLM as the description of the tool, letting the LLM know what the tool does and how to use it.
- Each parameter in the
It's important to note that any functions prefixed with _ are not sent to the LLM, so they will be invisible to the LLM
at runtime.
Below is the demo_py.py tool definition that comes pre-packaged with
Coyote and demonstrates how to create a Python-based tool:
import os
from typing import List, Literal, Optional
def run(
string: str,
string_enum: Literal["foo", "bar"],
boolean: bool,
integer: int,
number: float,
array: List[str],
string_optional: Optional[str] = None,
integer_with_default: int = 42,
boolean_with_default: bool = True,
number_with_default: float = 3.14,
string_with_default: str = "hello",
array_optional: Optional[List[str]] = None,
):
"""Demonstrates all supported Python parameter types and variations.
Args:
string: A required string property
string_enum: A required string property constrained to specific values
boolean: A required boolean property
integer: A required integer property
number: A required number (float) property
array: A required string array property
string_optional: An optional string property (Optional[str] with None default)
integer_with_default: An optional integer with a non-None default value
boolean_with_default: An optional boolean with a default value
number_with_default: An optional number with a default value
string_with_default: An optional string with a default value
array_optional: An optional string array property
"""
output = f"""string: {string}
string_enum: {string_enum}
boolean: {boolean}
integer: {integer}
number: {number}
array: {array}
string_optional: {string_optional}
integer_with_default: {integer_with_default}
boolean_with_default: {boolean_with_default}
number_with_default: {number_with_default}
string_with_default: {string_with_default}
array_optional: {array_optional}"""
for key, value in os.environ.items():
if key.startswith("LLM_"):
output = f"{output}\n{key}: {value}"
return output
Custom TypeScript-Based Tools
Coyote supports tools written in TypeScript. TypeScript tools require Node.js and
tsx (npx tsx is used as the default runtime).
Each TypeScript-based tool must follow a specific structure in order for Coyote to properly compile and execute it:
- The tool must be a TypeScript file with a
.tsfile extension. - The tool must have an
export function run(...)that serves as the entry point for the tool.- Non-exported functions are ignored by the compiler and can be used as private helpers.
- The
runfunction must accept flat parameters that define the inputs for the tool.- Always use type annotations to specify the data type of each parameter.
- Use
param?: typeortype | nullto indicate optional parameters. - Use
param: type = valuefor parameters with default values.
- The
runfunction must return astring(orPromise<string>for async functions).- For TypeScript, the return value is automatically written to the
LLM_OUTPUTenvironment variable, so there's no need to explicitly write to the environment variable within the function.
- For TypeScript, the return value is automatically written to the
- The function must have a JSDoc comment that describes the tool and its parameters.
- Each parameter should be documented using
@param name - descriptiontags. - These descriptions are passed to the LLM as the tool description, letting the LLM know what the tool does and how to use it.
- Each parameter should be documented using
- Async functions (
export async function run(...)) are fully supported and handled transparently.
Supported Parameter Types:
| TypeScript Type | JSON Schema | Notes |
|---|---|---|
string |
{"type": "string"} |
Required string |
number |
{"type": "number"} |
Required number |
boolean |
{"type": "boolean"} |
Required boolean |
string[] |
{"type": "array", "items": {"type": "string"}} |
Array (bracket syntax) |
Array<string> |
{"type": "array", "items": {"type": "string"}} |
Array (generic syntax) |
"foo" | "bar" |
{"type": "string", "enum": ["foo", "bar"]} |
String enum (literal union) |
param?: string |
{"type": "string"} (not required) |
Optional via question mark |
string | null |
{"type": "string"} (not required) |
Optional via null union |
param = "value" |
{"type": "string"} (not required) |
Optional via default value |
Unsupported Patterns (will produce a compile error):
- Rest parameters (
...args: string[]) - Destructured object parameters (
{ a, b }: { a: string, b: string }) - Arrow functions (
const run = (x: string) => ...) - Function expressions (
const run = function(x: string) { ... })
Only export function declarations are recognized. Non-exported functions are invisible to the compiler.
Below is the demo_ts.ts tool definition that comes pre-packaged with
Coyote and demonstrates how to create a TypeScript-based tool:
/**
* Demonstrates all supported TypeScript parameter types and variations.
*
* @param string - A required string property
* @param string_enum - A required string property constrained to specific values
* @param boolean - A required boolean property
* @param number - A required number property
* @param array_bracket - A required string array using bracket syntax
* @param array_generic - A required string array using generic syntax
* @param string_optional - An optional string using the question mark syntax
* @param string_nullable - An optional string using the union-with-null syntax
* @param number_with_default - An optional number with a default value
* @param boolean_with_default - An optional boolean with a default value
* @param string_with_default - An optional string with a default value
* @param array_optional - An optional string array using the question mark syntax
*/
export function run(
string: string,
string_enum: "foo" | "bar",
boolean: boolean,
number: number,
array_bracket: string[],
array_generic: Array<string>,
string_optional?: string,
string_nullable: string | null = null,
number_with_default: number = 42,
boolean_with_default: boolean = true,
string_with_default: string = "hello",
array_optional?: string[],
): string {
const parts = [
`string: ${string}`,
`string_enum: ${string_enum}`,
`boolean: ${boolean}`,
`number: ${number}`,
`array_bracket: ${JSON.stringify(array_bracket)}`,
`array_generic: ${JSON.stringify(array_generic)}`,
`string_optional: ${string_optional}`,
`string_nullable: ${string_nullable}`,
`number_with_default: ${number_with_default}`,
`boolean_with_default: ${boolean_with_default}`,
`string_with_default: ${string_with_default}`,
`array_optional: ${JSON.stringify(array_optional)}`,
];
for (const [key, value] of Object.entries(process.env)) {
if (key.startsWith("LLM_")) {
parts.push(`${key}: ${value}`);
}
}
return parts.join("\n");
}
Custom Runtime
By default, Coyote uses the following runtimes to execute tools:
| Language | Default Runtime | Requirement |
|---|---|---|
| Python | python |
Python 3 on $PATH |
| TypeScript | npx tsx |
Node.js + tsx (npm i -g tsx) |
| Bash | bash |
Bash on $PATH |
You can override the runtime for Python and TypeScript tools using a shebang line (#!) at the top of your
script. Coyote reads the first line of each tool file; if it starts with #!, the specified interpreter is used instead
of the default.
Examples:
#!/usr/bin/env python3.11
# This Python tool will be executed with python3.11 instead of the default `python`
def run(name: str):
"""Greet someone.
Args:
name: The name to greet
"""
return f"Hello, {name}!"
#!/usr/bin/env bun
// This TypeScript tool will be executed with Bun instead of the default `npx tsx`
/**
* Greet someone.
* @param name - The name to greet
*/
export function run(name: string): string {
return `Hello, ${name}!`;
}
This is useful for pinning a specific Python version, using an alternative TypeScript runtime like Bun or Deno, or working with virtual environments.