| Created | 2026-08-11 |
|---|---|
| Last-Modified | 2026-09-08 |
| SPDX-FileCopyrightText | 2026-present Arthit Suriyawongkul |
| SPDX-FileType | DOCUMENTATION |
| SPDX-License-Identifier | CC0-1.0 |
Use this when you want a one-off SBOM from a terminal, a Makefile target,
or any shell script. The console script is installed under two names,
loom and pitloom -- pick whichever reads better; they run the same
tool.
pip install pitloom
loom project . # SBOM for the Python project in the current dirloom -h shows the full option list.
pip install pitloomInstall with AI model metadata extraction support:
pip install "pitloom[ai]"Install with extra content type detection:
pip install "pitloom[content-type]"Install with SPDX 3 schema/SHACL validation support (pitloom fragment validate, pitloom validate-wheel):
pip install "pitloom[validate]"Generate a Source SBOM for a Python project in the current directory:
loom project .
loom project /path/to/project -o sbom.spdx3.jsonLimitation: the per-file inventory (file list, hashes, Merkle root) is backend-aware and accurate for Hatchling, setuptools, Poetry, PDM-backend, and Flit-core. Any other backend (
uv_build, etc.) falls back to a Hatchling-based heuristic and logs aWARNING:-- the file list can be silently incomplete or mis-pathed. Project-level metadata (name, version, dependencies, license, authors) is read independently and unaffected either way.
If a lock file (pylock.toml, uv.lock, poetry.lock, pdm.lock,
Pipfile.lock, or a fully pinned requirements.txt) is present next
to pyproject.toml (or setup.py, for Pipfile.lock/requirements.txt),
its resolved transitive dependencies are added to the Source SBOM's
dependency list too -- see
Dependency sources and precedence for which
one wins when more than one is present, and what counts as "resolved"
for each.
Generate an Analyzed SBOM from a pre-built wheel (extracting bundled binaries as phantom dependencies):
loom wheel path/to/mypackage-1.0.0-py3-none-any.whl -o sbom.spdx3.jsonGenerate and embed an SPDX 3 SBOM directly into one or more built .whl
files (writing to .dist-info/sboms/ and updating .dist-info/RECORD):
loom embed-wheel dist/mypackage-1.0.0-py3-none-any.whl
loom embed-wheel dist/*.whl --project-dir .With --project-dir, the file list and hashes always come from the
wheel itself, so they're accurate regardless of build backend. What can
still be affected by the Source SBOM limitation above is --content-type
and --extract-file-header, for any backend still on the Hatchling-based
fallback (see above): that per-file enrichment can silently fail to
attach to any file (falls back to no content-type/header data for it,
not a wrong one).
Or inject an existing pre-generated SBOM into built wheels:
loom embed-wheel dist/*.whl --sbom sbom.spdx3.jsonsbom.spdx3.json's declared subject name/version (PEP 503/440-normalised)
is cross-checked against the target wheel's own .dist-info/METADATA
before anything is written: a mismatch is an ERROR: that aborts the
embed (exit 1, nothing written); pass --allow-mismatch to downgrade it
to a WARNING: and embed anyway (useful for CI/automation that wants
best-effort embedding). A Pitloom-generated SBOM (no --sbom) is never
checked -- it's built from the same wheel metadata, so it can't diverge.
--sbom-basename NAME overrides the embedded file's basename (default:
derived from the wheel's own name/version, <name>-<version>.spdx3.json).
-o/--output names the modified wheel's own output path and is
rejected with an ERROR: when more than one wheel is passed -- ambiguous
without a per-wheel naming scheme; omit it to modify each wheel in place.
Check a wheel's embedded SBOM is at the correct PEP 770 location
(.dist-info/sboms/), uses its format's recommended extension, and its
declared subject name/version (PEP 503/440-normalised) match the wheel's
own .dist-info/METADATA:
loom verify-wheel dist/*.whl
loom verify-wheel dist/mypackage-1.0.0-py3-none-any.whl --sbom-filename mypackage-1.0.0.spdx3.json
loom verify-wheel dist/*.whl --fail-on-mismatchA missing SBOM is an ERROR: (exit 1); a present-but-non-conventional
extension is a WARNING: only -- not fatal, still exit 0. Multiple
sboms/ entries need --sbom-filename to pick one, else it's an
ERROR:. A name/version mismatch is a WARNING: by default (exit 0);
pass --fail-on-mismatch to make it an ERROR: (exit 1) instead. When
the SBOM's subject name/version can't be extracted at all (unsupported
format, or SPDX3 with an unexpected graph shape), the cross-check is
skipped with a WARNING: naming why, regardless of --fail-on-mismatch.
Validate a wheel's embedded SBOM content against its format's schema and
SHACL rules (currently SPDX3 JSON-LD only, via the same spdx3-validate
library used by pitloom fragment validate --
needs pip install "pitloom[validate]"):
loom validate-wheel dist/*.whlAn embedded file in an unrecognised format prints a WARNING: and skips
validation (exit 0) rather than failing -- unsupported isn't the same as
invalid. embed-wheel itself takes --verify/--validate as convenience
flags that run these same checks against the wheel just embedded:
loom embed-wheel dist/*.whl --project-dir . --verify --validateEmbedding and the post-embed check are independent steps -- a --verify/
--validate failure is reported and affects the exit code, but the
embed itself isn't rolled back.
Or use --embed directly on loom wheel:
loom wheel dist/mypackage-1.0.0-py3-none-any.whl --embedGenerate a Deployed SBOM reflecting the exact installed environment graph:
loom env -o env.spdx3.jsonGenerate an Analyzed SBOM for a single AI model file, without a Python
project directory. Supported local formats: GGUF, ONNX, Safetensors,
PyTorch (.pt/.pth), Keras, HDF5, NumPy, fastText -- see AI model
formats for the full extension/install-extra table:
loom model path/to/model.safetensors -o model.spdx3.json
loom model path/to/model.gguf --prettyOr pass a Hugging Face Hub URL or model ID directly -- no local file
required (needs pip install pitloom[huggingface_hub]):
loom model https://huggingface.co/mistralai/Mistral-7B-v0.1
loom model Qwen/Qwen3-235B-A22B # bare model ID also worksOr use the smart unified entrypoint, which auto-detects the target type:
loom generate . -o sbom.spdx3.json # project directory -> Source SBOM
loom generate path/to/model.safetensors -o model.spdx3.json # AI model asset -> Analyzed SBOM
loom generate env -o env.spdx3.json # installed venv -> Deployed SBOM-o/--output is required for generate: unlike project/wheel/
model/env, which each know their target type and so have an obvious
default filename, generate dispatches across several target types with
no single natural default -- pass -o explicitly, or use the
target-specific command for its own default.
Fill AI-model metadata gaps (license, datasets) from a local
README.md/MODEL_CARD.md's YAML frontmatter -- off by default, opt in
with --enrich on loom model/loom project/loom generate, or run it
standalone to produce a mergeable fragment:
loom model path/to/model.safetensors --enrich -o model.spdx3.json
# Standalone: writes a fragment, doesn't generate a full SBOM
loom enrich path/to/model.safetensors -o model.enrich.spdx3.json
# When merging into a project-level (not single-model) base SBOM, add:
loom enrich path/to/model.safetensors --project-dir . -o model.enrich.spdx3.jsonRegister the fragment under [tool.pitloom.fragment] and re-run
loom project/loom generate to merge it in.
Note:
--project-dir's document identity (and every spdxId in the resulting SBOM) is derived from the resolved file list, so it changes whenever that file list changes for the same project -- e.g. after a Pitloom upgrade that changes file discovery for the project's build backend (see the Source SBOM limitation above). If merging into a base SBOM generated by an older Pitloom version, regenerate that base SBOM first -- otherwise the fragment's element references won't match the base document's ids, and the merge fails outright (see below).
For prose-reading enrichment (an AI agent reading the actual README text,
not just its frontmatter), see the Agent Skills page
instead -- the sbom-enrich skill.
loom merge .spdx3-fragments/ -o combined.spdx3.jsonExits non-zero (with an ERROR: line, after a WARNING: naming each
offending reference) if any element in the merged result references an
id absent from the merge -- most commonly a fragment merged against a
stale base SBOM (see the note above). Regenerate the base SBOM and
re-run the fragment-producing step before merging again.
merge, fragment, and ids each take only their own small flag set,
not the common options below -- e.g. --offline/-v/--registry/
--enrich don't apply to any of them. merge's own --pretty also
defaults to True (pretty-printed), the opposite of every other
subcommand's compact default.
pitloom fragment validate combined.spdx3.json
pitloom fragment validate base.spdx3.json fragment.spdx3.json # + merged-graph checkChecks JSON Schema and SHACL conformance via
spdx3-validate's library
API (requires the validate extra above). Works on any SPDX 3 JSON
document, not just Pitloom's own output. Passing more than one path also
validates the graph formed by merging them, which catches type errors
across ExternalMap references -- pass --no-merge to skip that and
check each document only in isolation. Non-zero exit reports every
finding to stderr with every line ERROR:-tagged -- a SHACL violation's
Severity/Source Shape/Focus Node breakdown spans several ERROR: lines,
not just one.
Fragments are written by independent runs, so the same dataset or model
would normally get a different spdxId in each run. Pin ids ahead of
time, or reuse ids already present in an SBOM:
pitloom ids generate data src --entity model # pin ids before running
pitloom ids import existing-sbom.spdx3.json # or reuse ids from an SBOMids generate [PATH...] flags: -o/--registry FILE (registry file to
update, default .pitloom-ids.json under --project-dir), --project-dir DIR, -e/--entity NAME[:TYPE] (repeatable -- register an explicit
entity id ahead of a run; TYPE defaults to ai_AIPackage). ids import SBOM_FILE takes only -o/--registry FILE.
project/wheel/env also auto-harvest newly-minted ids back into the
resolved registry after each run (--update-registry/--no-update-registry,
on by default) -- see
Loom IDs across fragments
for what's excluded (ai_AIPackage, dataset_DatasetPackage) and why.
Available on project/generate/model/wheel/embed-wheel/env
(not merge/fragment/ids, see above), unless noted otherwise:
-o FILE/--output FILE-- explicit output path.--pretty-- indent the JSON for human reading (default: compact).--offline-- forbid network access (PyPI/Hugging Face lookups). Not onenricheither.-v/--verbose-- print effective options and where each came from.--registry FILE-- Loom ID registry file path, overriding the auto-resolved default -- see Pin ids across fragments.--describe-relationship/--no-describe-relationship-- include (or suppress) human-readable text on SPDX relationships.--content-type/--no-content-type-- detect each file's real content type via magika/mimetypes (off by default: real per-file cost).--content-type-method {auto,magika,extension}picks the detector:autotries magika and falls back to an extension guess,magikaerrors immediately if themagikapackage isn't installed,extensionskips magika entirely (stdlib-only).
See Enrich an SBOM above for --enrich/--no-enrich.
Every subcommand that writes an SBOM (project, model, env, wheel,
embed-wheel) prints PITLOOM_SBOM_OUTPUT_PATH=<path> to stdout after
writing it -- the resolved path, including when a command's own
default-naming logic picked it rather than an explicit -o. Scripts and
CI can parse this line instead of re-deriving the default-naming logic
themselves.
--debug is global -- unlike the flags above, it works before any
subcommand, including merge/fragment/ids:
loom --debug project .Surfaces DEBUG:-level diagnostics on stderr (e.g. why a metadata
extraction step was skipped) that are otherwise suppressed. Setting the
PITLOOM_DEBUG environment variable (1/true/yes/on,
case-insensitive) has the same effect and also covers entry points that
don't parse this flag themselves: the Hatchling build hook and every
public library-API function (generate_project_sbom(), etc.).
--no-debug overrides an ambient PITLOOM_DEBUG=1 back off for this
invocation -- useful when it's set globally (a shell profile, CI) and a
specific invocation should stay quiet. Omitting --debug entirely
(neither flag given) leaves PITLOOM_DEBUG as found, ambient or not.
Under the hood, --no-debug sets PITLOOM_DEBUG=0 in the process
environment for the rest of the run; this only looks scoped to "one
run" because the CLI process exits afterward. A script embedding
Pitloom's library API and calling it more than once in one long-lived
process should not rely on --no-debug/apply_debug_override(False)
to reset itself between calls -- see apply_debug_override()'s
docstring in pitloom/logging_config.py.
See Configuration for the full reference -- every
[tool.pitloom] setting, its default, and its CLI/Action/API mapping.
The sections below walk through the two settings with the most nuance.
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 project . --creator-name "Alice" --creator-email "alice@example.com"
loom project . --creator-name "Acme Corp" --creator-type organization
loom project . --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.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.
Controlled by [tool.pitloom.provenance] in pyproject.toml:
[tool.pitloom.provenance]
format = "both" # "annotation" | "comment" | "both" (default)
detail = "minimal" # "minimal" (default) | "full"
preserve-source-metadata = "auto" # "auto" (default) | "always" | "never"
max-source-metadata-bytes = 0 # 0 (default, unlimited) | a byte budgetmax-source-metadata-bytes also has a --max-source-metadata-bytes BYTES
CLI flag -- an operational override for the byte cap without editing
pyproject.toml, unlike every other key above.
See Metadata provenance for what each setting does and worked examples.
- Dependency sources and precedence -- how resolved lock files feed into Source SBOM dependencies.
- Python API -- calling Pitloom from Python code instead of the shell.
- Hatchling build hook -- generate the SBOM automatically at build time instead of a manual CLI call.
- GitHub Action -- run the CLI as a CI step.
- AI model formats -- every format
loom modelsupports, with install extras.