Command line
soldermask is installed by uv tool install git+https://github.com/ashwinsk01/soldermask — the repository is private while in alpha, so this needs access to it (request access) — or by pip install -e . from a checkout; python -m soldermask is the same thing.
Build a board
soldermask "<prompt>" [options]
soldermask --design design.json [options]| Flag | Default | Meaning |
|---|---|---|
prompt | what board to build | |
--design PATH | skip the model, load a design JSON | |
--out DIR | build | where the Gerbers, the KiCad file and the pictures go |
--seed N | the project's rules.seed, else 1 | anneal seed; the same design, pins and seed is the same placement. Given, it is written into the block so the version records it |
--place-only (--no-route) | stop after placing: the board is laid out and nothing is routed | |
--route-only | with --from: route that version's placement again. Its parts and positions are kept (the parts pinned by code onto a copy of the design, the positions handed to the placer as fixed, so the anneal has nothing to move) and the router runs on them as the constraints now stand. One realise() call like every build; a new version with --project | |
--sim-only | resolve and simulate, nothing placed or routed: the sim section and sim.json in --out, no version. With --from, the parts are the version's own | |
--router freerouting|krt | krt | which router lays the copper first, each at its own rule (A* 0.127/0.127 mm, freerouting 0.15/0.15, since 13 Sep 2026); the other gives a second opinion on a board left short of a pin. Saved with --project as constraints.rules.router, so rebuilds keep it |
--no-sim | skip the ngspice simulation; the board says it was not simulated | |
--no-pour | route ground as traces instead of pouring it; written into the block as rules.pour: false | |
--no-precedent | place from scratch even when the corpus holds a board like this one (still listed); written as rules.precedent: false | |
--no-mark | leave the maker's mark off the silkscreen — the Gerber, the .kicad_pcb and the viewer alike; written as rules.mark: false, so a rebuild keeps it off | |
--no-render | skip the KiCad raytrace and GLB (DRC still runs, and is the part worth having) | |
--qty N | 1 10 25 100 | build quantity to price and stock-check; repeatable |
--effort LEVEL | high | model effort (Claude) |
--repair N | 0 | feed the pipeline's findings back to the model and revise, up to N times |
--wire | a second model turn after the parts resolve: each part as ordered, with the exact names its pins answer to, in front of the model, and the nets written against those. One more call; a pin the part does not have cannot be written. wire.json in --out says what it changed | |
--backend gemini|claude | gemini | which model designs the board |
--model ID | override the backend's default model id | |
--project NAME | also save into this project as a new version (created if missing) | |
--from NAME | start from an existing project's current version: the prompt becomes an edit | |
--version, -V | print the version and exit |
The stages print in order — parts, ERC, sim, placement, DSN, routing, fab, kicad, drc/render (with KiCad), stock, 3D, saved — then board, the board itself, then VERDICT, then what this board does NOT guarantee.
board is the picture the home page's disc draws — outline, pads, copper and silk as lines on the terminal's own background, seen from the front — printed only for a person watching (colour on). In Ghostty, Kitty, WezTerm and iTerm2 it is an image inline, inked light or dark to suit the terminal's ground; anywhere else it is braille, two dots across a cell and four down, which shows the outline, the pads and the copper and not a 0.5 mm pitch. Under tmux and into a pipe there is no picture. The KiCad raytrace is still written to --out as <board>-3d.png; this is not it.
Colour is on when stdout is a terminal and off when it is not, NO_COLOR turns it off and FORCE_COLOR on; the orange is 24-bit where COLORTERM says the terminal has it and the nearest of the 256 colours where it does not. The banner prints when the window is 84 columns or wider and is one line otherwise.
One stage at a time
The build is one sequence, workspace.realise(), and the switches above are switches on it rather than a second entry point for each stage — the command line ran its own copy of the sequence for months and it drifted four times. What they give you:
soldermask --design d.json --place-only # resolve, simulate, place; no copper
soldermask --from ldo --route-only --project ldo # ldo's current placement, routed again
soldermask --from ldo --sim-only --out sim # ldo's netlist simulated, nothing else
soldermask --design d.json --seed 4 # a different placement of the same designThe router is deterministic: --route-only on an unchanged version reproduces its copper, and is for re-routing the same placement after something the router reads has changed — a track drawn by hand, a track floor, a part sent to the back, the pour switched off — without the anneal moving anything. --seed is the placer's and reaches nothing in a --route-only build. The app has the same three on the chain tab: Simulate, Place again, Route again. Exit status is 1 when ERC found errors or the KiCad file failed its frame check, 2 for missing credentials, 0 otherwise.
VERDICT is the pass rule, repair.passes(), applied to the board that was just built — the same rule the evaluator scores pass@1 with, so what you are told and what the harness measures are the same number. It reads did this become a board: every part resolved, every net the design asked for still on it and every connection still on its net, no ERC errors, routed completely, and the copper still clean once ground is poured over it. It fails loudly on the ones that have no other symptom — a connection dropped in the resolver leaves nothing behind to be unrouted or to fail ERC, whether it took the net with it or the net went on without the pin. Until 16 Sep 2026 only the first was counted: a stepper driver whose motor-supply pin would not place passed with why: [], because VMOT still had its terminal and its capacitor.
It does not refuse to build, and the gaps list below it is unchanged. The two answer different questions: the gaps say what is wrong with this board, the verdict says whether it is a board.
The output directory gets design.json, the Gerbers and drill, BOM and CPL, the .dsn/.ses, board.svg, inventory.json, <board>.kicad_pcb with models/*.wrl, <board>.kicad_sch (the schematic; with KiCad installed the schematic line says whether KiCad's netlist of it is the design's and what its ERC found), inspector.html (the standalone viewer with the 3D models inlined), and repair.json when --repair ran.
Set up the machine
soldermask setup [--check]Prints where the data and cache directories are, whether Java, kicad-cli
and poppler's pdftotext (the datasheet reader's; without it sheets fetch
and nothing reads them) were found, and where the router's jar is. When the jar is missing it fetches
it from the freerouting release and refuses it unless the SHA-256 matches --
a truncated or substituted router is worse than an absent one, because it
would route, and every label it produced would claim to be 2.4.1's. --check
reports without fetching. Exit status is 1 when Java or kicad-cli is
still missing.
A checkout keeps the jar in vendor/ as it always has and setup leaves it
alone; an install puts it under .cache/vendor/. SOLDERMASK_FREEROUTING_JAR
overrides both, and the version recorded against every label follows whatever
jar it points at.
It fetches the KiCad symbol library the same way (3 MB from the archived
KiCad/kicad-symbols, whose master
has not moved since December 2021) and indexes it. Without it every pin is
resolved from the vendor's symbol alone: a board still builds, and can pass or
fail differently from one built where the library is. --astar also clones
the A* router at its pinned commit and builds its Rust core (git and cargo;
minutes rather than seconds) -- the router a build tries first.
The parts line says where parts will come from. A catalogue JLCPCB's API
wrote is used whatever its size. The jlcparts mirror is used when its build is
whole; a mirror naming under half of the 582,650 components of a whole build
(on 26 Sep 2026 it named 33,406) is refused as an answer, because part of a
catalogue cannot say a part is not in it, and every lookup goes to tscircuit's
jlcsearch instead -- the line says so.
soldermask setup --partsTakes the whole parts catalogue, so a build never reaches the network for a
part. With JLCPCB_APP_ID, JLCPCB_ACCESSKEY and JLCPCB_SECRETKEY in the
environment and the app granted the Parts API, it comes from JLCPCB itself:
the dump of the listing (7.25 million records, just under an hour), a detail
record for every code with stock (the parameters; a quarter of an hour), the
catalogue written from those under .cache/jlcparts/, and the parametric index
rebuilt over it. Both phases resume where they stopped. Without the API it
fetches the jlcparts mirror's files instead. --check says which source the
catalogue on disk came from and when, whether the app is granted the API, what
a record carries that this tree does not read, and the newest snapshot.
Projects
soldermask projects list
soldermask projects show <name>
soldermask projects fork <name> <new name>
soldermask projects revert <name> <vNNN>
soldermask projects rejudge [<name>] [--dry-run]list prints every project with versions, parts, ERC errors, cost and summary. show prints the version table with the current one starred. Names are matched by slug or display name.
rejudge runs kicad-cli's DRC again over every stored version's own .kicad_pcb and rewrites the verdict where the answer moved, one line a version. A verdict is written once, at build time, and two faults in how it was made (12 Sep 2026: the router's count read for the DRC's, and the copper judged before its corners were rounded) left versions on disk saying passed: true over a short their own file contains; a fix to the build does nothing for those. Only the verdict, a rejudged record and a note are rewritten — the pour row stays as the build's record — and the first board.json is kept beside it as board.json.before-rejudge, never overwritten. A version built before there was a verdict is left unjudged, with what DRC finds today said on its line; --dry-run prints the lines and writes nothing.
Serve
soldermask serve [--port 8443] [--host 0.0.0.0] [--http]
[--token X | --no-auth] [--proxy] [--workers 2] [--log-file PATH] [--debug]| Flag | Meaning |
|---|---|
--port, --host | bind address; default 0.0.0.0:8443 |
--http | plain HTTP. Pages load, but the phone's tilt control needs a secure origin and will not work |
--token X | the access token other devices must present; default: generated once into .cache/access-token |
--no-auth | no token — anyone on the network can use the app |
--proxy | behind a reverse proxy: trust X-Forwarded-*, and require the token from every client, loopback included |
--workers N | how many builds may run at once; each runs the router |
--log-file PATH | append logs there instead of stderr |
--debug | verbose logging |
python serve.py is a compatibility shim for the same command. See Running in production.
Version and releases
soldermask version [--json]
soldermask release note "<text>" [--kind Added|Changed|Fixed|Removed|Security|Deprecated]
soldermask release show
soldermask release bump major|minor|patch [--no-git] [--allow-empty] [--dry-run]Docs
soldermask docs # build docs/ into site/ (served by the app at /docs)
soldermask docs serve # mkdocs live-reload server for editingMCP server
soldermask mcp # the pipeline as tools on stdin/stdout, for another model to drive
soldermask mcp --tools # print the tool list and stop
soldermask mcp --model-calls # let resolving a part ask this tree's own models (a datasheet's
# pin table on a cache miss, Jev for a pin name); off by default,
# so the client is the only model in a run
soldermask mcp --http # the same server over HTTP with bearer keys, for a client that is
# not on this machine: --port 8787 --keys FILE --projects DIR
# --builds N --origin URL
soldermask mcp keys add NAME # issue a key (printed once; the file keeps its digest)
soldermask mcp keys list # every key's name, directory and dates
soldermask mcp keys revoke NAME # the key stops working on the next requestSee MCP server. Register the stdio server with claude mcp add soldermask -- soldermask mcp, or the same command in any client's mcpServers block; the HTTP one with claude mcp add --transport http soldermask https://HOST/mcp --header "Authorization: Bearer sm_...".
The routability corpus and model
soldermask learn status routes on disk, boards, how many routed clean; the shipped model
soldermask learn generate [--hours H] [--workers N] [--per-board 40] [--synthetic 400] [--seed S]
soldermask learn train [--epochs 60] [--keep rudy|linear|cnn] [--out PATH]
soldermask learn report the shipped model's provenance and held-out scores
soldermask learn relabel route again every example the router never countedDescribed in Routability. generate needs Java and the freerouting jar; train needs pip install -e ".[learn]" for torch, and fits the two logistic baselines without it. Set SOLDERMASK_CORPUS to keep the corpus somewhere other than .cache/corpus, and SOLDERMASK_ROUTABILITY to point the app at a model file other than the shipped one.
Evaluation and by-hand repair
python -m soldermask.evaluate [--backends gemini,claude] [--n 8] [--out eval] [--repair N] [--designs DIR] [--no-route]
python -m soldermask.repair --design PATH --out DIR [--prompt ""] [--route] [--reset]Described in Repair loop and evaluation.