kicad_skills

kicad_skills — a containerised circuit-design toolkit

CI pins KiCad 10.0.4 | 9.0.9 Python 3.13 License

eda is a command-line toolkit for circuit design work: reading datasheets, simulating analog circuits with ngspice, reviewing KiCad schematics and PCB artwork, and producing the fabrication package.

Everything runs inside a container. KiCad, ngspice and the Python dependencies are never installed on the host — the only host requirements are docker and bash. The KiCad release is a build argument, so it can be pinned per project.

It is an ordinary CLI that prints JSON and exits non-zero on errors, so it works the same from a terminal, a Makefile, a CI job, or a coding assistant. The repository also ships usage guides for each area — plain Markdown, worth reading on their own, and installable into whatever layout your assistant expects.

./bin/eda.sh doctor                                   # builds the image on first use
./bin/eda.sh report      hardware/ -o build/report    # everything, on one page
./bin/eda.sh sch review  hardware/ --text
./bin/eda.sh pcb review  hardware/ --text
./bin/eda.sh sim run     sim/filter.cir -o sim/out
./bin/eda.sh datasheet parse docs/datasheets/lm321.pdf -o /tmp/ds

Install

Into an existing board project, as a submodule

cd ~/projects/my-board
git submodule add https://github.com/sabas0ba/kicad_skills tools/kicad_skills
./tools/kicad_skills/bin/install-skills.sh

That drops a one-line bin/eda.sh shim so the ./bin/eda.sh … commands work verbatim from your project root, and — for Claude Code users — links the usage guides into the layout it discovers:

my-board/
├── bin/eda.sh                                    shim -> tools/kicad_skills/bin/eda.sh
├── .claude/skills/kicad-pcb-review/SKILL.md ->   ../../../tools/kicad_skills/docs/guides/kicad-pcb-review.md
├── hardware/my-board.kicad_pro
└── tools/kicad_skills/                           the submodule

Commit bin/eda.sh so everyone who clones the project gets it. Upgrading is git submodule update --remote — the symlinks follow, with no re-install.

Flag Effect
--no-guides just the CLI shim, nothing else
--dest DIR another tool’s directory (e.g. --dest .cursor/rules)
--copy vendor the guides instead of symlinking (Windows checkouts)
--force / --uninstall replace what is already there / reverse the install

The guides themselves need no install — they are Markdown in tools/kicad_skills/docs/guides/, readable as they are.

Then, from your project root:

./bin/eda.sh doctor                       # builds the image on first use (~5 min)
./bin/eda.sh report hardware/ -o build/report

Or standalone

No install step is needed to try it — clone and run:

git clone https://github.com/sabas0ba/kicad_skills && cd kicad_skills
./bin/eda.sh doctor            # {"kicad_cli": "10.0.4", "ngspice": "...", "ok": true}
./bin/eda.sh report ~/projects/my-board/hardware -o /tmp/report

bin/eda.sh mounts the git repository root that contains the current directory at /work, runs as your uid/gid (no root-owned files), and gives the container no network. To work on the toolkit itself, see AGENTS.md.

If you drive this clone with Claude Code, make skills mirrors the guides into the layout it discovers. That directory is generated and git-ignored — the guides live in docs/guides/.

What it does

flowchart LR
    PDF[datasheet PDF] --> DS[eda datasheet]
    DS -->|part values| SIM[eda sim]
    SIM -->|verified circuit| SCH[eda sch review]
    SCH -->|netlist + ERC| PCB[eda pcb review]
    PCB -->|DRC clean| FAB[eda pcb fab]
    SCH --> REP[eda report]
    PCB --> REP
    SIM --> REP
    REP --> HTML[report.html + PNG + PDF + GLB]
