Writing designs with Claude Code
The workbench loads your designs alongside the built-ins. Each .py file in
your designs folder becomes one antenna under “Your designs” (the user.*
namespace) — drop a file in, refresh the page, and it shows up. Because each
design is just a small Python class, Claude Code
can write or edit one for you from a contract that’s seeded right into the folder.
Your designs folder
Section titled “Your designs folder”On first run the app creates ~/.antennaknobs/designs/ (override with the
ANTENNAKNOBS_USER_DIR environment variable) and seeds two reference files into
it:
TEMPLATE.py— a complete, working example dipole. Copy it, rename it, edit it.CLAUDE.md— the authoring contract, written for Claude Code. It’s the context that lets Claude write a correct design on the first try.
Both are refreshed from the package on every startup (they’re documentation, not
your content), so an upgrade brings the latest authoring guidance. Your own
*.py files are never touched.
A design is also a first-class CLI antenna: once my_dipole.py is in the folder,
antennaknobs draw --builder user.my_dipole (or sweep, pattern, …) works,
and antennaknobs list shows it.
The workflow
Section titled “The workflow”- Open Claude Code in the folder:
Claude reads the seeded
Terminal window cd ~/.antennaknobs/designsclaudeCLAUDE.mdas context automatically. - Ask for an antenna in plain language — for example:
- “Make me a 40-meter off-center-fed dipole fed 1/3 from one end.”
- “Design a 2-element 20-meter quad loop — driven element plus a reflector.”
- “Take my_dipole.py and add an adjustable height-above-ground slider.”
- “My design loads but resonates at 32 MHz — shorten it to hit 28.5 MHz.”
- Refresh the web page. A design file is a Python program that runs on your
computer, so the first time it appears under “designs need your OK to
run” — click it to see what it does, then Allow it to run (choose
Allow + my edits for a design you’ll keep editing, so it won’t ask again
each time you save). It then shows under “Your designs.” From the command
line the same gate is
antennaknobs allow user.my_dipole. If instead it lands in the “designs that failed to load” panel, that’s a real error — the panel shows the file, error, and line number; paste that back to Claude and iterate. - Adjust it with the knobs, watching the pattern plots and the impedance readout, until the resonance and pattern look right.
The contract (what Claude follows)
Section titled “The contract (what Claude follows)”The seeded CLAUDE.md is authoritative; in brief, a design file must:
- Be named
lowercase_with_underscores.py— the name becomes the antenna’s name (my_dipole.py→user.my_dipole). - Define a class
BuildersubclassingAntennaBuilder. - Provide a
default_paramsmapping (every key becomes a knob, read asself.<key>in the build) and abuild_wires(self)method returning(start, end, n_segments, feed)tuples — writeNoneforn_segments(the framework meshes at the design density; never hand-assign counts) and declare adesign_freqparam. Exactly one wire carries the feed1 + 0j, the restNone. - Import only from
antennaknobsand the standard library.
from antennaknobs import AntennaBuilderfrom antennaknobs.network import Wire
class Builder(AntennaBuilder): default_params = { "design_freq": 14.1, # MHz — the band this is cut for "freq": 14.1, # MHz — measurement frequency "length_factor": 0.96, "height": 10.0, # metres "ui_params": {"default_view": "xz"}, }
def build_wires(self): wavelength = self.design_wavelength # = C_LIGHT_MHZ_M / design_freq, in metres h = (wavelength / 4.0) * self.length_factor z, eps = self.height, 0.01 return [ Wire((0.0, -h, z), (0.0, -eps, z)), # left arm Wire((0.0, eps, z), (0.0, h, z)), # right arm Wire((0.0, -eps, z), (0.0, eps, z), ex=1 + 0j), # driven feed gap ]For path-shaped geometry (loops, vees, rhombics), build_wires can fly a
Drone instead of computing corners by hand —
return drone.wires(). Arrays of identical elements have dedicated builders
(Array1x2Builder, Array2x2Builder, …); reach for those only once a single
element works.