spiceguard
ngspice said exit 0. The answer was still wrong.
Modern ngspice recovers from many classic convergence problems on its own — and when it can't, it often returns exit code 0 with a plausible but wrong answer (a relaxed fallback estimate, or arbitrary voltages on an ungrounded node). Nothing in the standard flow warns you. This bites hardest on netlists you didn't write yourself: AI-generated circuits and PR submissions.
spiceguard answers one question about a SPICE run: can I trust this
result? It combines static netlist analysis, ngspice failure-log decoding,
and silent-failure detection — as a CLI, an MCP server for AI
agents, a VS Code extension, and a KiCad
workflow.
Project history (image-to-SPICE origins and the pivot): docs/HISTORY.md.
Install
Requirements: Python 3.9+, ngspice
brew install ngspice # macOS; Linux: apt install ngspice
From PyPI:
pip install spiceguard
From the repo (development):
pip install .
# or, without installing:
PYTHONPATH=src python3 -m spiceguard FILE...
Docker (zero setup — ngspice bundled):
docker build -t spiceguard -f docker/Dockerfile .
docker run --rm -v "$PWD:/work" spiceguard mycircuit.cir
This registers a spiceguard command on your PATH.
ngspice path resolution (priority order)
spiceguard locates the ngspice binary in this order:
--ngspice PATHCLI flag — if given and not executable, raises an error immediately (no fallthrough)$NGSPICEenvironment variable — same hard-configured semantics; error if set but not usablewhich ngspice(PATH lookup)- Legacy fallback
/opt/homebrew/bin/ngspice
If none of the above resolves to a usable binary, spiceguard exits with code 3 and prints a clear message listing what was tried. Install ngspice, or point to it explicitly:
spiceguard --ngspice /usr/local/bin/ngspice mynetlist.cir
# or
export NGSPICE=/usr/local/bin/ngspice
CLI usage
spiceguard [--ngspice PATH] [--no-exec] [--version] [--help] FILE...
spiceguard kicad [--ngspice PATH] [--no-exec] FILE...
Pass one or more netlist (or schematic) files. When multiple files are given, spiceguard evaluates each in sequence and exits with the worst verdict across all.
Options
| Flag | Description |
|---|---|
FILE... |
One or more netlist or schematic files to check |
--ngspice PATH |
Explicit path to the ngspice binary |
--json |
Emit results as a JSON array (for editors, CI, tooling) |
--no-exec |
Strip .control blocks and file-splicing directives (.include/.inc*/.lib) before simulation — for netlists you did not write; applies to both the default review mode and spiceguard kicad |
--version |
Print version and exit |
--help |
Show usage |
Exit codes
| Code | Meaning |
|---|---|
| 0 | TRUSTWORTHY — ngspice exited 0 and no trust issues found |
| 1 | FAILED — ngspice exited non-zero |
| 2 | SUSPECT — ngspice exited 0 but trust issues were detected |
| 3 | ngspice not found |
| 64 | Usage error (bad arguments) |
Example
$ PYTHONPATH=src python3 -m spiceguard tests/netlists/n5_healthy_control.cir
======================================================================
n5_healthy_control.cir
======================================================================
✓ TRUSTWORTHY (ngspice exit 0)
No trust issues detected.
$ PYTHONPATH=src python3 -m spiceguard tests/netlists/n1_missing_ground.cir
======================================================================
n1_missing_ground.cir
======================================================================
⚠ SUSPECT (ngspice exit 0)
[FATAL] no_ground
→ No node '0' (ground). SPICE has no voltage reference, so it may float the circuit and return arbitrary WRONG voltages with no error. Fix: tie a reference node to '0'.
Input formats
| Extension | Handling |
|---|---|
.cir, .net, .sp, .spice, .ckt |
Passed directly to ngspice |
| Any other netlist | ngspice natively translates KiCad, LTspice, PSpice, HSpice dialects |
.asc |
Converted in-process (experimental — see below) |
LTspice .asc (experimental): spiceguard converts .asc schematics using
built-in 2-pin symbol geometry (resistor, capacitor, inductor, diode, voltage
source, current source). Net connectivity is recovered by union-find over
wire/pin/flag coordinates. For anything beyond these built-in symbols, export
the netlist from LTspice ("View > SPICE Netlist") and feed that instead — that
path is exact. When a .asc is evaluated, the generated netlist is printed at
the end of the report so you can compare it against LTspice's own export.
Subcircuit support (Feature B)
spiceguard's parser handles:
.subckt/.endsblock collection and extractionX-instance flattening with automatic node namespacing (internal nodes becomeinstancename:nodeto avoid collisions)+continuation lines rejoined before parsing.includefile resolution (local files only; URL.includelines are warned and skipped)
Errors detected during subcircuit processing:
undefined_subckt— X-instance references a subckt name not defined in the netlistport_mismatch— X-instance provides a different number of nodes than the subckt port listsubckt_recursion— self- or mutual-referencing subckts (skipped with a warning)
KiCad workflow (Feature C)
spiceguard kicad myboard.cir
The kicad subcommand runs the standard trust check plus a KiCad-specific
preflight that detects the classic export gotcha:
kicad_ground_not_zero — ngspice requires the circuit ground to be exactly
node 0. KiCad schematics commonly use a GND net (or GNDA, VSS, 0V,
AGND, DGND, PGND) that is NOT automatically mapped to node 0 on SPICE
export. When such a net is present and node 0 is absent, the simulation silently
floats the entire circuit, yielding wrong voltages with no error. The message
includes KiCad-specific fix instructions (set node mapping in Symbol Properties
or place a PWR_FLAG).
Pipe form — export and check in a single step without writing a file:
kicad-cli sch export netlist --format spice myboard.kicad_sch -o - \
| spiceguard kicad -
Important: spiceguard is a command-line workflow helper, not a native KiCad
plugin. KiCad has no post-simulation event API, so there is no in-GUI integration;
run spiceguard kicad from your terminal or CI pipeline.
Verdict model
Every run produces one of three verdicts:
| Verdict | Meaning |
|---|---|
| TRUSTWORTHY | ngspice exited 0 and no trust-breaking issues found |
| SUSPECT | ngspice exited 0 but at least one FATAL/SILENT/WARN issue detected |
| FAILED | ngspice exited non-zero |
FAILED outranks SUSPECT, which outranks TRUSTWORTHY (this is why exit code 1 < 2 numerically but FAILED is the worst outcome).
Detectors
| Code | Severity | Description |
|---|---|---|
no_ground |
FATAL | No node '0' — circuit has no voltage reference |
source_conflict |
FATAL | Multiple voltage sources forced across the same node pair |
no_dc_path |
FATAL | Node reachable only through capacitors/current sources — no DC reference |
timestep_collapse |
FATAL | Transient timestep collapsed; specific culprit identified from the log |
singular_node |
FATAL | Singular matrix at a node — no defined DC solution |
singular_branch |
FATAL | Singular matrix at a branch (current unconstrained) |
silent_fallback |
SILENT | ngspice exited 0 after gmin/source stepping failed; operating point is a relaxed estimate, not a true solution |
kicad_ground_not_zero |
WARN | KiCad export uses GND/GNDA/etc. but no node 0 (kicad subcommand only) |
dangling_node |
WARN | Node connects to only one pin — likely a wiring mistake |
Integrations
| Surface | Where | What it gives you |
|---|---|---|
| Docker | docker/ |
Zero-setup image with ngspice bundled |
| VS Code | vscode/ |
Inline trust diagnostics as you edit .cir/.net/.sp files (consumes --json) |
| KiCad | kicad/ |
kicad-cli netlist export → spiceguard kicad, as a one-liner, helper script, or CI step |
| MCP server | mcp-server/ |
Lets AI agents (Claude Code, Cursor, ...) verify SPICE netlists they generate before treating the result as ground truth |
All four build on the same engine; the --json output makes spiceguard easy to
wire into editors, CI, and other tools.
Security
spiceguard runs ngspice in batch mode on the netlists you give it. Treat a netlist like a script you are about to run, because in two ways it is one:
- A SPICE netlist can contain
.control/shelldirectives that execute arbitrary shell commands. The captured log may include their output. .includereads whatever file path the netlist specifies (the same files ngspice itself would read), so a hostile netlist can point at files on your disk. The contents are parsed as a netlist, not printed.
Only run spiceguard on netlists you trust. It is a local dev/CI utility,
not a sandbox — --no-exec is defense-in-depth against the two specific
attack surfaces above, not a guarantee that a hostile netlist is safe to
evaluate.
For untrusted netlists (AI-generated, PR-submitted) use --no-exec, which
strips exactly these lines before simulation:
.control....endcblocks (arbitrary shell commands), and- file-splicing directives:
.includeand every ngspice-honored prefix abbreviation of it (.inc,.incl,.inclu,.includ), plus.lib(library file + section) and its.endlblock terminator — i.e. anything that pulls another file's content into the netlist ngspice runs.
The MCP server does this unconditionally.
Hardening that is in place:
- ngspice is invoked with
--no-spiceinit, so a.spiceinit/spice.rcconfig planted next to the netlist is not auto-executed. - The simulator is launched with a fixed argument list, never through a
shell (no
shell=True), so the path can't be shell-injected. - A 120s timeout terminates a hung/non-converging run (reported as FAILED, not
a crash); temp files use
mkstemp(0600) and are always cleaned up; subcircuit nesting is depth-bounded against stack exhaustion. - spiceguard has no third-party runtime dependencies, and the source passes
bandit -r src/with no findings (pip-auditreports no vulnerable deps).
Limitations (honest scope)
- Parser scope: The static netlist parser covers the element types listed in
netlist.py(NODE_COUNT). Exotic or simulator-specific elements not in that table are silently skipped (they do not affect the running netlist — ngspice still sees the full file). .ascis experimental and scoped to 6 built-in 2-pin symbol types. Any schematic with transistors, op-amps, subcircuit blocks, or custom symbols must be exported from LTspice first. Always verify the generated netlist against LTspice's own "View > SPICE Netlist".- Legacy fallback (
/opt/homebrew/bin/ngspice) is tier-4 last-resort only. It is not the preferred path; preferPATHor the$NGSPICEvariable. - KiCad integration is a CLI helper, not an in-application plugin.
Development
Run the test suite (no ngspice required for unit tests; integration tests auto-skip when ngspice is absent):
PYTHONPATH=src python3 -m pytest -q
Observed output on the current suite:
119 passed in 2.61s
Metadata
Release files for spiceguard 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| spiceguard-0.3.0.tar.gz | 50.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| spiceguard-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 81.2 kB
Release files / spiceguard-0.3.0.tar.gz
| Download URL | spiceguard-0.3.0.tar.gz |
|---|---|
| Size | 50.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
2b0612e22e24cf5118e53432e5df647bd37e683ea6fd3510e51d52f948cbe499
|
|
BLAKE2b-256 checksum How to use checksums |
9cae0b1a624576b3d370eb35091c0c20642eb347d3d392f44bf6ecefb205a2a6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.
Transparency logRelease files / spiceguard-0.3.0-py3-none-any.whl
| Download URL | spiceguard-0.3.0-py3-none-any.whl |
|---|---|
| Size | 31.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
0a6f0c392874eaffca87b6ad66c0165b946f519a0ca3d7b6aa58276dd28c04cd
|
|
BLAKE2b-256 checksum How to use checksums |
b0d1e63b9a0a63174536c2f840af90d4dd865e8e32ead2e03f8d028736a1ee5a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.
Transparency log