File Filtering
Which files xgrep scans by default and how to control it — git-tracked files, ignore patterns, globs, and size limits.
File Filtering
xgrep decides which files to scan from the target you point it at. The defaults favor completeness over speed-by-omission, with flags to opt into Semgrep-style filtering when you want it.
What gets scanned
By default xgrep scans only the git-tracked files under the target
(git ls-files --cached), which automatically respects .gitignore. Untracked
files — those present in the working tree but not yet committed — are skipped, so
local data dumps, build output, and scratch files don't bloat the scan. Add
--all-files to also scan untracked-but-not-ignored files (useful for reviewing
new code before you commit it):
xgrep scan . # tracked files only (default)
xgrep scan --all-files . # tracked + untracked (still respects .gitignore)When the target isn't a git repository, xgrep falls back to a filesystem walk
(scanning everything not ignored). Narrow or widen the set further with
--include / --exclude globs.
Files larger than 1 MB are skipped by default (--max-target-bytes, matching
Semgrep's default; pass 0 for no limit). Compiled formats are the exception —
a class file, a .NET assembly, a .pyc or a native executable is read up to
256 MB, because those are parsed by their own readers rather than as source and
their cost does not grow with the file's size. A per-file 60s timeout guards
against a single pathological file (set --timeout, or --timeout 0 to disable).
A conservative set of dependency/build/lockfile directories is ignored by
default (node_modules/, vendor/, deps/, dist/, *.min.*, lockfiles,
…); --include-vendored scans the dependency directories, --no-ignore scans
everything, and --xgrepignore adds the opinionated test/docs/example set on
top. A directory is only treated as a dependency tree when its name is one the
language reserves, or when there is evidence it holds dependencies rather than
your own code — so a first-party package that happens to be called deps is
still scanned. All of this is explained in
Design decisions below.
Machine-generated files (Code generated … DO NOT EDIT, @generated) and binary
files are skipped by default; use --include-generated to scan generated files.
Some generators emit large, low-signal output without a DO NOT EDIT marker —
these are recognized by their preamble instead: tree-sitter parsers
(tree_sitter/parser.h plus the generated STATE_COUNT/LANGUAGE_VERSION
table defines), GNU Bison, and flex. Detection is content-based, so a
hand-written external scanner that merely includes parser.h is still scanned.
Minified files and bundles
A file is treated as minified when its first 64 KB contains a line over
7,000 bytes, averages over 700 bytes per line, or is under 7% whitespace (the
last catches densely-packed bundles with short lines, like
react.production.min.js). This decision is content-based, never name-based —
xgrep looks at the bytes, not the filename, so a renamed or inlined bundle is
still recognized and a hand-written file that happens to be named *.min.js is
still ordinary source.
Minified files are skipped by default, with one exception: your own build output is scanned. A minified file is scanned when both of these hold:
- It carries a usable source map — one that parses and names at least one
original source, found either through its
//# sourceMappingURLcomment or as a<file>.mapsibling. The map is what lets every finding in the file be reported at a line of the code you actually edit, instead of a column of generated text (seeoriginal_location). - It is not a vendored third-party asset — not a library sitting in a vendor
directory under a static root, and not opening with a
/*!library banner.
Both are needed, because libraries ship source maps too: bootstrap's npm dist/
carries bootstrap.min.js alongside a full bootstrap.min.js.map. The first
test on its own would let every vendored library back in.
# scans dist/main.a1b2.js (your bundle) and reports each finding
# at its line in src/, but still skips vendored bootstrap.min.js
xgrep scan ./dist
# audit everything minified, vendored libraries included
xgrep scan --include-minified ./frontend
# skip everything minified, your own bundles included
xgrep scan --exclude-minified-files ./frontendFindings in vendored minified code are rarely actionable — the code is unfixable
in place and the fix is always "upgrade the dependency" — and such files are by
far the most expensive to scan. That cost is real for your own bundles too: a
bundle of a few hundred kilobytes can exceed the per-file time budget, in which
case the scan warns that findings were dropped. Reach for
--exclude-minified-files if a bundle is costing more than it is telling you.
Run with --verbose to see which minified files were skipped and why.
One caveat if you name your own bundle *.min.js: that glob is in the default-on
ignore set, so such a file is dropped during the directory walk before the rule
above is ever consulted. Scan it with --no-ignore, or emit your build output
under a name that does not end in .min.js.
The skip is applied by default at every scan entry point — the CLI, the MCP
scan tool, and the library scan.Options — so minified and generated files
never slip back in unless you opt in. For Semgrep compatibility,
--no-exclude-minified-files is an alias for --include-minified.
Design decisions
Two ignore tiers: a default-on noise set, and an opt-in opinionated set. xgrep splits the built-in ignore patterns:
- Default-on (disable with
--no-ignore): dependency, build, and generated output that is never the executable surface under review —.git/,node_modules/,vendor/,deps/,dist/,dev/breeze/,*.min.js,*.min.css, lockfiles (*.lock,go.sum,package-lock.json,yarn.lock). Skipping these avoids walking and regex-scanning large generated trees — the dominant cost on a big repo. (build/is deliberately not here: some projects keep build-system source under it.) - Opt-in (
--xgrepignore): the opinionated set that can legitimately hold findings — test directories, docs, and examples. xgrep does not skip these by default, because test files are valid SAST targets: vulnerabilities in test fixtures can indicate real patterns, and test code often contains hardcoded credentials or copy-pasted production code.
--no-ignore disables both tiers ("scan everything").
A dependency directory is identified by ownership, not just by name. Two different things get called "vendored", and xgrep treats them differently because only one of them is unambiguous:
- Names the language reserves —
node_modules/,vendor/inside a Go module,__pycache__/,site-packages/. The toolchain owns these, so your own code cannot live there:go list ./...will not name a package undervendor/. They are skipped wherever they appear, without comment. - Names that are only a convention —
deps/,third_party/,3rdparty/,dist/, andvendor/outside a Go module. These are a tool's default setting or a habit, and nothing stops a first-party package from using one. Elixir'sdeps/is an overridable setting; a Kotlin package calleddepsis just as legitimate. So xgrep skips one only when there is evidence it holds dependencies: a package manager wrote a record inside it (vendor/modules.txt,vendor/composer/installed.json), its subdirectories are themselves packages with their own manifests, or it sits at the top of a repository or module. Otherwise it is scanned as your code.
When a conventional directory is skipped, the scan says so:
note: skipped 62 file(s) in 1 directory as vendored third-party code (deps); pass --include-vendored to scan themReserved directories stay quiet — a line about node_modules on every Node
repository would be noise, not information.
Two more rules follow from this:
--include-vendoredcovers both kinds of vendored code: these dependency directories, and the vendored web assets described below.--no-ignoreremains the blunt "scan everything", and is the only way to scan a reserved directory.- An explicit
--includewins. If you name a path, xgrep scans it —--include 'deps/**'reaches a tree that would otherwise be skipped, because naming a path is a stronger signal than any built-in default.
Production scope: findings about the executable surface are dropped in test paths. File selection and finding reporting are separate steps. xgrep still scans test, spec, fixture, and example files (above), but by default it drops the findings located in those paths from the report — a vulnerability in a test fixture is rarely exploitable in production, and reporting it is noise that buries real bugs. The same reasoning covers the other categories that describe what the deployed program does: a dead store, an allocation inside a loop, or a field read without the lock that guards it is a defect in shipped code, and in a unit test it is neither deployed nor usually load-bearing. So security, correctness, performance and concurrency findings are all scoped this way. Two deliberate exceptions keep it from hiding anything that matters:
- Secrets are always kept, wherever they live — a credential committed to a test file is just as compromised as one in production code.
--include-teststurns those findings back on, for when the test code itself is the surface you're reviewing — a flaky test caused by a race, or a slow suite, is a real problem, just not the one a scan of the deployed artifact is reporting.
Scoping is always announced, never silent. A scan that narrows itself prints
note: skipped N file(s) as out of production scope … to stderr, and reports the
same file count in the machine-readable formats — run.scan.files_skipped_out_of_scope
in --json and the files-skipped-out-of-scope invocation property in --sarif
(see output formats). All three are omitted when nothing was
skipped, so a count appearing always means the scan covered less than the path you
pointed at. It counts files, not findings: the rules are skipped before evaluation,
so there is no suppressed-finding number to report.
This is distinct from --xgrepignore, which removes the files from the scan
entirely; production scope scans them but filters their non-secret findings.
Pass --xgrepignore to opt into the built-in ignore set, which skips non-source
surfaces: test/, benchmark{,s}/, eval{,s}/, example{,s}/, docs/, vendor/,
node_modules/, dist/, build/, .github/, lockfiles, and *.{md,test.js,spec.ts,bench.ts}
and similar. To customize the set, drop a .xgrepignore file (one glob per line, #
comments allowed) at the scan root; a .semgrepignore file is honored as a
fallback. The first such file that exists is authoritative — a present-but-empty
(or comment-only) file is an explicit "ignore nothing extra" override that keeps
the flag active while disabling the built-in patterns. --semgrepignore remains
as an alias for --xgrepignore.
This is the recommended flag for embedders scanning a focused executable surface
(e.g. an AI-agent skill: a SKILL.md plus scripts/), where benchmark/eval/example
trees are noise rather than the code under review.
Max file size. Source files over --max-target-bytes (1 MB by default) are
skipped: the AST and taint matchers scale with a file's size, and a handful of
multi-megabyte bundles can exhaust a whole-repo scan budget. Raise it, or pass
0 for no limit, when you need large files covered — the engine invests in
being performant on them (per-function scoping, literal pre-checks,
context-based timeouts) rather than in the cap.
Compiled formats are exempt, up to 256 MB. A class file, a .NET assembly, a
.pyc and a native executable are not parsed as source; each is read by its own
reader, which bounds how much of one file reaches the rules however large the
file is. Applying a source-shaped cap to them would skip every artifact worth
reading — real native binaries are all larger than 1 MB — and the scan would
exit cleanly having read nothing.
See Semgrep compatibility for the full parity matrix.
Kotlin
What xgrep detects in Kotlin — injection, XSS, unsafe deserialization, weak crypto, insecure TLS, open redirect, and ReDoS — for Android apps and JVM backends.
Semgrep/OpenGrep compatibility
Feature parity between xgrep's CLI and Semgrep / OpenGrep — subcommands, flags, output, and xgrep-only extras.