Custom Tools
Custom tools let you define your own callable functions that the LLM can invoke during a session. They extend the built-in tool set with project-specific operations, internal API integrations, and domain-specific actions.
When to Use Custom Tools
Custom tools are useful when the built-in tools are insufficient:
| Use Case | Example |
|---|---|
| Internal API queries | Fetch deployment status from your CI/CD system |
| Domain-specific operations | Compile a design token file from Figma API data |
| Custom services | Trigger a database backup, invalidate a CDN cache |
| Business logic validation | Check if a proposed change violates a business rule |
| Workflow automation | Create a Jira ticket, send a Slack notification |
Defining Custom Tools
Custom tools are defined in the opencode.json configuration file under the custom_tools key.
Basic Structure
{
"custom_tools": {
"get-deployment-status": {
"description": "Get the current deployment status from Vercel",
"command": "vercel status --json",
"permission": "allow"
},
"invalidate-cdn-cache": {
"description": "Purge the CDN cache for a given path",
"command": "curl -X POST https://api.fastly.com/service/xxx/purge/{path}",
"parameters": {
"path": {
"type": "string",
"description": "The URL path to purge (e.g. /api/v1/*)"
}
},
"permission": "ask"
}
}
}
Schema
| Field | Required | Type | Description |
|---|---|---|---|
description | Yes | string | Tells the LLM when and why to use this tool. Be specific. |
command | Yes | string | The shell command to execute. Supports {parameter} interpolation. |
parameters | No | object | JSON Schema describing the tool's parameters. |
permission | No | string | "allow", "ask", or "deny". Defaults to "ask". |
Parameters
When parameters is defined, the LLM must provide values for each parameter before invoking the tool. Parameters use JSON Schema syntax:
{
"parameters": {
"service": {
"type": "string",
"description": "The service name to restart (e.g. api, worker, web)",
"enum": ["api", "worker", "web", "cron"]
},
"environment": {
"type": "string",
"description": "Target environment",
"enum": ["staging", "production"],
"default": "staging"
},
"force": {
"type": "boolean",
"description": "Skip confirmation prompts",
"default": false
}
}
}
The command template references parameters with {name} syntax:
{
"command": "deployctl restart --service {service} --env {environment}"
}
If force is true, the command becomes:
{
"command": "deployctl restart --service {service} --env {environment} --force"
}
Advanced Examples
Database Query Tool
{
"custom_tools": {
"run-database-query": {
"description": "Execute a read-only SQL query against the production database. Returns results as JSON. Never use for writes.",
"command": "psql $DATABASE_URL -c {query} --json",
"parameters": {
"query": {
"type": "string",
"description": "The SQL query to execute. Must be a SELECT statement."
}
},
"permission": "ask"
}
}
}
Jira Ticket Lookup
{
"custom_tools": {
"get-jira-ticket": {
"description": "Fetch details of a Jira ticket by its key (e.g. PROJ-1234). Returns summary, status, assignee, and description.",
"command": "curl -s -u $JIRA_AUTH 'https://jira.company.com/rest/api/2/issue/{ticket_key}' | jq '{key: .key, summary: .fields.summary, status: .fields.status.name, assignee: .fields.assignee.displayName, description: .fields.description}'",
"parameters": {
"ticket_key": {
"type": "string",
"description": "The Jira ticket key (e.g. PROJ-1234)",
"pattern": "^[A-Z]+-\\d+$"
}
},
"permission": "allow"
}
}
}
Slack Notification
{
"custom_tools": {
"send-slack-message": {
"description": "Send a message to a Slack channel. Use this to notify the team about deployments, failures, or important updates.",
"command": "curl -s -X POST -H 'Content-Type: application/json' -d '{\"channel\": \"{channel}\", \"text\": \"{message}\"}' $SLACK_WEBHOOK_URL",
"parameters": {
"channel": {
"type": "string",
"description": "The Slack channel name (with or without # prefix)",
"pattern": "^#?[a-z0-9_-]+$"
},
"message": {
"type": "string",
"description": "The message text to send"
}
},
"permission": "ask"
}
}
}
Permission Pairing
Every custom tool must have an explicit permission setting. This prevents accidental exposure of dangerous operations.
Safe Default
If no permission is specified, the custom tool defaults to "ask", requiring approval before every invocation:
{
"custom_tools": {
"my-tool": {
"description": "...",
"command": "...",
"permission": "ask"
}
}
}
Allow with Caution
Use "allow" only for read-only operations that have no side effects:
{
"custom_tools": {
"get-build-status": {
"description": "Check the latest CI build status",
"command": "curl -s https://ci.example.com/projects/my-project/latest",
"permission": "allow"
}
}
}
Deny for Safety
Use "deny" to register a tool but prevent its use in the current configuration. This is useful when defining tools for future use or when sharing configuration templates:
{
"custom_tools": {
"delete-production-data": {
"description": "Permanently delete data from production",
"command": "dangerctl delete --confirm",
"permission": "deny"
}
}
}
Combining with Agent Permissions
Custom tools respect agent-level permission overrides. You can restrict which agents can use which custom tools:
{
"agent": {
"deploy-agent": {
"description": "Handles deployments",
"mode": "subagent",
"permission": {
"custom_tools": {
"get-deployment-status": "allow",
"invalidate-cdn-cache": "allow",
"send-slack-message": "allow",
"run-database-query": "deny"
}
}
}
}
}
Best Practices
Be Specific in Descriptions
Write clear, detailed descriptions so the LLM knows exactly when to use the tool:
Good: "Fetch the latest deployment status from Vercel for a given environment.
Returns the build ID, status (building/ready/error), and deploy URL."
Bad: "Get deployment status"
Validate Parameters
Use JSON Schema constraints to prevent invalid inputs:
enumfor fixed sets of values.patternfor format validation.minimum/maximumfor numeric ranges.
Use Environment Variables
Store secrets and configuration in environment variables, not in the tool definition:
{
"command": "curl -s -H 'Authorization: Bearer $API_TOKEN' https://api.example.com/status"
}
Keep Commands Simple
Complex commands are harder to debug. If a command is more than a few piped operations, consider wrapping it in a script and calling the script from the custom tool:
{
"command": "bash scripts/deploy-check.sh {environment}"
}
Test Permissions
Start with "ask" for every custom tool. After verifying correct behavior, promote frequently used tools to "allow" and restrict others as needed.