Agent Tools (Custom clientTools)
The qelos agent command can expose client-side tools to the AI model.
These tools run locally on the machine where you execute the CLI (not on the server), and are only used in:
- streaming mode:
qelos agent ... --stream - interactive mode:
qelos agent ... --interactive
Tools are implemented in tools/cli/services/agent/tools.mjs and executed from the stream loop in tools/cli/controllers/agent.mjs.
Enabling tools
Tools can be enabled from two places:
- CLI flag
--tools(built-in tools only) - Config
qelos.config.json→agents[agentNameOrId].clientTools- can include built-in tool names
- can include custom tool objects
The CLI merges both lists and deduplicates by tool name.
Built-in tools (quick reference)
Built-in tools are predefined in BUILTIN_TOOLS.
| Tool | Description | Args |
|---|---|---|
bash | Run a shell command | { command: string } |
node | Run Node.js code | { code: string } |
read | Read a file | { path: string, startLine?: number, endLine?: number } |
write | Write a file | { path: string, content: string } |
writeInLine | Insert content at line | { path: string, line: number, content: string } |
removeLines | Remove line range | { path: string, startLine: number, endLine: number } |
Creating custom tools (via clientTools)
A custom tool is an object inside agents[...].clientTools.
At minimum, it must have:
name(required)
In practice, you should also provide:
description(recommended): shown to the model to help it decide when to call the toolpropertiesorschema: describes the tool arguments the model should passhandler: tells the CLI how to execute the tool locally
Custom tool object shape
{
"name": "<toolName>",
"description": "<what the tool does>",
"properties": {
"<argName>": {
"type": "string",
"description": "..."
}
},
"handler": {
"bash": "<bash script/command>",
"injectArgsAs": "env"
}
}The CLI reads this object in buildClientTools(...) and:
- Builds the JSON schema sent to the backend:
- If you provide
schema, it uses it as-is. - Else if you provide
properties, it builds:schema = { "type": "object", "properties": <properties> }
- If you provide
- Builds a runnable handler:
- If
handleris an object with abashkey, the CLI creates a handler usingcreateCustomToolHandler(name, handler).
- If
The handler (bash-backed)
Currently, the supported config-based custom handler is:
"handler": {
"bash": "...",
"injectArgsAs": "env" | "argv" | "both"
}handler.bash
This is the bash command/script that will be executed locally using:
shell: '/bin/bash'cwd: process.cwd()
So paths are relative to where you run qelos agent.
You can point it to an executable script:
"bash": "./scripts/my-tool.sh"Or run an inline bash command (be careful with quoting):
"bash": "echo \"Hello\""handler.injectArgsAs
Controls how the tool-call arguments are passed to your bash script.
1) injectArgsAs: "env" (default)
Each argument is injected as an uppercase environment variable.
Example tool call args:
{ "path": "README.md", "pattern": "TODO" }Your script receives:
PATH=README.mdPATTERN=TODO
In bash:
#!/usr/bin/env bash
set -euo pipefail
grep -n "$PATTERN" "$PATH"2) injectArgsAs: "argv"
The CLI appends one argument to your bash command containing the tool args as JSON.
Important detail: the CLI double-stringifies the JSON:
- it appends
JSON.stringify(JSON.stringify(args))
So the argv value looks like a JSON string containing JSON.
If you want to parse it in bash, a common pattern is:
#!/usr/bin/env bash
set -euo pipefail
# $1 is a JSON string (that itself contains JSON)
ARGS_JSON=$(node -e 'console.log(JSON.parse(process.argv[1]))' "$1")
# Now parse fields from ARGS_JSON (using node/jq/etc.)
PATH_VALUE=$(node -e 'const a=JSON.parse(process.argv[1]); console.log(a.path)' "$ARGS_JSON")
echo "path=$PATH_VALUE"If you’re already using jq, you can also do:
ARGS_JSON=$(node -e 'console.log(JSON.parse(process.argv[1]))' "$1")
echo "$ARGS_JSON" | jq .3) injectArgsAs: "both"
Uses both env and argv injection.
Full example: custom tool in qelos.config.json
This example adds a tool named grepFile that searches for a pattern inside a file.
1) Create the script
Create ./scripts/grep-file.sh:
#!/usr/bin/env bash
set -euo pipefail
# Injected by the CLI (injectArgsAs: env)
# - PATH: file path
# - PATTERN: search pattern
grep -n "$PATTERN" "$PATH"Make it executable:
chmod +x ./scripts/grep-file.sh2) Register the tool in config
In qelos.config.json:
{
"agents": {
"code-wizard": {
"stream": true,
"clientTools": [
{
"name": "grepFile",
"description": "Search for a text pattern in a file and return matching lines.",
"properties": {
"path": { "type": "string", "description": "Path to the file to search" },
"pattern": { "type": "string", "description": "Text/regex to search for" }
},
"handler": {
"bash": "./scripts/grep-file.sh",
"injectArgsAs": "env"
}
}
]
}
}
}3) Run the agent
qelos agent code-wizard --stream --message "Search for TODOs in README.md using grepFile"If the model decides to call grepFile, the CLI will execute your script locally and feed the tool output back into the conversation.
Notes / gotchas
- Security: custom tools execute locally. Treat prompts as code execution requests.
- Name collisions: if a custom tool uses the same name as a built-in tool, the last one inserted into the tool map wins.
- Output truncation: the CLI truncates tool output in the terminal unless you pass
--verbose. - Argument casing: env injection uppercases keys (
path→PATH).
