Skip to main content

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 CaseExample
Internal API queriesFetch deployment status from your CI/CD system
Domain-specific operationsCompile a design token file from Figma API data
Custom servicesTrigger a database backup, invalidate a CDN cache
Business logic validationCheck if a proposed change violates a business rule
Workflow automationCreate 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

FieldRequiredTypeDescription
descriptionYesstringTells the LLM when and why to use this tool. Be specific.
commandYesstringThe shell command to execute. Supports {parameter} interpolation.
parametersNoobjectJSON Schema describing the tool's parameters.
permissionNostring"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:

  • enum for fixed sets of values.
  • pattern for format validation.
  • minimum/maximum for 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.