Agent Tool Design skill for Codex
Tools a model picks right and calls right first time. Codex designs agent tools around user tasks rather than raw endpoints, writes descriptions that say when not to use a tool, and turns closed sets into enums under strict schemas. Outputs are capped and paginated with truncation hints, errors tell the model how to recover, destructive tools sit behind human confirmation, and a tool-selection eval proves the descriptions work.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- agent-tool-design Installs to ~/.agents/skills/agent-tool-design/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install agent-tool-design The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project agent-tool-design Installs to .agents/skills/agent-tool-design/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/agent-tool-design.zipunzip -o agent-tool-design.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) agent-tool-design PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $agent-tool-design, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Designs the tools, function schemas and MCP tools that LLM agents call - naming, descriptions, JSON Schema parameters with enums and strict mode, granularity, response shaping and pagination, actionable error messages, idempotency, read-only vs destructive annotations and confirmation gates - and evaluates tool selection accuracy. Use when writing function-calling definitions or MCP server tools, when an agent picks the wrong tool or passes bad arguments, when tool outputs flood the context window, or when reviewing a toolset before release.
Use it when
- You are writing function-calling or MCP tool definitions
- An agent picks the wrong tool or passes invalid arguments
- Tool results overflow the context window
- A toolset includes delete, send, pay or deploy actions
Use something else when
- You need the OpenAI-specific tool loop and parallel call mechanics: use openai-function-calling
- You are scaffolding a whole MCP server project: use mcp-server-builder
How to check the result
A tool-selection eval of 30 or more real requests reaches the target accuracy with stubbed execution, and every destructive tool requires confirmation in a test.
The mistake it prevents
Exposing dozens of overlapping, endpoint-shaped tools, then trying to fix wrong tool choices with longer system prompts instead of better descriptions.
Done well means: an agent with no extra prompting picks the right tool from its name and description, fills arguments correctly on the first try, gets back a compact result it can act on, recovers from errors because the message says what to do next, and cannot perform a destructive action without a confirmation step.
Tools are a user interface whose user is a model. Design them for that reader.
Workflow
- [ ] 1. List the user tasks the agent must complete (5-15 real requests)
- [ ] 2. Map each task to the fewest tool calls; that defines the toolset
- [ ] 3. Write names, descriptions and schemas (rules below)
- [ ] 4. Shape outputs: fields the next step needs, IDs for follow-ups, truncation hints
- [ ] 5. Write error messages that name the fix
- [ ] 6. Classify each tool: read-only, idempotent write, destructive/outward-facing
- [ ] 7. Build a tool-selection eval and run it; iterate on descriptions, not prompts
Granularity: design around tasks, not endpoints
| Smell | Better |
|---|---|
list_users, then get_user per ID, then get_orders per user | search_customers(query, include=["orders"]) |
One api_request(method, path, body) tool | Named tools per task with typed parameters |
| 60 tools exposed at once | 5-20 per agent; group by role or load per task (tool search, active tool subsets, MCP server per domain) |
| Two tools whose descriptions overlap (“search_docs”, “find_docs”) | Merge, or state crisply when to use each |
A model choosing between 40 similar tools makes more mistakes than one choosing among 8 distinct ones.
Names and descriptions
- Name:
verb_nounin snake_case, specific (create_calendar_event, notcalendar). Prefix by service when tools from several systems coexist (github_create_issue,linear_create_issue). - Description, in this order: what it does; when to use it (and when to use another tool instead); what it returns; important limits. 1-4 sentences.
- Put formats in the description and schema: “Dates are ISO 8601 (
2026-03-01)”, “IDs look likeORD-1234”.
{
"type": "function",
"name": "search_orders",
"description": "Search a customer's orders by status and date range. Use for questions about order history; use get_order for one known order ID. Returns at most `limit` orders, newest first, with order_id, status, total and created_at.",
"strict": true,
"parameters": {
"type": "object",
"properties": {
"customer_id": { "type": "string", "description": "Customer ID like CUS-123" },
"status": { "type": ["string", "null"], "enum": ["pending", "shipped", "delivered", "cancelled", null] },
"created_after": { "type": ["string", "null"], "description": "ISO date, inclusive" },
"limit": { "type": "integer", "description": "1-50, default 10 when unsure" }
},
"required": ["customer_id", "status", "created_after", "limit"],
"additionalProperties": false
}
}
Parameter schema rules
- Enums for every closed set. Free text where an enum fits is the top cause of bad arguments.
- With strict mode (OpenAI
strict: true), every property is required andadditionalProperties: false; express optional as a nullable type. Details:$openai-function-callingand$openai-structured-outputs. - Flat over nested. Two levels deep at most.
- No parameters the model cannot know (internal DB IDs it never saw, auth tokens, tenant IDs). Inject identity and tenancy server-side from the session; never accept them as arguments.
- Units in names:
timeout_seconds,amount_cents. - Validate again in the handler: schemas constrain shape, not authorisation or business rules.
Output shaping
| Rule | Why |
|---|---|
| Return only fields the next step needs | Context window is the agent’s working memory |
| Always include stable IDs for follow-up calls | Avoids a second lookup |
Paginate: limit param, return next_cursor and total | Large lists flood context |
Truncate long text with an explicit marker: "…[truncated 12,400 chars; call get_document(id, offset=4000)]" | Model knows more exists and how to get it |
Human-readable values over codes ("status": "shipped" not 3) | Fewer misreadings |
| Consistent shape across tools | Fewer parsing mistakes |
| Cap any tool result (e.g. 20k characters) | One huge result can push instructions out of context |
Tool output is untrusted input to the model: web pages, emails and tickets can contain
injected instructions. Mark it as data and see $prompt-injection-defense.
Error messages
Errors are returned to the model as tool results, so write them for recovery:
def get_order(order_id: str) -> dict:
if not ORDER_ID.fullmatch(order_id):
return {"error": "invalid_order_id",
"message": f"'{order_id}' is not an order ID. IDs look like ORD-1234; "
"use search_orders to find one by customer."}
order = repo.get(order_id, tenant=current_tenant())
if order is None:
return {"error": "not_found", "message": f"No order {order_id} for this customer."}
return order.to_agent_view()
- Say what was wrong and the next action. Never return a raw stack trace (leaks internals, wastes tokens).
- Distinguish retryable (
rate_limited,timeout, includeretry_after_seconds) from permanent errors so the agent stops retrying the impossible.
Safety classification
| Class | Examples | Required handling |
|---|---|---|
| Read-only | search, get, list | None beyond auth and tenancy |
| Idempotent write | upsert profile field, set status | Idempotency key or natural key; safe to retry |
| Destructive or outward-facing | delete, refund, send email/message, deploy, publish, pay | Human confirmation outside the model; show exact action and target first |
- MCP tools: set annotations
readOnlyHint,destructiveHint,idempotentHint,openWorldHinthonestly; clients use them for approval UX. Annotations are hints, so the server must still enforce permissions. - Split risky operations:
draft_email(safe) andsend_email(draft_id)(gated) is easier to approve and audit than onesend_email(to, body). - Destructive tools take explicit targets (
invoice_id), never filters like “all overdue”.
Evaluate tool selection
Build 30-100 cases of {request, expected_tool, key_args} from real tasks, including
near-misses between similar tools and requests that should use no tool. Run the agent with
tool execution stubbed, then score:
- tool selection accuracy (correct first tool),
- argument validity (schema + business validation pass),
- calls per task (fewer is better at equal success),
- refusals or confirmations on destructive cases.
Change descriptions and schemas, rerun, compare. Treat the toolset like an API: version it
and keep the eval in CI ($llm-evals).
Gotchas
- Descriptions that say “ALWAYS use this tool” make the model overuse it. State the condition.
- Optional parameters with silent defaults hide mistakes; make defaults explicit in the description.
- Returning success for partial failures (
{"ok": true}when 3 of 10 emails bounced) makes the agent report false completion. Return per-item results. - Timeouts: long tools should return a job ID and offer
get_job_status, not block for minutes. - Renaming a tool breaks saved conversations and evals; keep aliases during a transition.
Review checklist
- [ ] Every tool maps to a real user task; no overlapping pairs without "use X instead" text
- [ ] Names verb_noun, descriptions state when/when-not, return shape, limits
- [ ] Closed sets are enums; strict schemas where supported; no tenant/auth params
- [ ] Outputs capped, paginated, IDs included, truncation explained
- [ ] Errors actionable, retryable vs permanent marked
- [ ] Destructive/outward tools gated by human confirmation and take explicit targets
- [ ] Selection eval: accuracy >= target (e.g. 95%) on N cases, run in CI What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Agent Tool Design"
short_description: "Tools a model picks right and calls right first time"
default_prompt: "$agent-tool-design Our agent keeps calling search_docs when it should call get_ticket; fix the tool definitions" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.