Automated transparency, woven from the ground up.
Pitloom automates the generation of SPDX 3-compliant SBOMs for AI models and Python projects. It reads metadata directly from Python packages and AI models (GGUF, ONNX, PyTorch, Safetensors), producing standardized SPDX 3 JSON artifacts -- as a CLI, a library, or a native Hatchling build hook.
When used with Hatchling, it embeds the generated SBOM directly into
the Python distribution package (wheel) .dist-info/sboms --
follows PEP 770.
pip install pitloom
loom . # SBOM for the Python project in the current dirInstall extras to enable metadata extraction from AI model files or from Hugging Face Hub:
pip install -e ".[ai]" # all supported AI model formatsSee CONTRIBUTING.md for the dev install.
| Surface | Reach for this when... |
|---|---|
Command line (loom / pitloom) |
You want a one-off SBOM from a terminal, a Makefile target, or any shell script. |
| Hatchling build hook | You build wheels with Hatchling and want an SBOM embedded automatically. |
| Python API | You are calling Pitloom from Python code you control. |
| Python tracking decorator | You are training/fine-tuning a model and want to capture provenance as you go, as an SPDX fragment. |
| GitHub Action | Your project isn't Hatchling-based, or you just want CI to produce an SBOM artifact with one uses: line. |
| Agent Skill | You want an AI coding agent to generate (and optionally enrich) an SBOM on request. |
| Claude Code plugin | You use Claude Code and want the Skills installable with one command. |
Generate an SBOM for a Python project in the current directory:
loom .
loom /path/to/project -o sbom.spdx3.jsonGenerate an SBOM for a single AI model file, without a Python project
directory (output written to the current working directory). Supported
local formats: GGUF, ONNX, Safetensors, PyTorch (.pt/.pth), Keras,
HDF5, NumPy, fastText:
loom -m path/to/model.safetensors -o model.spdx3.json
loom -m path/to/model.gguf --prettyOr pass a Hugging Face Hub URL or model ID directly -- no local file
required. Pitloom fetches metadata from the Hub (model card, config.json,
tokenizer_config.json, generation_config.json) and produces an enriched
ai_AIPackage SBOM. Requires huggingface_hub
(pip install pitloom[huggingface]):
loom -m https://huggingface.co/mistralai/Mistral-7B-v0.1
loom -m Qwen/Qwen3-235B-A22B # bare model ID also worksloom -h shows the full option list.
Pitloom can embed an SBOM automatically into every wheel you build, at
.dist-info/sboms/sbom.spdx3.json, per
PEP 770 (wheels only). Add pitloom
as a build requirement (Hatchling 1.28.0+ required) and register the
hook:
[build-system]
requires = ["hatchling>=1.28.0", "pitloom>=0.11.0"]
build-backend = "hatchling.build"
[tool.hatch.build.hooks.pitloom]
enabled = true # set to false to skip SBOM generationThat's all -- hatch build/python -m build now embeds the SBOM, always
as compact canonical JSON. Basename and fragments are configured under
[tool.pitloom]; creator/tool metadata uses the same
[[tool.pitloom.creator]] / [[tool.pitloom.creation-tool]] /
[tool.pitloom.creation] tables the CLI reads (see
Creation metadata below):
[tool.pitloom]
sbom-basename = "sbom" # -> "sbom.spdx3.json"
[tool.pitloom.fragments]
files = ["fragments/model.json"] # merge externally tracked fragmentsThe SBOM generator can be used programmatically:
from pathlib import Path
from pitloom.core.creation import CreationMetadata, Creator
from pitloom.assemble import generate_sbom
generate_sbom(
project_dir=Path("/path/to/project"),
output_path=Path("sbom.spdx3.json"),
creation_metadata=CreationMetadata(creators=[Creator(name="Your Name")]),
)pitloom.assemble also exposes generate_ai_model_sbom() (a local model
file) and generate_huggingface_sbom() (a Hub model ID or URL), with the
same output_path/creation_metadata/pretty keywords.
Developers can annotate scripts or Jupyter notebooks to generate external
SBOM fragments that Pitloom will merge during the build process, as a
function decorator or a context manager. Use set_model when generating
a new model, and use_model when consuming one for inference or evaluation:
from pitloom import loom
@loom.run(output_file="fragments/sentiment_model.json")
def train_model():
loom.set_model("sentiment-clf")
loom.add_dataset("imdb-reviews", dataset_type="text")
# ... training logic ...
@loom.run(output_file="fragments/sentiment_eval.json")
def evaluate_model():
loom.use_model("sentiment-clf")
loom.add_dataset("imdb-test-set", dataset_type="text")
# ... evaluation logic ...See Python tracking decorator advanced usage below for more details.
Add SBOM generation to any repository's CI with a single step, for any Python build backend, not just Hatchling:
- uses: bact/pitloom@v0.12.0See working-docs/implementation/github-action.md for inputs, outputs, and more recipes.
skills/sbom/ and skills/enrich/ are ready-to-install
Agent Skills for Claude Code and the Claude
Agent SDK: sbom generates an SBOM on request; enrich augments an
existing one with detail read from a README or model card, via Pitloom's
fragment system.
mkdir -p ~/.claude/skills # or .claude/skills for a project-scoped install
cp -r /path/to/pitloom/skills/sbom /path/to/pitloom/skills/enrich ~/.claude/skills/See working-docs/implementation/agent-skill.md for full install instructions.
The Skills above are also installable as a plugin, self-hosted from this repository:
/plugin marketplace add bact/pitloom
/plugin install pitloom@pitloom
Once installed: /pitloom:sbom, /pitloom:enrich (or just ask in plain
language). See
working-docs/implementation/claude-code-plugin.md
for what the plugin bundles.
git clone https://github.com/bact/sentimentdemo.git
loom sentimentdemoThe generated SBOM includes project metadata, dependencies with version constraints, SPDX relationships, creator/creation info, and per-field metadata provenance. See a more complete example in the examples/ directory.
These flags apply to project, AI model, and Hugging Face SBOM generation
alike. --creator-name is repeatable -- each occurrence starts a new
creator, in order; --creator-type (person default, organization,
software-agent, agent) and --creator-email set the type/email of the
most recently named creator. --creation-tool records what produced
it (default "Pitloom", also repeatable; --no-creation-tool to omit);
--creation-comment/--creation-datetime set free-text provenance and an
ISO 8601 timestamp:
loom . --creator-name "Alice" --creator-email "alice@example.com"
loom . --creator-name "Acme Corp" --creator-type organization
loom . --creator-name "Acme Corp" --creator-type organization --creator-name Alice
loom . --creation-datetime "2026-01-15T10:00:00Z" --creation-comment "CI run #123"The same fields can be set in pyproject.toml under
[[tool.pitloom.creator]] / [[tool.pitloom.creation-tool]] (CLI flags
take precedence, replacing the whole list rather than merging):
[[tool.pitloom.creator]]
name = "Alice"
email = "alice@example.com"
type = "person" # or "organization", "software-agent", "agent"
[[tool.pitloom.creator]]
name = "Acme Corp"
type = "organization"
[[tool.pitloom.creation-tool]]
name = "MyCompany SBOM Wrapper"
[tool.pitloom.creation]
creation-datetime = "2026-01-15T10:00:00Z"
creation-comment = "Generated in CI pipeline #123"See Creation metadata for what these fields record and why -- the who/what/when/how model behind every element Pitloom emits.
Fragments are written by independent runs, so the same dataset or model
would normally get a different spdxId in each -- leaving the merged SBOM
as disconnected islands. The Loom ID registry (loom-ids.json) fixes that:
pitloom ids generate data src --entity model # pin ids before running
pitloom ids import existing-sbom.spdx3.json # or reuse ids from an SBOMpitloom.loom, loom -m, the build hook, and generate_sbom() all
auto-discover the registry (or take it from [tool.pitloom.ids] file),
so the same file/entity carries the same id everywhere. Regeneration is
stable: an unchanged file keeps its id; changed content gets a fresh one
(different bytes are different provenance).
At build time merge_fragments unifies fragment elements -- by shared
spdxId, by identical SHA-256 content, or (for the per-fragment "Pitloom"
Agent/Tool copies) by structural equality; never by name alone.
Fragment envelopes are dropped, duplicate relationships removed, the
document's profileConformance gains ai/dataset as appropriate, and a
second software_Sbom rooted at the merged ai_AIPackage is added, so the
wheel ships one connected AI-pipeline graph: the packaged training script
generates the model, which was trainedOn datasets that trace back
via hasInput to the raw data.
loom.run accepts the same creation metadata as the
CLI and build hook, via creation_metadata=CreationMetadata(...). With
none given, the fragment records the unattended-run default (Pitloom
itself as both creator and tool).
The run also records which script produced what: the calling script
becomes a software_File (with a SHA-256 hash) with generates
relationships to the model it trained and/or the output datasets it wrote.
Datasets that exist on disk get verifiedUsing SHA-256 hashes. These
generates edges are scoped build (LifecycleScopedRelationship) --
they describe a build-time step, not something that runs in the shipped
artifact. Contrast with the hasDataFile relationship Pitloom emits when it
detects a script using a model file at runtime (e.g. a predict.py that
loads it) -- that one is scoped runtime.
A single run can cover more than one independent preprocessing stage --
e.g. producing train/valid/test splits from separate raw sources in one
loom.run block -- without their hasInput lineage bleeding into each
other. Pass input_datasets= on add_output_dataset() to name exactly
which add_input_dataset() calls a given output derives from:
with loom.run("fragments/preprocess.json") as run:
for split in ("train", "valid", "test"):
sources = [f"rawdata/{split}/{label}.txt" for label in labels]
for source in sources:
run.add_input_dataset(source, dataset_type="text")
run.add_output_dataset(
f"data/{split}.txt", dataset_type="text", input_datasets=sources
)Omit input_datasets (the default) when a run has exactly one output
batch -- it then derives from every input the run declared, as before.
Pitloom tracks the source of each metadata field in the SBOM using the
SPDX 3 comment attribute, so questions like "why does the SBOM say the
concluded license is MIT?" have a traceable answer. See
Metadata provenance for the full explainer
and a worked example.
- SPDX 3.0 Specification
- PEP 770 – SBOM metadata in Python packages
- Design document
- Bennet et al., “Implementing AI Bill of Materials with SPDX 3.0”, The Linux Foundation, 2024.
- Source code: Apache License 2.0.
- Documentation: Creative Commons Attribution 4.0 International.
- Test fixture AI models: individually licensed (Apache-2.0, CC0-1.0, or MIT); see tests/fixtures/README.md. Source repository only -- not included in distribution packages.
A pit loom is a traditional handloom built into a ground-level pit to house its internal mechanisms and the weaver's legs. This "grounded" design provides stability and precision during the weaving process.
We use the loom as a metaphor for the tool's function: it weaves disparate threads of metadata into a cohesive SBOM, creating a transparent, structured "fabric" for the software build.
