| Created | 2026-08-11 |
|---|---|
| Last-Modified | 2026-10-30 |
| 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 as loom and pitloom;
they run the same tool.
See also: SBOM from a Git URL for loom project <url>,
Wheel SBOMs and PEP 770 embedding,
SBOM fragments and Loom ID registry, which hold the
embed-wheel, verify-wheel, validate-wheel, merge, fragment and id
subcommands.
pip install pitloom
loom project . # SBOM for the Python project in the current dirloom -h shows the full option list.
pip install pitloom
pip install "pitloom[ai]" # AI model metadata extraction
pip install "pitloom[content-type]" # content type detection (magika)
pip install "pitloom[validate]" # SPDX 3 schema/SHACL validation (loom fragment validate, loom validate-wheel)Generate a Source SBOM for a Python project (default: the current directory):
loom project .
loom project /path/to/project -o sbom.spdx3.json
loom project https://gitlab.com/group/project # a Git URL: see belowA target that is the http or https URL of a Git repository, or of one of its
web pages (a branch, tag or commit and a folder may be named), is fetched
shallowly into a temporary directory and read as a project; loom generate takes
the same URL. The repository is untrusted: its own [tool.pitloom] is ignored
(--config FILE applies) and its build backend is never run. --offline is an
error for it. Every redirect or change of the URL is an INFO: naming both URLs,
and a URL that is refused or not followed is a WARNING: or ERROR:. See
SBOM from a Git URL for the URL forms, the platforms checked and
the messages.
Limitation: the per-file inventory (file list and hashes) is backend-aware and accurate for Flit-core, PDM-backend, Poetry, Hatchling, setuptools and uv_build (uv_build needs
--allow-build). Apyproject.tomldeclaring nobuild-backendis read as setuptools, as PEP 517 build frontends build it. Other backends (maturin, scikit-build-core, meson-python, ...) fall back to a heuristic and log aWARNING:.
If a lock file (pylock.toml, uv.lock, poetry.lock, pdm.lock,
Pipfile.lock, or a fully pinned requirements.txt) sits next to
pyproject.toml (or setup.py, for Pipfile.lock/requirements.txt), its
resolved transitive dependencies join the dependency list. This is on by
default; --no-use-lockfile (or [tool.pitloom] use-lockfile = false) falls
back to direct dependencies and environment introspection. See Dependency
sources and precedence for which lock file wins and
what counts as "resolved".
Generate an Analyzed SBOM from a built wheel (bundled binaries become phantom dependencies):
loom wheel path/to/mypackage-1.0.0-py3-none-any.whl -o sbom.spdx3.jsonTo embed SBOMs in wheels, check them, or recompute a package hash, see Wheel SBOMs and PEP 770 embedding.
Generate a Deployed SBOM of the installed environment graph:
loom env -o env.spdx3.jsonGenerate an Analyzed SBOM for a single AI model file, with no Python
project directory. Supported formats: GGUF, ONNX, Safetensors, PyTorch
(.pt/.pth, PT2/ExecuTorch .pt2), Keras, HDF5, NumPy, fastText,
CRFsuite; see AI model formats for extensions and
install extras:
loom model path/to/model.safetensors -o model.spdx3.json
loom model path/to/model.gguf --prettyA model whose read fails (truncated, a bound exceeded, a missing extra) is
still written as a format-only entry, with one WARNING: and exit 0, as a
project scan lists it. A file that is not a model (empty, an unknown format, a
header contradicting the suffix, or a Git LFS pointer, whose ERROR: gives
the oid, the stated size and git lfs pull), absent or unreadable is an
ERROR: and exit 1. See
AI model scan limits.
Or pass a Hugging Face Hub URL or model ID, with no local file (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 worksA Hugging Face Space or dataset is a Git repository, not a model: give
its URL to loom project (SBOM from a Git URL); loom model
refuses it and names loom project.
Or let generate detect 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 SBOMgenerate requires -o/--output: it dispatches across several target
types with no single natural default filename, unlike project/wheel/
model/env. Pass -o, or use the target-specific command.
When a directory has both, Pitloom follows setuptools' own precedence (see
setupcfg.py):
a setup() keyword is used, and the same setup.cfg option only when the
keyword is empty ("", [], {}, None, an install_requires of only
comments) or absent. It is decided per option (author and author_email
apart, url and project_urls apart), and a list replaces the other, never
joins it. The name is setup.py's literal, else setup.cfg's. Differences
from setuptools:
setup.pyis read, never run. A keyword that is not a literal (name=NAME), is blank (" ") or holds a value Pitloom does not read (url=1) is ignored with aWARNING:, andsetup.cfg's is used.- A
setup.cfgfile:directive, and[tool.setuptools.dynamic]version = {file = ...}, read a file only inside the project directory and up to 1 MiB; another file is skipped with aWARNING:. Requirements files follow Dependency sources. - A placeholder licence (
UNKNOWN,NOASSERTION) insetup()gives way to the licence classifier kept, else to thelicenseofsetup.cfgit overrode; setuptools keeps the placeholder.NONEis a statement. - When
setup.pyoverrides a different real name, licence, version orpython_requiresofsetup.cfg, Pitloom keepssetup.py's, records the other as a conflict annotation and gives oneWARNING:per field. For the licence, a reallicensefield in either file beats a classifier, as in the built wheel.
Only a directory is read this way; an sdist or wheel carries the metadata
setuptools already merged. With detail = "full" provenance, an author or URL
entry built from both files names both, setup.py first:
Source: setup.py, setup.cfg | Field: setup(author=...), metadata.author/author_email.
Fill AI-model metadata gaps (licence, datasets) from a local
README.md/MODEL_CARD.md's YAML frontmatter (read up to 1 MiB: Size and
count caps). Off by default: opt
in with --enrich on loom model/project/generate, which sets the model's
licence and datasets, or run it standalone to produce a mergeable fragment.
The fragment carries the datasets and their trainedOn link; the card's
licence appears only in its enrichment annotation:
loom model path/to/model.safetensors --enrich -o model.spdx3.json
# Standalone: writes a fragment, not 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. For an AI agent that reads the README
prose, not just its frontmatter, use the sbom-enrich skill (Agent
Skills).
Note:
--project-dir's document identity (and every spdxId in the resulting SBOM) derives from the resolved file list, so it changes whenever that 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). When merging into a base SBOM from an older Pitloom, regenerate the base first, or the fragment's element references will not match the base's ids and the merge fails outright.The same applies to
--use-lockfile/--no-use-lockfile: the identity also depends on whether the lock-file cascade ran.loom enrich --project-dir DIRmatches DIR's own[tool.pitloom] use-lockfilewhen no flag is given, so pass one only if the base SBOM was generated with an explicit CLI override that disagreed with that config.
loom merge, loom fragment validate and loom fragment list: see SBOM
fragments.
These take their own flags, besides the shared ones of Useful
flags where noted (embed-wheel takes both):
| Command | Flag | Meaning |
|---|---|---|
embed-wheel |
--sbom FILE |
Embed a pre-generated SBOM instead of generating one; invalid JSON, a non-object or a .spdx3.json that is not SPDX 3 is an ERROR: |
embed-wheel |
--sbom-basename NAME |
Name of the embedded file under .dist-info/sboms/, without extension |
embed-wheel |
--allow-mismatch |
With --sbom: embed with a WARNING: when its name/version differs from the wheel's |
embed-wheel |
--verify / --validate |
Run verify-wheel / validate-wheel on the result; with --sbom, --validate checks the file first |
verify-wheel |
--sbom-filename NAME |
The exact file expected under .dist-info/sboms/ |
verify-wheel |
--fail-on-mismatch |
Exit non-zero (not only WARNING:) when the SBOM's name/version differs from the wheel's |
validate-wheel |
--sbom-filename NAME |
The exact file expected under .dist-info/sboms/ |
merge |
-o FILE, --pretty / --no-pretty |
Output path and indentation |
fragment validate |
--no-merge |
Validate each document alone, skipping the merged-graph check |
fragment list |
--project-dir DIR |
The project whose configured fragments are listed |
id generate, id import |
see below | Pin ids across fragments |
Details: Wheel SBOMs and PEP 770 embedding and SBOM fragments.
Independent runs give the same dataset or model different spdxIds. Pin ids
ahead of time, or reuse ids from an existing SBOM:
loom id generate data src --entity model -o loom-id-registry.json
loom id import existing-sbom.spdx3.json -o loom-id-registry.jsonid import roots a new registry at --project-dir (default: the current
directory), the directory the SBOM's file names are relative to; run from above
the project without it, a later run of the project finds no file ids. id generate also takes --namespace IRI, -e NAME[:TYPE] (a file, model
or dataset that exists below --project-dir is stored by its path with its
content hash, else by name; a package is NAME@VERSION, one release) and
--max-dataset-hash-bytes BYTES (the most
bytes read to hash one dataset; default: the project's own
max-dataset-hash-bytes, else 4 GiB), and both
commands write only a changed registry. The file is JSON (version: 4); a
version 3 or 2 file from an earlier release is an ERROR: (exit 1) on every
command, left as it is: delete it and re-run. Format, flags and log lines:
Loom ID registry; keys: keys and content
gates.
Full reference, including the --id-registry precedence and automatic harvest
of new ids (--update-id-registry): Loom ID registry.
Available on project/generate/model/wheel/embed-wheel/env (not
merge/fragment/id, which take only their own flags), unless noted:
-o FILE/--output FILE-- explicit output path.--config FILE-- read[tool.pitloom]from FILE instead of the target's ownpyproject.toml. On a project target (project,generateon a project directory or sdist,embed-wheel --project-dir) it replaces the project's own config outright, not merges with it. On every other target (wheel,env,model,enrich,embed-wheelwithout--project-dir) it is the only config that target can get: none of them read the current directory or the target's own location. A relative path inside FILE (id-registry, a fragment'spath) resolves against FILE's directory. A missing or invalid FILE is anERROR:, except underembed-wheel --sbom, where it is not read and only warns. The replaced project config is not parsed, so--configalso rescues a project or sdist whose own[tool.pitloom]is invalid. See Where settings come from for the precedence table.--pretty-- indent the JSON (default: compact). Only whitespace between tokens is added; key order and values are the compact (RFC 8785) text's.--offline/--no-offline-- forbid network access (PyPI/Hugging Face lookups), or, with--no-offline, allow it over a configoffline = true. Not onenricheither.--use-lockfile/--no-use-lockfile-- only onproject/generate(the other targets never read a lock file) andenrich(for--project-dirdocument identity matching, see Enrich an SBOM). On by default; see Dependency sources and precedence.-v/--verbose-- onproject/generatewith a project directory or sdist: log each effective option on stderr asINFO: OPTION=<name> SOURCE=<source> VALUE=<value>(an sdist's ownpyproject.tomlsource is its member, e.g.demo-1.0.0.tar.gz:pyproject.toml).wheel/env/model/enrichlogPITLOOM_VERSION, the target,OUTPUT_PATH(-: none) and thePROJECT_DIRofenrichandwheel.generateon any other target andembed-wheelwarn that-vhas no effect.--project-dir DIR-- onwheel, andproject/generatewith an sdist or wheel target: the project directory the archive was built from. With a Loom ID registry rooted above the project (a monorepo), the archive's files are keyed below DIR's path from the registry's root, asembed-wheel --project-dirkeys them, so they reuse the idsid generateorprojectrecorded. Nothing else is read from DIR. A DIR that is not a directory is anERROR:; without a registry, or for a project directory target, it warns that it has no effect. See Monorepo.--id-registry FILE-- declare a Loom ID registry, taking precedence over the target's ownid-registrykey. Nothing is searched for; a declared file that is missing, unreadable or invalid is anERROR:and exit 1. See Loom ID registry.--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, then an extension guess;magikaerrors at once if magika is not installed;extensionskips magika (stdlib-only).--scan-model-usage/--no-scan-model-usage-- on a project directory (project,generate <dir>,embed-wheel --project-dir, and the Hatchling hook via thescan-model-usagekey) or a built wheel (wheel,wheel --embed,embed-wheelwithout--project-dir), also record which Python files reference each discovered AI model file (hasDataFile). Off by default: it reads every Python file. AI models are found either way. When the setting was never given (no flag, no config key), oneINFO:line says how many were found and names the flag (on a wheel also a--configfile orpitloom_config=, as no config is read implicitly there), once perembed-wheelrun. Caps and what it misses: AI model scan limits.--allow-signed-wheel--embed-wheelandwheel --embed: embed into a wheel with aRECORDsignature by removing the signature. See Signed wheels. No[tool.pitloom]equivalent.--trust-wheel-model-- on a built wheel (wheel,wheel --embed,embed-wheelwithout--project-dir), also read the AI model formats that are otherwise listed without metadata in a wheel, because a hostile file can crash, hang or exhaust memory in their readers. Use it only for a wheel you trust. Withembed-wheel --project-dirthe models are read from the project, not the wheel. No config key, so no config file can opt in. Which formats, and why: Formats gated in wheels; see also Settings that change the SBOM.--max-model-extract-bytes BYTES-- on the same wheel targets, the largest model file copied out to be read (four times it for the wheel); beats--config'smax-model-extract-bytes(512 MiB by default). A positive integer:0or a negative value exits 2, even where it has no effect.
--enrich/--no-enrich: see Enrich an SBOM.
--allow-build/--no-build-isolation/--build-timeout (only on project/
generate/embed-wheel): see Building a project to discover its file
list, an off-by-default, security-relevant opt-in with no
[tool.pitloom] equivalent.
A subcommand's parent parser offers every shared flag to every target, but not
every target can act on every one: a wheel has no source files to scan a
header from, and a wheel-embedded SBOM is always canonical JSON regardless of
--pretty. A flag that does not apply prints one WARNING: Options: <target>: <flag> has no effect <reason> and is dropped, never silently ignored:
| Target | Options that warn |
|---|---|
| project directory | --trust-wheel-model, --max-model-extract-bytes, --project-dir |
| remote Git repository | --trust-wheel-model, --max-model-extract-bytes, --project-dir |
| sdist archive | --enrich, --extract-file-header, --content-type, --scan-model-usage, --trust-wheel-model, --max-model-extract-bytes, --use-lockfile |
| wheel | --enrich, --extract-file-header, --content-type, --use-lockfile |
| wheel --embed | --pretty, --describe-relationship, --enrich, --extract-file-header, --content-type, --update-id-registry |
| installed environment | --enrich, --extract-file-header, --content-type, --scan-model-usage, --trust-wheel-model, --max-model-extract-bytes, --use-lockfile, --project-dir |
| local model file | --extract-file-header, --content-type, --scan-model-usage, --trust-wheel-model, --max-model-extract-bytes, --content-type-method, --offline, --use-lockfile, --update-id-registry, --project-dir |
| Hugging Face model | --enrich, --extract-file-header, --content-type, --scan-model-usage, --trust-wheel-model, --max-model-extract-bytes, --content-type-method, --use-lockfile, --id-registry, --update-id-registry, --project-dir |
| enrich --project-dir | --describe-relationship, --extract-file-header, --content-type, --scan-model-usage, --trust-wheel-model, --max-model-extract-bytes, --content-type-method, --max-source-metadata-bytes, --update-id-registry |
| enrich without --project-dir | --describe-relationship, --extract-file-header, --content-type, --scan-model-usage, --trust-wheel-model, --max-model-extract-bytes, --content-type-method, --max-source-metadata-bytes, --use-lockfile, --update-id-registry |
| embed-wheel --project-dir | --pretty, --describe-relationship, --trust-wheel-model, --max-model-extract-bytes, --update-id-registry |
| embed-wheel without --project-dir | --pretty, --describe-relationship, --enrich, --extract-file-header, --content-type, --update-id-registry |
| Hatchling build hook | --describe-relationship, --update-id-registry |
| embed-wheel --sbom | --pretty, --describe-relationship, --enrich, --extract-file-header, --content-type, --scan-model-usage, --trust-wheel-model, --max-model-extract-bytes, --content-type-method, --max-source-metadata-bytes, --offline, --id-registry, --update-id-registry, --creator-*, --config, --project-dir |
Each --flag also covers its --no-flag form where one exists (e.g.
--no-enrich); the warning names both, e.g. --enrich/--no-enrich.
The Hatchling build hook row is not a command: the hook reads
[tool.pitloom], not flags, so a key set to true there is noted with one
INFO: (same wording, the flag's spelling) rather than a warning. false
agrees with what the hook does and is not noted. embed-wheel --project-dir
does the same for a key in the project's [tool.pitloom], except one whose
flag was also given (that flag has its own warning).
The --describe-relationship warning on embed-wheel/enrich is a current
decision, not a permanent one. --use-lockfile is offered only by project,
generate and enrich; the rows list it for the targets those commands (or
the library's use_lockfile=) can reach without a lock-file cascade.
--allow-build/--no-build-isolation/--build-timeout have their own
no-effect warning: see Building a project to discover its file
list.
Stdout is data only (--help/--version aside), one KEY=VALUE record a
line; counts, hints and -v go to stderr as INFO:. A written SBOM prints
PITLOOM_SBOM_OUTPUT_PATH=<path>; embed prints WHEEL=<w> SBOM=<arcname>.
With -o - stdout is the SBOM alone (loom project . -o - | jq .): no path
line, and the embed record is an INFO: line. Others: verify-/
validate-wheel WHEEL=<w> STATUS=ok|valid|skipped|failed; fragment validate FILE=<f> STATUS=valid; id PITLOOM_ID_REGISTRY_PATH=<path>. A
value with a non-printable character (tab, NBSP, ZWJ) prints quoted,
ASCII-escaped. A catalogued stderr line ends in a stable [code]: see
Messages and codes.
Without -o, an SBOM is written to the current directory:
| Command | File name |
|---|---|
project (also a Git URL) |
<name>-<version>.spdx3.json; <name>.spdx3.json when the project has no version; sbom.spdx3.json when it has no name or the name is not a safe file name (con). A character no file name may hold (a path separator, :, a control character, whitespace) becomes _. [tool.pitloom] sbom-basename replaces the base name. |
wheel |
<wheel file name>.spdx3.json, e.g. demo-1.0-py3-none-any.whl.spdx3.json; with --embed, no file unless -o is given |
model |
<model file name>.spdx3.json; for a Hugging Face model, the last segment of its ID plus .spdx3.json |
enrich |
<model file name>.enrich.spdx3.json |
env |
deployed-environment.spdx3.json |
merge |
merged.spdx3.json |
generate |
none: -o is required |
--debug is global: unlike the flags above, it works before any subcommand,
including merge/fragment/id:
loom --debug project .It surfaces DEBUG:-level diagnostics on stderr (e.g. why a metadata
extraction step was skipped). Setting the PITLOOM_DEBUG environment variable
(1/true/yes/on, case-insensitive) has the same effect and also covers
entry points that do not parse this flag: 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 is set globally (a shell profile, CI). Omitting
both flags leaves PITLOOM_DEBUG as found. --no-debug sets
PITLOOM_DEBUG=0 in the process environment for the rest of the run, which
looks scoped to one run only because the CLI process then exits. A script
calling the library API repeatedly in one long-lived process should not rely
on --no-debug/apply_debug_override(False) to reset between calls; see
apply_debug_override()'s docstring in pitloom/logging_config.py.
Ctrl-C ends any command with one ERROR: interrupted line and exit status
130. With --debug (or PITLOOM_DEBUG) the Python traceback follows, showing
where the command was interrupted: useful for a run that seems stuck.
See Configuration for every [tool.pitloom] setting, its
default and its CLI/Action/API mapping. Two settings need more explanation.
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 the SBOM (default
"Pitloom", repeatable; --no-creation-tool omits it).
--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 . --creator-name "Acme Corp" --creator-type organization --creator-name Alice
loom project . --creator-name "My CI service" --creator-type software-agent
loom project . --creation-datetime "2026-01-15T10:00:00Z" --creation-comment "CI run #123"A software-agent creator (a service that runs Pitloom) is the SBOM's author;
it is never the main package's supplier, which stays the first person,
organisation or generic-agent creator, or none.
The same fields can be set in pyproject.toml. 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.
Set by [tool.pitloom.provenance] (keys and defaults:
Configuration; what each does:
Metadata provenance). Of its keys, only
max-source-metadata-bytes has a flag, --max-source-metadata-bytes BYTES,
to change the byte cap for one run. --max-model-extract-bytes BYTES does
the same for [tool.pitloom] max-model-extract-bytes, the per-model ceiling
of a wheel's model scan (AI model scan
limits).
- Wheel SBOMs and PEP 770 embedding --
embed-wheel,verify-wheel,validate-wheel, the package hash. - Building a project to discover its file list -- the
--allow-build/--no-build-isolation/--build-timeoutreference. - Dependency sources and precedence -- how lock files feed Source SBOM dependencies.
- SBOM fragments and Loom ID registry.
- Python API -- the same targets from Python code.
- Hatchling build hook and GitHub Action -- generate SBOMs at build time or in CI.
- AI model formats -- every format
loom modelsupports, with install extras.