Area What it does One command Guide
Datasheets Text, parameter tables, embedded figures and rendered page images from a PDF eda datasheet parse lm321.pdf -o out/ guide
Simulation ngspice op/dc/ac/tran/noise, THD, Monte Carlo tolerance analysis, temperature sweeps — with measurements and plots eda sim run filter.cir -o out/ guide
Schematic review Components, nets and hierarchy from .kicad_sch; ERC plus decoupling / floating-input / annotation / pull-up checks eda sch review hardware/ --text guide
Board review DRC, schematic parity, track widths, drills, exact board-edge clearance, ground pour, silkscreen; current capacity, resistance and impedance from the stackup; layer plots and 3D renders eda pcb review hardware/ --text guide
Fabrication Gerbers, Excellon drill, pick-and-place, BOM, STEP/IPC-2581, zipped with a manifest eda pcb fab hardware/ -o fab/ guide
The container Build, pin, verify and troubleshoot the toolchain eda doctor guide

Examples

Review a board and see why — findings are structured, so they can be filtered, counted and gated on in CI; --text is the human digest.

$ ./bin/eda.sh pcb review hardware/ --text
## summary: error=1, warning=3, info=4

## findings
  ERROR   board.copper_outside_outline: 46 copper item(s) lie outside the board outline
  WARNING board.edge_clearance: 2 copper item(s) within 0.3 mm of the board outline
  WARNING layout.decoupling_distance [U3.14 / +3V3]: C12 is 8.4 mm away
  INFO    board.size: board outline is 63.5 x 40.6 mm

Exit code is 2 when a review found errors, so eda pcb review hardware/ is also a CI gate; -o report.json keeps the structured version.

Simulate before committing to a part value — the −3 dB corner, the phase margin and the tolerance spread come back as numbers, not a picture to squint at.

$ ./bin/eda.sh sim montecarlo sim/rc.cir -o sim/mc \
    --vary R1=1% --vary C1=10% --metric 'ac.v(out).f_minus_3db_hz' --trials 200
{"metric": "ac.v(out).f_minus_3db_hz", "nominal_metric": 997.7,
 "statistics": {"samples": 200, "mean": 1001.1, "stdev": 35.5,
                "p05": 940.1, "median": 1000.7, "p95": 1064.0, "spread_pct": 20.4},
 "sensitivity": {"explained_pct": 99.7, "parameters": [
     {"parameter": "C1", "contribution_pct": 99.3, "elasticity": -0.99},
     {"parameter": "R1", "contribution_pct":  0.7, "elasticity": -0.96}]},
 "failures": [], "histogram": "sim/mc/histogram.png", "csv": "sim/mc/trials.csv"}

The 10 % capacitor is the entire spread; the 1 % resistor is noise. That is the line item worth changing, and it came out of the trials that were already run.

Read a datasheet without opening a viewer — page images for the curves, tables for the numbers.

$ ./bin/eda.sh datasheet find docs/lm321.pdf "supply current"
[{"page": 3, "line": "Supply Current  IS  VS = 5 V, no load  0.43  0.75  mA"}]
$ ./bin/eda.sh datasheet pages docs/lm321.pdf -o out/ --pages 7 --dpi 200

One command for the whole project — see the worked example below.

./bin/eda.sh report hardware/ -o build/report --glb
open build/report/report.html

Pinning the KiCad version

KICAD_VERSION=10.0.4 make build     # default, current stable
KICAD_VERSION=9.0.9  make build     # a second image for older projects
KICAD_VERSION=9.0.9 ./bin/eda.sh pcb review board.kicad_pcb

Each version produces its own image tag (eda-toolkit:<version>) so several can coexist. Tags: https://hub.docker.com/r/kicad/kicad/tags. KiCad upgrades project files in place when it opens something older than itself, so match the version to the project.

CI runs the whole suite against both 10.0.4 and 9.0.9, because “the KiCad version is configurable” is a claim that has to be backed up. It is what caught these, all of which are handled at runtime rather than pinned away:

Adding a version is a digest in docker/kicad-digests.txt plus an entry in the workflow matrix — tests/test_pinning.py fails if the two disagree.

Worked example: a real board

Everything below is actual output from the Open Air Max demo project that ships with KiCad (210 footprints, 4 layers, a 3-sheet hierarchical schematic) — a far better test of “does this help?” than a two-resistor fixture. One command produced all of it:

