Filter precedence

FileConcat decides which files end up in your output through a four-layer pipeline. Each layer runs in order and the next one only sees what survived the previous. Knowing the order makes it easier to predict why a file appears or disappears when you type a pattern.

The four layers

  1. Drop-time prune runs at ingestion.
  2. Validation cache runs once per file at ingestion.
  3. Pattern layer runs live every time you edit a textarea.
  4. Manual overrides are sticky and applied last.

The pipeline is deterministic. Same inputs produce the same included decision for every file.

Layer 1: drop-time prune

Two kinds of path are left out before a byte of them is read:

  • A directory the default ignore list names by itself: .git, node_modules, vendor, dist, build, target, __pycache__, .venv, coverage, .next and the rest of the plain names in that list. The walker that traverses a dropped folder never enters one, and the folder picker drops their files from its list.
  • A file whose extension never holds text: fonts, audio and video, compiled code and libraries (.o, .dll, .class, .pyc, .jar), databases, and data or model files. Images are not on this list: a dropped image gets the recognition offer instead.

Until 2026-09-15 only .git and node_modules were pruned here, and everything else was read into memory first and hidden by the pattern layer. A file that never reaches the bundle still cost a read and a classification, and the largest drops were mostly such files.

The folder you drop is exempt: drop dist by itself and it is read, whatever it is called, and the result's ledger says it was read because you chose it. Only what sits inside a drop is judged by name. The ledger names what this layer turned away (the folders, and the files by extension), and a drop turned away whole says so on the empty screen.

An archive in the drop is opened and its contents meet the same door, with the archive standing as their root: build.zip is opened whatever it is called, and the fonts/ and node_modules/ inside it are turned away like the ones beside it. Until 2026-09-15 an archive's contents skipped this layer, so a zipped build tree was read in full while the unzipped one next to it was not.

What this layer drops cannot be turned back on from the drawer, because it never entered memory. Every glob and every plain file name in the default list (*.log, *.test.ts, package-lock.json) runs at filter time instead, and those can be. The test is prunedAtWalk in packages/core/src/path-utils/skip-paths.ts.

Layer 2: validation cache

When a file is ingested, FileConcat checks two things and records the result in a sticky validations map:

  • Is the content binary? Genuine binaries (images, archives) are excluded permanently. Extractable documents (PDF, Word, Excel, PowerPoint, and OpenDocument files) are the exception: their text is pulled out and included as text, right in the browser, instead of the file being dropped. Jupyter notebooks, .srt / .vtt caption tracks and saved .eml messages take the same path for the opposite reason: they are already text, but the text is a serialization, so what lands in the bundle is the document rather than the file. When a document carries no recoverable text (a scanned image-only or encrypted PDF), it is surfaced as excluded rather than silently dropped.
  • Does the file exceed the configured max size (default 32 MB)? Oversize files are excluded permanently.

This check runs once per file. Pattern edits do not retrigger it, which is why pattern changes feel instantaneous even on large drops.

Layer 3: pattern layer

Two textareas in the Adjust what's included drawer drive this layer:

  • Include is a comma-separated list of glob patterns. Empty means every surviving file passes.
  • Ignore is a comma-separated list of glob patterns. Files matching any of them are excluded.

There is one rule that surprises people: if the Include textarea has any non-empty content, the Ignore textarea is skipped entirely. The include list is treated as an exclusive whitelist. To go back to "every file except these", you have to clear the Include textarea.

Pattern matching uses the pathMatches helper in packages/core/src/path-utils/glob-match.ts. Two semantics matter:

  • Bare directory or filename patterns match anywhere in the path. Typing node_modules matches apps/web/node_modules/react/index.js, not just a top-level node_modules. The same goes for *.log, matching every .log file at any depth.
  • Slashed patterns match the full path or any path suffix obtained by stripping leading directories. Typing src/**/*.ts matches src/app.tsx and also myrepo/src/app.tsx. This lets patterns work when you drop a folder whose name you do not want to type.

See File filtering for the pattern syntax in more detail.

Layer 4: manual overrides

Every checkbox in the drawer's file tree writes to a sticky map called userToggled. An entry for a path can be "include" or "exclude", and either value wins over whatever the pattern layer decided.

The map only stores divergences. If you tick a checkbox so that the file's state matches the pattern decision anyway, the entry is removed instead of recorded. The result is that the override map stays small and only tracks the deliberate exceptions.

Pattern edits never touch this map, so a preset chip click or an include rewrite cannot accidentally lose your individual decisions. Starting over clears the bundle and the overrides together.

Worked example

You drop a Node project into FileConcat:

  1. Drop-time prune removes .git/, node_modules/, dist/ and the fonts under public/. Everything else lands in memory.
  2. Validation marks any binaries (PNGs) as excluded.
  3. You type examples into the Ignore textarea. The file tree updates immediately. Every file under examples/ disappears.
  4. You manually re-check one file at examples/quickstart.ts because you want it in the context. userToggled["examples/quickstart.ts"] = "include".
  5. You edit the Ignore textarea to add docs. Pattern layer hides docs/* but the override for examples/quickstart.ts is untouched.

Every other interaction in FileConcat is a variation on this loop.