DEV Community

Cover image for I Replaced a 461-Million-Downloads-a-Month Glob Package With One Rust File
Mohana Krishna
Mohana Krishna

Posted on AI-assisted

I Replaced a 461-Million-Downloads-a-Month Glob Package With One Rust File

I wanted a boring command.

Give it a filesystem query, let it find the right files, and get out of the way. It should start quickly enough to use in scripts, stream results instead of building a giant array, stop after the first useful answer, and avoid crawling directories that obviously cannot match.

That sounds like one job. In practice, it often becomes a small dependency stack.

There is a glob package for patterns, a directory walker for traversal, an ignore package for .gitignore, an argument parser for the CLI, a serializer for machine-readable output, and sometimes a task pool or command runner on top. Each piece is reasonable on its own. The combined result is less appealing: more startup work, more code in the supply chain, more memory, and several independent stages that know nothing about each other.

A conventional glob pipeline opens the whole filesystem tree before filtering, while Branchcut compiles the query and prunes branches before opening them.

The difference Branchcut is built around: filter after walking, or compile enough knowledge to avoid the walk.

The walker does not know that the matcher only cares about packages/core/src. The matcher does not know that the caller will stop after ten results. An exclusion may reject node_modules, but only after the traversal has already entered it.

That was the reason I created Branchcut: I wanted the entire query to become one traversal plan.

The Zero Dependency hackathon supplied the constraint that made the idea interesting: Rust standard library only, an empty dependency manifest, and no vendored implementation hiding behind it. Branchcut ended up as a 2,231-line src/main.rs, zero crates, and a release executable that is about 356 KiB on my current Windows build.

The size varies by platform and toolchain. The important part is that the executable is self-contained. There is no runtime, package directory, or transitive dependency tree to carry with it.

The package I set out to replace

The direct Package Killer target is fast-glob@3.3.3.

It is not a toy chosen because it would be easy to beat. The official npm downloads API recorded 5,536,931,736 downloads from September 2025 through August 2026. That works out to an average of 461,410,978 downloads per month.

fast-glob is mature and supports more syntax and configuration than Branchcut. I was not interested in cloning its JavaScript API or pretending that three days of work had replaced years of compatibility knowledge. I wanted to replace the common workflow:

const paths = await glob(patterns, options);
const filtered = paths.filter(extraPredicates);
const limited = filtered.slice(0, limit);
Enter fullscreen mode Exit fullscreen mode

Branchcut expresses that work as one command:

branchcut \
  --glob 'packages/**/src/**/*.{rs,ts}' \
  --exclude '**/{target,node_modules,dist}/**' \
  --type file \
  --limit 100 \
  --stats
Enter fullscreen mode Exit fullscreen mode

The point is not that Rust has a faster * loop than JavaScript. The point is that the compiler now sees the positive patterns, exclusions, type filter, hidden-file policy, and termination condition together.

It can use that information before opening a directory.

Compiling a query instead of filtering a walk

Branchcut parses each pattern into path components. A pattern such as:

packages/core/src/**/[a-z]*.{rs,ts}
Enter fullscreen mode Exit fullscreen mode

becomes a compact representation containing literal components, a path-level globstar, character ranges, a wildcard segment, and expanded brace alternatives.

The planner then looks for useful structure.

For a fixed-prefix pattern such as:

packages/core/src/**/*.rs
Enter fullscreen mode Exit fullscreen mode

the traversal begins at packages/core/src. It does not open the repository root and rediscover that prefix one entry at a time.

When several patterns share a prefix:

src/**/*.rs
src/**/*.toml
src/**/test*.rs
src/components/**/*.css
Enter fullscreen mode Exit fullscreen mode

they are compiled into a shared trie/NFA-style program. A directory entry advances the active shared states once rather than being converted into a full string and independently tested against every pattern.

Each traversal frame carries the positive and exclusion states that are still alive at that location. Before descending into a directory, Branchcut asks two questions:

Can any positive state still match a descendant here?
Does an exclusion safely cover this entire subtree?
Enter fullscreen mode Exit fullscreen mode

If the first answer is no, or the second answer is yes, the directory is pruned. No read_dir, no entries, and no paths to throw away later.

This is why the name is Branchcut. The useful optimization is not walking every branch a few percent faster. It is cutting branches that cannot produce an answer.

One executable instead of a toolchain made of packages

Globbing alone would not have solved the original problem. I wanted something useful from the terminal without immediately wrapping it in another script.

Globbing, walking, ignore handling, filtering, streaming, command execution, explain output, and traversal statistics converge into one Branchcut executable built from one Rust source file.

Globbing alone would not have solved the original problem. I wanted something useful from the terminal without immediately wrapping it in another script.

