KiCadStamp automates component placement and block cloning on KiCad 10 printed circuit boards. It connects to a running KiCad over the IPC API and makes repeatable what otherwise has to be done by hand hundreds of times: placing components, dropping vias, laying tracks and moving whole functional blocks from one place to another.
It is an advanced alternative to the KiCad Replicate Layout plugin, aimed at complex multi-channel boards, hierarchical schematics and the reuse of finished routing: replicating board sections, copying channels, cloning pi filters and power rails, and carrying routing across identical blocks.
It ships a graphical interface (PyQt6), a command line, and an MCP server for use from Claude Code.
The core idea is that components are chosen by role, not by refdes. A block is described once in local coordinates and bound to the board through roles and nets, so re-annotating the schematic breaks nothing.
- Cells — a block's geometry in local coordinates: components, vias, tracks. A cell can be rotated, shifted, mirrored and applied anywhere on the board.
- Role-based lookup — instead of
C12/R7you write roles (PI_FILTER_C1,HEAVY), and the actual instances come from a pool keyed by the schematic'sRolefield and by net. - Placement trees (
trees:) — where each instance actually sits. A tree node references an entity and carries the position; the entity itself has no position at all. - Section cloning — by selection (a one-off instance) or by nets (a block repeated many times, with net names parametrised).
- Extraction from the board — select a block in KiCad, read it into a cell, then apply it anywhere.
- Placement registry — remembers the UUIDs of created vias and tracks and reconciles against the live board, so a repeat run updates instead of duplicating and cleans up what went stale.
- Up-front validation — the whole config is checked before the first board edit: cells, pads and anchors exist, pools hold enough components, names are unique, nets resolve.
- Undo of the last operation.
- Schematic-side work — bulk setting and renaming of
Role/Clusterfields directly in.kicad_sch(fieldstool), without IPC and with KiCad closed. - File-based cloner — parses
.netand.kicad_pcbwithout IPC and builds a twin map of channels for hierarchical projects. - Python API — placement can be built as code instead of config: board queries through
kicadstamp.explore, placement construction throughkicadstamp.author.
DRC is deliberately out of scope: track collisions are KiCad's own job.
- Python 3.10 or newer.
- KiCad 10.0.4 or newer with the IPC API enabled (Preferences → Plugins → Enable IPC API server).
- For the GUI, a working Qt stack (installed along with the dependencies).
git clone <repo>
cd KiCadStamp
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e .Dependencies are installed automatically and version-pinned. The ones that carry the runtime:
kicad-python==0.7.1 (the KiCad IPC wrapper, imported as kipy), PyQt6==6.11.0, sexpdata==1.0.2,
pynng==0.9.0, protobuf==5.29.6.
Optional extras:
pip install -e ".[dev]" # pytest, babel, pyflakes
pip install -e ".[diagnostics]" # numpy, scipy, psutil, rich, watchdog
pip install -e ".[mcp]" # mcp — only needed for the MCP serverInstalling gives you three commands:
| Command | What it starts |
|---|---|
kicadstamp |
the command-line interface |
kicadstamp-gui |
the graphical interface |
kicadstamp-mcp |
the MCP server (stdio) |
Running from a source checkout without installing, the equivalent scripts live in the repository root:
kicadstamp_cli.py, kicadstamp_gui.py, fieldstool_cli.py.
A component takes part in placement only if it carries a custom Role field:
- Open the symbol in Eeschema.
- Add a field named Role whose value matches the role in the cell (for example
LIGHT,HEAVY). - Run Update PCB from Schematic so the field reaches the board.
- Check that the field is readable: select the component in KiCad and run
The script prints refdes, value, footprint, position, angle, pads, nets and the
python -m kicadstamp.diagnostics.get_selected_component
Rolefield.
You do not have to set roles on dozens of components by hand — that is what fieldstool is for (a tab in
the GUI, or fieldstool_cli.py); see docs/fieldstool.md.
kicadstamp-gui # graphical interface
kicadstamp apply --config profiles/my/config.sexp --dry-run--dry-run prints the plan without touching the board. That is the right first run against a config you
do not know yet.
A short glossary. The details live in docs/config.md and docs/placement.md.
Role — a component field in the schematic. A role says what the component does in the block, not what it is called.
Cluster — a second component field grouping the instances of one block. Role plus cluster identify a component without relying on its refdes.
Cell — a block described in local coordinates: components by role, vias, tracks. A cell has no board position of its own — it is a template.
Cell anchor — the point of the cell that lands on the board (anchor_xy/anchor_role/anchor_pad).
It is expressed in the cell's own frame and resolved to live coordinates when applied.
Entity — everything about an entity except where it sits: which cell, its electrics, its identity. An entity has no position fields at all.
Tree (trees:) — where it stands. A tree node references an entity and carries coordinates; nodes
nest, and a child's coordinates are measured from its parent.
ManualSpoke — a via with a track from a component pad, written by hand inside a chain (chains:).
ClonePlacement — the historical way to place a clone (by selection or by nets). Still supported, but
new profiles should prefer the Entity + Tree model; tools/convert_placements.py migrates the old ones.
Imprint — a snapshot of a set of board components that can be re-read and re-placed as a whole.
Registry — a journal of created vias and tracks with their UUIDs. It is what makes a repeat run idempotent: it updates rather than duplicates.
The config is an s-expression (.sexp), like KiCad's own formats. A file with any other extension is
rejected with a fatal error. (.json is still read as well — it backs a legacy scheme_lists.json side
file; the GUI now creates its own scheme_lists.sexp the first time an Imprint is recorded, and keeps
using an existing scheme_lists.json as-is where a profile already has one.)
A profile can be split across several files: include: at the root pulls in other .sexp/.json files
recursively, and each may carry any combination of sections. The flatten command folds such a graph back
into one self-contained file.
The full reference, with examples taken from a live profile, is docs/config.md.
| Command | Purpose |
|---|---|
apply |
apply the placement described by a config to the open board |
undo |
undo the last operation |
extract |
extract a cell from the current board selection |
extract-net |
capture one net's copper (tracks + vias) as a net_traces: record |
clone-extract |
snapshot a channel to .sexp (file-based cloner, no IPC) |
clone-plan |
generate a ready clone_placements: block for a channel clone |
channel-copy |
copy a whole channel's placement from one channel to another via a twin map |
flatten |
merge an include: graph into one self-contained file |
convert-trees |
rewrite trees: from the removed own_anchor grammar to mount nodes |
Every flag is documented in docs/commands.md.
Example:
kicadstamp apply --config profiles/my/config.sexp --dry-run # plan only
kicadstamp apply --config profiles/my/config.sexp # apply
kicadstamp undo --verbose # roll backkicadstamp-gui [--timeout-ms 20000] [--verbose]This is the main way to work with a project. On the left, three trees — Components (board and schematic components), Config (the config's structure) and Trees (placement trees); on the right, a context panel that follows the selected node; along the bottom, the log.
Edits are not written to disk as you make them: they accumulate in a working set, and File → Save commits them all at once.
See docs/gui.md for the details and docs/hotkeys.md for the keyboard shortcuts.
Placement does not have to be written as config — it can be written as code, through two optional libraries layered over the same pipeline, with no format of their own and no way around validation.
kicadstamp.explore reads the board: Board.select(role=..., cluster=..., sheet=..., net=...) answers
"which components carry Role=AD_DAC", "what net is this pad on", "which sheet instance is this
footprint under" — instead of a throwaway script every time.
kicadstamp.author builds placement: ClonePlacement and Chain are plain dataclasses, so a loop with
real Python variables replaces copy-paste and placeholder substitution. The result is either applied
straight away through apply_config() or saved to a file pulled in with include:.
from kicadstamp.explore import Board
board = Board.connect(config_path="profiles/my/config.sexp")
for fp in board.select(role="AD_DAC"):
print(fp.ref, fp.cluster, fp.sheet)The full reference, walked through a real script, is docs/python.md.
pip install -e ".[mcp]"
kicadstamp-mcpAn MCP server over stdio: Claude Code and other MCP clients can see the live board and act on it — read
the board identity, footprints with their roles and clusters, the current selection and the board's nets;
apply a config through the same validated pipeline as apply; and, when explicitly enabled, move items
directly.
See docs/mcp.md.
KiCadStamp/
├── kicadstamp/ # core: config, planning, execution, IPC
│ ├── config/ # config loading and model, the include: graph
│ ├── domain/ # board DTOs (Footprint, Track, Via, Pad, Net, Zone)
│ ├── kicad/ # the KiCad IPC adapter and the IBoardAdapter interface
│ ├── placement/ # planner, executors, services
│ ├── geometry/ # geometry: layout, keepout, cloning
│ ├── cloner/ # file-based cloner (.net/.kicad_pcb, no IPC)
│ ├── diagnostics/ # diagnostic scripts
│ ├── cell_*.py # cells: frame, geometry, placement copying
│ ├── trees.py, link_trees.py, tree_position.py # placement trees
│ ├── net_*.py # net resolution, matching and traces
│ ├── schematic_*.py # .kicad_sch handling (fieldstool)
│ ├── config_working_set.py # the staged-edit model
│ └── registry.py # via and track registry
├── gui/ # PyQt6 GUI: docks, trees, editors, board overlay
├── mcp_server/ # MCP server (stdio)
├── docs/ # documentation, bilingual (en + _ru)
├── tests/ # tests
├── tools/ # utilities and profile converters
├── locales/ # gettext translation catalogues
├── diagnostics/ # throwaway probes and reproductions
└── packaging/ # distribution builds
With the schematic editor open, the first transaction of a session
(begin_commit()/push_commit(), even an empty one) could crash KiCad — a null pointer in
API_HANDLER_EDITOR::checkForBusy. From the client side KiCad closed silently and you got
ConnectionError: Error receiving reply from KiCad: Timed out.
Fixed upstream on 2026-09-07 (Jon Evans, commit
bdcb1509),
milestone 10.0.7. Both tickets are closed.
On 10.0.6 and older the workaround still applies: close the schematic editor before the first write.
The warning (check_write_crash_risk) and the delayed retries stay in the code — they are harmless and
simply never fire on a fixed build. It was specifically the first write of a session that was exposed:
if the first apply ran with only the PCB Editor open, opening the Schematic Editor afterwards was
usually safe.
The full write-up and the crash-hunting toolkit are in docs/crash_hunting.md.
In kicadstamp/diagnostics/:
diagnose_first_write_crash.py— a read/write ladder for pinning the crash down, see docs/diagnose_first_write_crash.md;test_move_one_cap.py,test_flip_one_cap.py,test_create_one_via.py— minimal operation tests;test_pad_mirror_convention.py— an empirical check of how pads mirror on flip;get_selected_component.py— details of the selected components, including theRolefield;get_pad_bbox.py,diagnostic_keepout.py— helpers.
An overview is in docs/diagnostics.md.
Every page is bilingual: docs/<topic>.md is English, docs/<topic>_ru.md is Russian.
- Project architecture
- CLI commands
.sexpconfiguration reference- PyQt6 GUI
- Keyboard shortcuts
- MCP server
- Planning and execution
- Geometry utilities
- Rotating and transforming cells
- KiCad adapter
- Using kipy
- Coding placement in Python: explore/author
- fieldstool: Role/Cluster in
.kicad_sch - File-based cloner
- Diagnostics
- Hunting KiCad crashes
diagnose_first_write_crash.pyreference- Top-level modules
- Module dependency diagram
- Internationalisation (i18n) — gettext/Babel
- Tests
The single source of truth is __version__ in kicadstamp/_version.py: this
README's heading and the --version/-V flag of every entry point read the version from there instead of
keeping a literal of their own.
We count by stages, not by commits: MINOR goes up by one per noticeable block of work (one refactoring
session is one step, however many commits it contains), PATCH covers point fixes between stages, and
MAJOR is reserved for genuine breaking changes to the CLI or to the config format (.sexp/.json).
2.1.0 — inter-node copper read into the tree, and the full copper stack (2026-09-12): the
"copper between pads" unit is read from the board into a flat net_traces: list and can be selected
on the board (and back — "whose copper is this?"); a cell's and a net trace's copper may sit on ANY
copper layer (F.Cu, In1.Cu…In30.Cu, B.Cu) and is placed back on that layer, with strict
name parsing; mirroring swaps F.Cu/B.Cu only, and copper on an inner layer keeps its own layer.
The format widened: older configs still load, but a config this version writes with
layer: In1.Cu on a track will not load on 2.0.x — hence MINOR, not PATCH.
2.0.0 — .sexp became the config format (2026-08-28) and placement moved to the Entity + Tree model.
KiCad, KiCad 10, KiCad IPC API, kipy, PCB automation, replicate layout, board section replication, channel cloning, copy placement, multi-channel PCB, hierarchical schematics, repeated blocks, component placement, vias, via stitching, thermal vias, template-based routing, Role/Cluster, placement generation, Python, PyQt6, MCP, Claude Code.
This project is distributed under the MIT license. See the LICENSE file for details.