./bin/eda.sh report openair-max/ -o build/report --dpi 150 --glb
build/report/
├── report.html          the page below, self-contained
├── report.md            same content, for a PR comment or a commit message
├── report.json          every finding, machine readable
├── bom.csv
├── schematic/           schematic.pdf + one PNG per sheet + contact sheet
└── board/               6 view plots, 4 per-layer plots, 3 renders,
                         contact-sheet.png, board.glb, the PDFs behind them

The board at a glance — 13 plots and renders tiled into one image, so “is anything on the wrong layer” is one look, not thirteen file-opens:

board contact sheet

3D, for the things no 2D plot shows — connector orientation, component collisions, which side is which. --glb additionally writes a board.glb that GitHub and every browser render interactively:

3D render

The schematic, rasterised per sheet plus a real PDF (sheet 1 of 3):

schematic sheet 1

And the verdict, abridged from report.md — note the collapsing: 199 identical drill violations are one line, not 199:

## Verdict
* schematic: 3 error, 19 warning, 4 info
* board: 8 error, 26 warning, 6 info
* 99.45 x 72.0 mm, 4 layers, 210 footprints, 156 nets, 410 vias

## Board findings
| severity | rule | where | message |
| --- | --- | --- | --- |
| error   | `drc.drill_out_of_range` | 199 locations | 199 occurrences. First: Hole size out of range (min hole 0.5080 mm; actual 0.4000 mm) |
| error   | `drc.clearance`          | 13 locations  | 13 occurrences. First: Clearance violation (clearance 0.1000 mm; actual 0.0910 mm) |
| error   | `drc.zones_intersect`    | Zone [BAT-] on F.Cu, priority 5 | Copper zones intersect (must have distinct priorities) |
| warning | `drc.parity.net_conflict`| Pad 6 of I2C3 on B.Cu | Pad net doesn't match net given by schematic (GND) |
| warning | `drc.silk_edge_clearance`| 8 locations   | 8 occurrences. First: Silkscreen clipped by board edge |

Simulation

The same “show, don’t describe” applies to analog work. An AC sweep of the RC reference circuit, and 200 Monte Carlo trials of the same circuit with 1 % resistors and 10 % capacitors:

sim run (AC) sim montecarlo
AC sweep Monte Carlo
{"nominal_metric": 997.7,
 "statistics": {"samples": 200, "mean": 1001.1, "stdev": 35.5,
                "p05": 940.1, "median": 1000.7, "p95": 1064.0, "spread_pct": 20.4}}

The 10 % capacitor sets the spread: ±1 % on the resistor barely moves it. That is the kind of answer that changes a BOM line.

Provenance and licensing of these images: docs/examples/README.md.

Command reference

eda doctor                                    tool versions in the environment
eda diff         OLD NEW -o DIR [--dpi 150] [--no-images]
eda report       TARGET -o DIR [--dpi 200] [--glb] [--simulation NETLIST]
                              [--no-3d] [--no-per-layer] [--no-bom] [--title T]

eda datasheet info   PDF
eda datasheet find   PDF QUERY... [--regex]
eda datasheet text   PDF [--pages 1-5] [--layout] [--ocr]
eda datasheet tables PDF [--pages 5]
eda datasheet images PDF -o DIR [--pages]
eda datasheet pages  PDF -o DIR [--pages] [--dpi 200]
eda datasheet parse  PDF -o DIR [--renders] [--ocr]

eda sim lint     NETLIST
eda sim run      NETLIST -o DIR [--no-plots] [--timeout S]
eda sim montecarlo NETLIST -o DIR --vary R1=1% --metric ac.v(out).f_minus_3db_hz [--trials N]
eda sim temperature NETLIST -o DIR [--temperatures -40 25 85] [--metric ...]
eda sim measure  RAW [--thd SIGNAL --fundamental HZ] [--skip S]
eda sim plot     RAW -o DIR [--signals ...]
eda sim netlist  SCHEMATIC -o FILE           export a SPICE deck from KiCad