Branchcut pulls the jobs usually spread across a small package stack into one executable: globbing, walking, ignore handling, filtering, streaming, shell-free execution, plan explanation, and traversal statistics.

One query compiler, one traversal engine, one self-contained binary.

Those features would normally suggest several crates: clap, globset, walkdir, ignore, regex, rayon, serde_json, and a command helper. Branchcut does not include them. The complete substitution ledger is in STDLIB.md.

That does not mean the standard library made everything easy. It means the difficult parts were visible instead of delegated.

The parts that fought back during development

Globstar correctness, ignore re-inclusion, output semantics, and parallel ordering converge on one rule: correctness before speed.

The first deceptively small problem was **.

At the path-component level, globstar means zero or more components. Therefore:

src/**/mod.rs
Enter fullscreen mode Exit fullscreen mode

must match both:

src/mod.rs
src/a/b/mod.rs
Enter fullscreen mode Exit fullscreen mode

Treating it as an ordinary greedy string wildcard breaks the zero-directory case. In the compiled program it needs both a transition that stays on the globstar while consuming a component and an epsilon transition that moves forward without consuming one.

That was only the beginning. The same state has to remain useful while deciding whether a directory can still produce a match. A matcher that can answer “does this path match?” is not automatically a planner that can answer “is it safe to avoid opening this subtree?” Multiple patterns made that more interesting: shared prefixes needed shared traversal state, which meant restructuring the walker so each directory frame could carry the still-possible positive and exclusion states forward without repeatedly rebuilding full path strings or testing every pattern from scratch.

Ignore rules were harder in a different way. Given:

generated/
!generated/keep.rs
Enter fullscreen mode Exit fullscreen mode

pruning generated/ immediately would be fast and wrong. A later negation can re-include a descendant, so Branchcut keeps the subtree open when re-inclusion is possible. I would rather open one extra directory than publish a pruning counter achieved by losing valid results.

The bugs were not all exotic matcher failures. Some were ordinary command-line semantics that become very visible in a tool people put into scripts. A simple positional search such as branchcut config needed to remain a literal filename search, not quietly become a glob expression. Hidden paths and file-type filters had to preserve their meaning after prefix planning narrowed the traversal root. Count-only output still had to honor --limit. Sorting had to change the meaning of a limit: a streaming limit can stop traversal immediately, while a globally sorted limit must inspect and collect every match first.

Even writing output required care. A closed pipe should not turn a useful command into a panic just because its consumer stopped reading. Unix filenames are byte sequences, not guaranteed UTF-8 strings, so the matcher works from OsStr bytes there instead of converting every name through to_string_lossy(). Windows has different string semantics, so the compatibility document states the current lossy boundary rather than hiding it.

Parallel traversal arrived only after the sequential engine was correct. A bounded worker pool needed bounded per-worker queues, work stealing, outstanding-task tracking, condition-variable sleeping, atomic cancellation, reusable worker-local buffers, and one coordinator responsible for buffered output and errors. It also changed what the CLI could honestly promise: Branchcut rejects --threads with --limit or --exec, because those options require exact global early-stop or execution ordering.

Zero dependencies meant owning the unglamorous code too: argument parsing with std::env::args_os, contextual errors without anyhow, JSON Lines without serde_json, hierarchical ignore parsing without ignore, and shell-free execution through std::process::Command. The one-source-file rule made feature cuts necessary. Branchcut does not claim extglobs, nested braces, metadata predicates, every Git ignore escape rule, full fast-glob API compatibility, or a watch mode. Cutting an unsupported feature is better than shipping a convenient lie.

Making the planner observable

Optimization claims are easy when the work is invisible. I wanted Branchcut to show its reasoning.

--explain prints decisions before traversal:

QUERY PLAN

ROOT
  packages

SHARED LITERAL PREFIX
  packages

POSITIVE PATTERNS
  packages/**/src/**/*.rs [FixedPrefixRecursive]
  packages/**/src/**/*.ts [FixedPrefixRecursive]

