Skip to content

Add a custom tool

The agent can call tools you define. A custom tool is a plain LangChain tool you build in a JavaScript config and hand to the agent through the top-level tools array — your own schema, your own code, whatever API you want behind it.

The main use case: let the agent log work to Jira

Section titled “The main use case: let the agent log work to Jira”

Goal: give the agent a jira_log_work tool so a coding session can log time against a Jira issue for you.

A tool that runs your own code lives in a JavaScript config (.gsloth.config.mjs) — the tools array carries live tool instances, which a JSON config can’t express. Build the tool with LangChain’s tool() and a zod schema, then return it from configure():

.gsloth.config.mjs
import { tool } from '@langchain/core/tools';
import { z } from 'zod';
const jiraLogWork = tool(
async ({ jiraId, timeInSeconds, comment }) => {
const auth = Buffer.from(
`${process.env.JIRA_EMAIL}:${process.env.JIRA_API_TOKEN}`
).toString('base64');
const res = await fetch(
`${process.env.JIRA_BASE_URL}/rest/api/2/issue/${jiraId}/worklog`,
{
method: 'POST',
headers: { Authorization: `Basic ${auth}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ timeSpentSeconds: timeInSeconds, comment }),
}
);
if (!res.ok) {
return `Failed to log work to ${jiraId}: ${res.status} ${await res.text()}`;
}
return `Logged ${timeInSeconds}s against ${jiraId}`;
},
{
name: 'jira_log_work',
description:
'Log work time to a Jira issue. Example: jira_log_work({ jiraId: "PROJ-42", timeInSeconds: 3600, comment: "Implemented feature X" })',
schema: z.object({
jiraId: z.string().describe('The Jira issue ID (e.g. "PROJ-42")'),
timeInSeconds: z.number().describe('Time spent in seconds'),
comment: z.string().optional().describe('Work log comment'),
}),
}
);
export async function configure() {
const anthropic = await import('@langchain/anthropic');
return {
llm: new anthropic.ChatAnthropic({ model: 'claude-sonnet-4-5' }),
tools: [jiraLogWork],
};
}

Three parts make this a tool the agent can actually call:

  • name — how the model refers to the tool, and what shows up in the run’s tool list. Keep it stable.
  • schema — a zod object; each .describe() tells the model what to pass. The agent fills these arguments from the conversation.
  • the function body — arbitrary JS. Return a string, and that string goes back to the model as the tool result. Read secrets from the environment as above — don’t hardcode them in a config file the agent can read.

Then point the agent at it:

Terminal window
export JIRA_BASE_URL="https://your-org.atlassian.net"
export JIRA_EMAIL="you@your-org.com"
export JIRA_API_TOKEN="…"
gth code "Log 1 hour against PROJ-42 with the comment 'pairing on the auth bug'"

The agent sees jira_log_work in its toolset, calls it with { jiraId: "PROJ-42", timeInSeconds: 3600, comment: "pairing on the auth bug" }, and reports the result string back to you.

A tool in the top-level tools array is available to every command that runs the agent (ask, chat, code, exec, pr, review). To restrict a run to specific tools by name, list them in allowedTools.

If your “tool” is really just a command line, you don’t need JavaScript. The customTools config key wraps a shell command as a tool and works in a plain JSON config. Each entry is a name → { command, description }, with optional ${...} parameters:

{
"llm": { "type": "anthropic", "model": "claude-sonnet-4-5" },
"customTools": {
"run_migration": {
"command": "npm run migrate -- ${migrationName}",
"description": "Run a specific database migration",
"parameters": {
"migrationName": { "description": "Name of the migration to run" }
}
}
}
}

Parameter values are validated before the command runs: absolute paths, .., and shell metacharacters (|, &, ;, backticks, $, …) are rejected unless you opt out per parameter with "allow": ["shell-injection"] (also "absolute-paths", "directory-traversal", "null-bytes"). Add "timeout": <seconds> to kill a command that runs too long. customTools works across commands by default; set commands.<cmd>.customTools to override it for one command, or false to disable it there.

The command inherits Gaunt Sloth’s environment unchanged by default, API keys included, so a customTools entry that runs gth itself or calls a model API works without extra setup. It also means a command that prints its environment puts those keys into the tool result, which goes to the model provider and into session history. To remove credentials from the command’s environment, set commandEnv.scrubCredentials and list the variables the command still needs in commandEnv.passthrough.

Terminal window
# Log work through the agent during a coding session
gth code "Log 1 hour against PROJ-42, comment 'pairing on the auth bug'"
# Reach the same jira_log_work tool from a one-shot question
gth ask "Log 15 minutes against PROJ-42 for the standup"
# Drive a wrapped shell-command tool (customTools)
gth code "run the add_users_table migration"
  • Every tool knob — built-in tools, customTools, allowedTools, filesystem access: Tools configuration.
  • Connecting an external service that already speaks the Model Context Protocol, instead of writing a tool yourself: MCP servers.
  • The JavaScript config format (configure(), providers, middleware): Configuration.