eda sch info     TARGET [--no-cli]
eda sch review   TARGET [--text] [--collapse N] [-o report.json] [--no-cli]
eda sch bom      TARGET -o bom.csv [--group-by ...] [--fields ...]
eda sch erc      TARGET                      raw KiCad ERC JSON
eda sch netlist  TARGET [--format json|kicadxml|spice|...] [-o FILE]
eda sch render   TARGET -o DIR [--dpi 200]   PDF + one PNG per sheet + contact sheet
eda sch pdf      TARGET -o FILE              just the PDF

eda pcb info     TARGET
eda pcb review   TARGET [--text] [--collapse N] [--threshold KEY=VALUE] [-o report.json]
eda pcb fab      TARGET -o DIR [--step] [--ipc2581] [--pos-format csv]
eda pcb drc      TARGET [--no-parity]        raw KiCad DRC JSON
eda pcb render   TARGET -o DIR [--views ...] [--per-layer] [--no-3d] [--no-sheet]
                              [--glb] [--dpi 300]
eda pcb glb      TARGET -o FILE              3D model a browser can display
eda pcb electrical TARGET [--temperature-rise K] [--top N]
eda pcb stats    TARGET

TARGET accepts a .kicad_sch/.kicad_pcb file, a .kicad_pro, or a project directory. All commands print JSON by default (the review commands also take --text for a human readable digest) and exit 2 when a review found errors.

Environment variables

Variable Effect
KICAD_VERSION KiCad release / image tag to use (default 10.0.4)
EDA_IMAGE override the image name entirely
EDA_NETWORK 1 gives the container network access (default: offline)
EDA_MOUNT host directory to mount at /work (default: git root or $PWD)
EDA_ENV_PASSTHROUGH extra environment variable names to forward
EDA_DOCKER_ARGS extra arguments for docker run

Using it in CI

The reviews are exit-code gates, so a board can be checked on every push the way code is. In a project that added this as a submodule:

# .github/workflows/hardware.yml
- uses: actions/checkout@... # with: submodules: true
- name: Review the board
  run: |
    ./bin/eda.sh sch review hardware/ --text
    ./bin/eda.sh pcb review hardware/ --text
- name: Build the report
  if: always()
  run: ./bin/eda.sh report hardware/ -o build/report
- uses: actions/upload-artifact@...
  if: always()
  with: { name: hardware-report, path: build/report }

On a pull request the more useful question is what changed. eda diff answers it against the design rather than the file: a moved component rewrites thousands of coordinates in the .kicad_pcb, and a text diff cannot tell that apart from a rerouted net.

- name: What changed
  run: |
    git worktree add /tmp/base "$GITHUB_BASE_REF"
    ./bin/eda.sh diff /tmp/base/hardware hardware/ -o build/diff
    cat build/diff/diff.md >> "$GITHUB_STEP_SUMMARY"
## Connectivity
* **VCC**: gained U3.14, lost -
## Components
* **R7**: value '10k' -> '4k7'
## Board
* moved: U3 (5.0 mm), C12 (1.2 mm)
## Artwork
* `copper-front`: 0.28% of pixels changed

Both drawings are rendered and compared too — the schematic sheets and the board plots. What only the old revision had is drawn in red, what only the new one has in green, over a faded copy of the new drawing, so a part that moved reads as red where it was and green where it is now. Where the change is a speck on an A4 sheet, a zoomed crop is written alongside the full page:

schematic: C2 moved, R1 re-valued board: the same R1 move
schematic diff board diff

Exit 0 clean, 1 on a usage error, 2 when a review found errors. Loosen or tighten what counts as an error with --threshold KEY=VALUE, or post-process report.json if you want your own policy. This repository’s own CI reviews its test fixture and uploads the report the same way — see ci.yml.

Nothing needs the network at run time, so the container stays offline unless you ask for EDA_NETWORK=1.

Using it with a coding assistant

The toolkit is a CLI: any assistant that can run a shell command can drive it, and the JSON output is meant to be parsed rather than screen-scraped. What makes the difference in practice is not the commands but knowing how to use them — which is what docs/guides/ is for. Those guides cover the review methodology, what each finding means, and what the tool cannot judge for you.

Contributor and agent instructions for this repository itself live in AGENTS.md (CLAUDE.md is a pointer to it, not a second copy).