EXCLUSIONS
  **/target/** [UnboundedRecursive]
  **/node_modules/** [UnboundedRecursive]

METADATA
  not required

TERMINATION
  first 100 matches
Enter fullscreen mode Exit fullscreen mode

--stats reports what happened:

matched                 10000
directories considered   101
directories opened         81
directories pruned         20
entries inspected       13100
candidate files         13000
metadata calls              1
filesystem errors           0
Enter fullscreen mode Exit fullscreen mode

The metadata count matters. If a query only needs entry names and file types, Branchcut uses DirEntry::file_type() and avoids a separate metadata() call for every candidate. The root is inspected once with symlink_metadata; the current filters do not need per-entry metadata.

The counters turned the planner from an architectural claim into something I could test. A performance improvement was only interesting if the result set remained correct and the counters explained where the time went.

Then benchmarking became the painful part

Node, native, and Zig implementations must produce the same result set before cold CLI and hot-engine timings mean anything.

I expected the glob parser to consume most of the time. It did not. The most frustrating part of the project was trying to produce a benchmark I could actually believe.

The tools do not naturally run under the same conditions. fast-glob and tinyglobby are Node packages whose normal APIs return arrays. Branchcut is a native executable that streams by default. zlob is a native Zig project with a CLI, a public matching API, and other walker paths with different capabilities. A single stopwatch number can silently mix process startup, module loading, traversal, sorting, output capture, and entirely different result sets.

My first rule became embarrassingly simple: count the results before trusting the time. For every comparable workload, I normalized separators, converted outputs into sets, and checked both directions:

Branchcut - competitor
competitor - Branchcut
Enter fullscreen mode Exit fullscreen mode

Both differences had to be empty. Matching counts alone were not enough; two wrong sets can have the same size. A tool that finishes instantly because a Windows path was interpreted differently has not won a benchmark. It has answered a different query.

This caught real inconsistencies. The zlob CLI I tested returned the expected result for a broad **/*.rs query, but its nested-globstar results on Windows did not agree with Branchcut, fast-glob, or tinyglobby. Another official benchmark path returned zero matches when given a Windows drive-letter path. Those runs were useless as performance evidence, so I retained the mismatches in the comparison notes and excluded the invalid timings instead of quietly presenting a spectacular zero-work victory.

Startup needed its own category. Launching Node, loading a module, executing a query, sorting, and capturing output is a legitimate measurement for a command-line user. It is not a clean engine comparison. I therefore kept two categories:

  • Cold CLI: launch a fresh process and include startup, loading, traversal, sorting, and output capture.
  • Hot engine: load the Node modules once, warm them up, consume results without printing, and time repeated queries inside the same process.

Branchcut needed the same care. Its --stats timer begins after argument parsing and planning, so the hot comparison uses count-only output and the internal elapsed value. For zlob, I built a temporary Zig harness around its public filesystem API so repeated queries could run inside one process. That harness was development equipment, not a dependency shipped with Branchcut.

Even after the harnesses agreed, filesystem benchmarks moved around. The first run could be dominated by cold caches. Antivirus activity and unrelated machine load appeared as outliers. Printing 13,000 paths could cost more than matching them. Sorting one side but not the other could reverse a result. I used warmups, repeated samples, medians, P90 where available, identical corpora, and identical result requirements. I retained the slow runs and the cases Branchcut could not fairly claim.

That process was slower and less satisfying than writing an optimization. It was also more valuable. The final table is modest compared with the number of ways I found to produce a misleading table.

What the measurements showed

With those rules in place, I used a generated 16,000-file corpus shaped like a small monorepo: 20 packages containing source files, target output, and node_modules content. For the supported cases in the published comparison, Branchcut, fast-glob, and tinyglobby returned equal sets.

The hot query was:

**/*.{rs,toml}
Enter fullscreen mode Exit fullscreen mode

It returned 13,000 matches with hidden files excluded and no path serialization. Node packages were loaded once and warmed up. Branchcut used its internal elapsed statistic after argument parsing and planning.

Engine Time per query Relative to Branchcut
Branchcut 22.079 ms median 1.00×
tinyglobby 0.2.14 35.666 ms median 1.61× slower
fast-glob 3.3.3 37.528 ms median 1.70× slower
zlob 1.6.3 public match API 133.408 ms average 6.04× slower

The zlob number needs context. It came from its direct, single-threaded public match API on Windows and does not represent every optimized walker path zlob offers on every platform.

I measured cold command invocation separately. For an exclusion-heavy query returning the same 10,000 sorted paths, Branchcut's median was 24.99 ms and fast-glob's was 148.53 ms. That 5.94× ratio includes Node startup and module loading, so I do not present it as an engine-only comparison.

The complete environment, workload, warmups, result counts, caveats, and even the awkward competitor behavior are recorded in COMPARISON.md and BENCHMARKS.md.

One limitation also deserves to stay visible: two clean Windows release builds did not produce byte-identical hashes. I suspect linker metadata, but suspicion is not evidence. Branchcut therefore does not claim the hackathon's reproducible-build bonus.

How Branchcut compares with fd and ripgrep

fast-glob was the Package Killer target, but it was not the only comparison worth making. Developers often reach for fd or ripgrep when they need to locate files from a terminal. They are mature native tools with excellent defaults, so this was not an attempt to manufacture an easy win.

I benchmarked branchcut.exe directly against fd 10.4.2 and ripgrep 15.1.0 on a synthetic Windows tree containing 16,247 files and 1,057 directories. These are observed end-to-end wall-clock timings for comparable file-finding workloads, including fresh process startup and PowerShell invocation. They are not pure traversal timings or universal claims.

Workload Branchcut fd ripgrep
All files ~176 ms ~276 ms ~215 ms
*.ts files ~142 ms ~142 ms ~154 ms
First matching file ~21 ms ~34 ms ~31 ms
Branchcut internal traversal, 1 thread 81 ms
Branchcut internal traversal, 8 threads 20 ms

The wall-clock and internal numbers answer different questions. For the TypeScript workload, Branchcut's --stats reported about 86 ms after argument parsing and planning, while the eight-thread internal run was about 20 ms. The larger end-to-end number includes the surrounding Windows and PowerShell launch cost. That distinction matters; calling either number “the traversal time” would be misleading.

The more useful comparison is capability shape:

Feature Branchcut fd ripgrep
Glob matching Yes Yes Yes
Extension filtering Native Native Via glob/filter
File, directory, and symlink filtering Yes Yes Primarily files
Hidden files --hidden --hidden --hidden
Exclusion patterns Yes Yes Yes
Prunes excluded subtrees Yes, explicitly Yes Yes
First or limited results --first, --limit --max-results Shell or pipeline workflow
Deterministic sorting --sort External tool or shell External tool or shell
.gitignore support Opt-in --gitignore Native default Native default
File-result JSON Lines Native --json No direct equivalent No direct equivalent
Command execution Native --exec Native --exec No direct equivalent
Explain query plan Native --explain No No
Traversal statistics Native --stats No No
Strict filesystem errors Native --strict Limited Limited
Parallel traversal control Explicit --threads N Internal Internal

This is the difference I care about. Branchcut is not merely trying to be a faster filename lister. fd is still an excellent default for a quick interactive find, and ripgrep remains the right tool when the primary question is about file content. Branchcut becomes interesting when the query itself needs to be inspected, combined, limited, streamed, explained, and made accountable for the filesystem work it performed.

That makes it closer to a programmable filesystem query engine than a basic glob utility: positive and negative globs become one plan, exclusions become pruning decisions, limits become cancellation, and --stats and --explain expose the result instead of asking users to trust a black box.

Correctness had to come before speed

The project has no external Rust test framework. Its tests live at the bottom of the same source file using #[test].

They cover the basic matcher syntax, zero-component globstars, braces, common-prefix planning, shared compilation, hidden paths, extension filters, exclusions, early termination, sorted limits, nested ignore rules, broken pipes, deep trees, symlink policy, parallel/sequential equality, and non-UTF-8 Unix names.

Whenever an optimization changed traversal, the relevant result comparison came first. A fast filesystem query engine that occasionally omits a path is simply a bug with an impressive benchmark.

The same rule influenced features I did not add. Branchcut does not support extglobs, nested braces, metadata predicates, or every .gitignore escape rule. It is not a drop-in replacement for the entire fast-glob API. Those limitations are written down in COMPATIBILITY.md.

I would rather have a smaller language with testable semantics than a long feature list made of optimistic claims.

Installing it

Branchcut can be installed directly from GitHub with Cargo:

cargo install --locked --git https://github.com/codex-mohan/branchcut.git
Enter fullscreen mode Exit fullscreen mode

Then a simple filename search is just:

branchcut config
Enter fullscreen mode Exit fullscreen mode

An explicit query can combine several concerns without another wrapper:

branchcut \
  --glob 'src/**/*.{rs,toml}' \
  --exclude '**/target/**' \
  --type file \
  --limit 20 \
  --stats
Enter fullscreen mode Exit fullscreen mode

And the installation can be removed cleanly:

cargo uninstall branchcut
Enter fullscreen mode Exit fullscreen mode

The repository also contains dedicated PowerShell and POSIX installers plus matching uninstallers. A CI matrix exercises the install, query, and uninstall lifecycle on Windows, Ubuntu, and macOS.

What I took away from the constraint

Zero dependencies did not automatically make the program fast. Rewriting a generic walker badly would only have produced a dependency-free slow program.

The useful part of the constraint was that it forced the layers into the same room. The parser could describe the query in a form the traversal understood. Exclusions could become pruning decisions. A limit could become cancellation. Type information could prevent metadata calls. Shared prefixes could become one starting directory instead of repeated matches against unrelated paths.

That is the part of Branchcut I want to keep developing.

There will always be cases where fast-glob, zlob, fd, or another mature tool is the better choice. Branchcut's argument is more specific: if a filesystem query contains enough information to prove that a subtree is irrelevant, the engine should use that information before it pays to open the subtree.

Compile the query. Cut the tree.


Branchcut was built for the Zero Dependency 72-Hour Hackathon by Hackathon Raptors, also to make good FOSS project that everyone can use :)

Top comments (0)