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)
└── tool.py # the tool source — this filename matters
tool.pyThe loader looks for tool.py, falling back to any .py whose filename
contains tool. A file named weather.py is not found — the push succeeds,
the tool never registers, and the agent simply runs without it. Name it
tool.py.
tool.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 — 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. Declare it in your manifest and push
Declare the tool alongside your agent in waxell.yaml and push them together:
version: 1
tools:
- name: get_weather
version: "1.0.0"
description: Current weather for a city.
source: ./weather # the folder containing tool.py
agents:
- name: trip_planner
version: "1.1.0" # bump on every push — versions are immutable
framework: claude_agent_sdk
tools: [Read, WebFetch, get_weather] # built-ins + your custom tool
wax push --dry-run # validate first
wax push --include-source
--include-source is required — without it the tool is registered without its
code and cannot execute.
wax push-tool, for adapter agentswax push-tool writes to a different store than the one the model-facing tool
bridge reads. A tool pushed that way is invisible to claude_agent_sdk,
pydantic_ai, langgraph, crewai and autogen agents — it registers cleanly
and the model never sees it. Declaring the tool in waxell.yaml and pushing the
manifest is the path that works for every framework.
Confirm it registered:
wax registry list tool # `kind` is positional, not --kind
wax agents info trip_planner # the tool should appear under bindings
What the platform checks on push
- Obvious dangerous calls are rejected —
os.system(,subprocess.Popen,subprocess.run,subprocess.call,eval(,exec(,__import__. This is a substring scan, not a sandbox: treat it as a guardrail against accidents, not a security boundary. Tool source runs in the worker process, so only push code you trust. - No third-party dependencies in v1 — keep it stdlib +
ctx. For external APIs, call them through a domain (await ctx.domain(...)), which is the governed, per-user-auth boundary. See Domains.
3. How it runs
When the agent runs, the platform surfaces your tool to the model — for
claude_agent_sdk agents under the MCP bridge — and executes it host-side,
so secrets and domains resolve under governance rather than in the model.
Versioning & updates
Bump the tool's version: in waxell.yaml and push again:
tools:
- name: get_weather
version: "1.0.1" # bumped
source: ./weather
wax push --include-source
Each push is a new immutable version — re-pushing the same version is rejected
with 409 semver_conflict. Bump the agent's version too when you change
which tools it binds.
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.