eda report exists largely for this use case: an assistant working headlessly can produce one page — renders, findings, BOM, simulation plots — that a human can check at a glance instead of taking a summary on trust.

How it fits together

bin/eda.sh            host wrapper: docker run, uid mapping, network policy, path rewriting
bin/install-skills.sh CLI shim + renders docs/guides/ into an assistant's layout
docker/Dockerfile     kicad/kicad:<version> + ngspice + an isolated virtualenv
src/eda_toolkit/
├── cli.py                 the `eda` command
├── report.py              the one-command report: collect, then render md/html
├── datasheet/             PDF text, table, image and page extraction
├── spice/                 ngspice runner, raw-file parser, measurements, plots
└── kicad/                 s-expression parser, schematic/board models,
                           outline geometry, kicad-cli wrapper, review rules,
                           renderers, fabrication package
tests/                     pytest suite + fixtures + smoke test
docs/guides/               the usage guides - prose, and the source of truth
docs/examples/             committed output, so the README shows rather than tells
AGENTS.md                  contributor / agent instructions (CLAUDE.md points here)

Design notes:

Review quality

The rules were tuned by running them over the 18 KiCad demo projects that ship with the image (/usr/share/kicad/demos), which is a far harsher corpus than the test fixture. That pass produced three fixes:

Re-run it after changing a rule:

docker run --rm -v "$PWD:/work" -w /work -e PYTHONPATH=/work/src \
  --entrypoint python3 eda-toolkit:10.0.4 tools/review_demos.py /tmp/out

Reproducibility

Every external input is pinned, and the pins are enforced by tests/test_pinning.py (which runs in CI):

Input Pin
KiCad base image manifest digest, per version, in docker/kicad-digests.txt
pip / uv exact version + wheel SHA-256 (ARG PIP_*, docker/uv-bootstrap.txt)
Build backend exact version in [build-system] requires + wheel SHA-256 in docker/build-backend.txtuv.lock cannot cover it, so the image installs it separately and builds with --no-build-isolation-package
Python packages uv.lock - exact versions + artifact hashes for the whole tree, installed with uv sync --frozen
GitHub Actions 40 character commit SHA, with the tag in a trailing comment
CI runner ubuntu-24.04, never -latest; Python 3.13.5

Keeping them current:

To change a dependency, edit pyproject.toml and regenerate the lock:

make lock          # uv lock
make rebuild

To use another KiCad release, add its digest to docker/kicad-digests.txt (the file documents the one-liner) and build with KICAD_VERSION=<version>. The build fails loudly if a version has no pinned digest.

Testing

make test          # everything, inside the container
make test-coverage # the same, with a coverage report
make test-host     # pure-python subset on the host (needs a local venv)
make lint          # ruff
make smoke         # end-to-end: every top-level command
make skills        # mirror docs/guides/ into .claude/skills (generated)
make site          # render the GitHub Pages site into _site/

CI (.github/workflows/ci.yml) runs on every push and pull request:

Job What it proves
unit ruff (lint + format) and the pure-python suite against the hash-pinned dependency set
container (KiCad 10.0.4) the full suite, coverage and the smoke test inside the freshly built image
container (KiCad 9.0.9) the same, one KiCad major behind - the toolkit really is version-portable

Both container jobs upload eda report output for the example project, so every run leaves behind a page you can look at. Nothing in CI touches the network beyond pulling the pinned base image and the locked wheels.

The suite covers the s-expression parser, schematic and board models, the outline geometry (arcs, circles, cutouts, seams), every review rule, the report renderers, the submodule installer, the ngspice raw-file parser (ASCII/binary, real/complex), the measurement maths (checked against analytically known circuits), the datasheet PDF extraction (against a generated PDF), and the CLI. Tests that need kicad-cli or ngspice are marked and skipped automatically outside the container.

Integration tests run the real tools: KiCad ERC and DRC on the example project, a deliberately introduced short to prove DRC catches it, netlist comparison between kicad-cli and the fallback extractor, layer/3D rendering, and an RC filter whose simulated −3 dB corner is compared against 1/(2πRC).

Limitations