airom fs <path> walks a directory tree, runs the detectors over it, and emits an AIBOM.
Scan a directory
Unknown records and the scan continues.
airom scan ./my-ai-app resolves to the same thing: scan tries an existing local path first. Use dir: to force it.What gets walked
Paths in the output are root-relative and slash-separated, regardless of platform.
Ignore handling
Four layers decide whether a path is walked. They are evaluated in this order:1
Default skips (non-overridable)
VCS and dependency internals are always skipped:
.git/, node_modules/, vendor/, .venv/, venv/, __pycache__/, .tox/, .mypy_cache/.These live in an isolated matcher consulted before your ignore stack, so no ! re-inclusion rule can bring them back. “Always-on” is enforced, not aspirational.2
.gitignore
Standard gitignore syntax with the usual nested, per-directory semantics. A
.gitignore in a subdirectory scopes its patterns to that subtree.3
.airomignore
Same syntax, same nested semantics, read after
.gitignore in each directory — so it is applied on top and can re-include (!) what .gitignore excluded. Use it for AIROM-specific exclusions you don’t want in .gitignore.4
--ignore globs
Repeatable doublestar globs matched against root-relative paths, applied on top of the ignore-file stack. An invalid glob is a startup error, not a silent no-op.
Prune fixtures and test data
**/fixtures/** will not descend into fixtures/.
Case folding on macOS and Windows
Case folding on macOS and Windows
On macOS and Windows, ignore matching folds case, mirroring git’s default
core.ignorecase=true on those platforms. AIROM skips what your git skips.Known limitation: POSIX character classes
Known limitation: POSIX character classes
POSIX character classes (e.g.
[[:digit:]]) are not supported in ignore patterns by the underlying matcher. Use plain gitignore globs instead.Ignored files are never read
Ignored files are never read
An ignored path is excluded from both walk phases. A detector that queries files by glob during the project phase sees exactly what the walk saw — it can never reach behind an ignore rule.
Frozen applications
A PyInstaller onefile executable is a blind spot with no partial answer: every module is compiled into a compressed PYZ appended to the bootloader, so there is no.py, no .pyc, and usually no dist-info anywhere on disk. Import rules
and manifest detectors both find exactly nothing.
AIROM reads the archive itself — the CArchive cookie at EOF, its table of
contents, and the PYZ module list — and reports the AI packages inside. The
version comes from whichever of three sources can answer, and the evidence says
which:
That third row is deliberate. The package is demonstrably in the binary, and
staying silent about it would read as absence. Some packages ship no metadata at
all, so the
__version__ path is the only way they can ever be versioned.
No code object is ever unmarshaled. Only the handful of
marshal types a PYZ
directory can contain are implemented, and everything else — TYPE_CODE above
all — is refused. Version strings are recovered from decompressed bytes
instead. marshal on untrusted input is a code-execution surface, and a
scanner that reads other people’s executables must not be the thing that runs
them.Server-side database extensions
A vector database is not always something a project imports. pgvector is a PostgreSQL extension: it shows up in DDL, and otherwise only as files the server installation left on disk. A host running it has no manifest entry and no import for a source rule to match. AIROM reads the extension’s own control file —share/extension/vector.control
— which names the version outright, and the loadable module beside it:
All three fold into one component, so a host with the extension installed and a
schema that uses it reports pgvector 0.8.1 once, with every sighting behind
it, rather than three partial findings.
default_version is the version installed on disk. A database created
before an upgrade keeps running the older one until ALTER EXTENSION ... UPDATE, so the two can differ — the evidence string says so rather than
implying the running database was inspected.vector.so anywhere else is far too generic to claim a database
extension from.
Bounding the scan
Peak memory is a function of these caps, never of the size of the tree.string
default:"1m"
Full-content read cap for text detectors. Accepts
k/m/g suffixes. Header-only binary parsers (GGUF, safetensors, …) are exempt — a 40 GB model file still costs only a header read.string
default:"256m"
Byte-weighted I/O semaphore budget, independent of CPU parallelism. Accepts
k/m/g suffixes.int
default:"GOMAXPROCS"
Detector worker count.
Tune for a large monorepo
Narrowing the detectors
--select takes a detector selection expression. Bare names set the base set; + adds and - removes.
Python detectors, plus GGUF, minus dataset
See the effective selection
Filtering the results
Extension-only dataset detection and keyword-onlyai-config detection are deliberately shallow, and on a general-purpose directory they emit low-confidence (0.5–0.6) noise. --min-confidence 0.8 is the practical filter.
Scan, filter, write JSON
--min-confidence is a presentation-layer filter — it changes what is emitted, not what was detected. See /concepts/confidence.
Example
Default table output