soldermaskdocs

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]
FlagDefaultMeaning
promptwhat board to build
--design PATHskip the model, load a design JSON
--out DIRbuildwhere the Gerbers, the KiCad file and the pictures go
--seed Nthe project's rules.seed, else 1anneal 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-onlywith --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-onlyresolve 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|krtkrtwhich 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-simskip the ngspice simulation; the board says it was not simulated
--no-pourroute ground as traces instead of pouring it; written into the block as rules.pour: false
--no-precedentplace from scratch even when the corpus holds a board like this one (still listed); written as rules.precedent: false
--no-markleave 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-renderskip the KiCad raytrace and GLB (DRC still runs, and is the part worth having)
--qty N1 10 25 100build quantity to price and stock-check; repeatable
--effort LEVELhighmodel effort (Claude)
--repair N0feed the pipeline's findings back to the model and revise, up to N times
--wirea 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|claudegeminiwhich model designs the board
--model IDoverride the backend's default model id
--project NAMEalso save into this project as a new version (created if missing)
--from NAMEstart from an existing project's current version: the prompt becomes an edit
--version, -Vprint 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 design

The 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 --parts

Takes 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]
FlagMeaning
--port, --hostbind address; default 0.0.0.0:8443
--httpplain HTTP. Pages load, but the phone's tilt control needs a secure origin and will not work
--token Xthe access token other devices must present; default: generated once into .cache/access-token
--no-authno token — anyone on the network can use the app
--proxybehind a reverse proxy: trust X-Forwarded-*, and require the token from every client, loopback included
--workers Nhow many builds may run at once; each runs the router
--log-file PATHappend logs there instead of stderr
--debugverbose 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]

See Versioning and releases.

Docs

soldermask docs            # build docs/ into site/ (served by the app at /docs)
soldermask docs serve      # mkdocs live-reload server for editing

MCP 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 request

See 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 counted

Described 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.

On this page