Build & Push a Custom Tool
Built-in tools (Read, Glob, WebFetch, …) cover a lot, but eventually your
agent needs to do something specific — hit your internal API, transform data a
particular way, look something up. That's a custom tool: you write it once,
push it to Waxell, and any of the tenant's agents can reference it by name.
This is the companion to Ship a Claude Agent — same
wax CLI, one extra artifact.
1. Write the tool
A tool is a folder with a Python file and an optional TOOL.json manifest:
weather/
├── TOOL.json # name + metadata (optional)
└── weather.py # the tool source
weather.py — a @tool-decorated function. Type the params; the docstring
becomes the description the model sees:
from waxell_runtime import tool
@tool
def get_weather(ctx, city: str, units: str = "celsius") -> dict:
"""Get the current weather for a city. Units: celsius or fahrenheit."""
# Pure-Python logic + ctx.* helpers (domains, secrets, the LLM router).
# Do real I/O through a domain or ctx.domain(...) — see the v1 limits below.
return {"city": city, "temp": 21, "units": units, "conditions": "clear"}
TOOL.json (optional — --name and the folder name are fallbacks):
{
"name": "get_weather",
"display_name": "Get Weather",
"description": "Current weather for a city."
}
2. Push it
wax -p acme_prod push-tool ./weather
wax -p acme_prod push-tool ./weather --change-summary "add fahrenheit"
The control plane validates the source on push:
- No dangerous calls —
os.system,subprocess.*,eval,exec,__import__are rejected (the source runs sandboxed). - No third-party dependencies in v1 — keep it stdlib +
ctx. For external APIs, call them through a domain (ctx.domain(...)), which is the governed, per-user-auth boundary. See Ship a Claude Agent §4.
Confirm it registered:
wax -p acme_prod registry list --kind tool # or: wax agents info <agent>
3. Wire it into an agent
Reference the tool by name in the agent's waxell.yaml, then re-push the agent
(bump the version):
agents:
- name: trip_planner
version: 1.1.0 # bumped
framework: claude_agent_sdk
tools: [Read, WebFetch, get_weather] # built-ins + your custom tool
# ...
wax -p acme_prod push --include-source
wax -p acme_prod agents info trip_planner # confirm the tool shows under bindings
When the agent runs, the platform surfaces your tool to the model (for
claude_agent_sdk agents it appears under the MCP bridge) and executes it
host-side — so secrets and domains resolve under governance, not in the model.
Versioning & updates
push-tool promotes the new version to default unless you pass --no-default.
To iterate:
wax -p acme_prod push-tool ./weather --change-summary "fix rounding"
Each push is a new immutable version; the agent picks up the default. Use
--no-default to stage a version without promoting it.
Gotchas
- Bump the agent
version:when you re-push the agent after adding a tool. - v1 = pure Python, no deps. Need a library or a network call? Put that
behind a domain and call it with
ctx.domain(...)— that's also where per-end-user auth lives. - Don't hardcode the tool name in the system prompt — the platform may
expose it as
mcp__…__get_weather; tell the agent to "use the weather tool."
Next
- Ship a Claude Agent — the full publish flow.
@toolDecorator reference — params, approval gates, built-in tools.