Code Scanning

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:

  1. It carries a usable source map — one that parses and names at least one original source, found either through its //# sourceMappingURL comment or as a <file>.map sibling. 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 (see original_location).
  2. 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 ./frontend

Findings 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 reservesnode_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 under vendor/. They are skipped wherever they appear, without comment.
  • Names that are only a conventiondeps/, third_party/, 3rdparty/, dist/, and vendor/ 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's deps/ is an overridable setting; a Kotlin package called deps is 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 them

Reserved directories stay quiet — a line about node_modules on every Node repository would be noise, not information.

Two more rules follow from this:

  • --include-vendored covers both kinds of vendored code: these dependency directories, and the vendored web assets described below. --no-ignore remains the blunt "scan everything", and is the only way to scan a reserved directory.
  • An explicit --include wins. 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-tests turns 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.

On this page