You have a Node.js project that uses minimatch to filter files. It works fine with simple patterns like *.js or src/**/*.ts. But one day you add a pattern with braces like {src,lib}/**/*.{js,ts,jsx,tsx} and notice the build takes longer than expected. You investigate and discover that minimatch converts that pattern into a regular expression that JavaScript’s engine executes with exponential backtracking.
The problem isn’t just performance. Minimatch has CVE-2022-3517, a denial of service vulnerability through regular expressions. An attacker can send a malicious pattern that freezes your server for minutes. The vulnerability has been documented for years but remains present in millions of projects because minimatch is a transitive dependency that nobody reviews.
You run npm ls minimatch in your project and see it’s used by glob, fast-glob, rimraf, del, and ten other dependencies you didn’t even know you had. Updating minimatch directly doesn’t help because each package brings its own version. And even if you updated it, performance remains the same.
The problem with backtracking regular expressions
Minimatch converts glob patterns to regular expressions. The pattern *.js becomes something like ^(?:(?!\.)(?=.)[^/]*?\.js)$. This expression works correctly, but when the pattern has alternatives like {a,b,c} or ranges like {1..100}, the resulting regular expression grows exponentially.
JavaScript’s regular expression engine uses backtracking to find matches. When one branch of the expression doesn’t match, the engine backtracks and tries another branch. With complex expressions, the number of combinations to try grows exponentially. A seemingly innocent pattern can generate millions of backtracking operations.
CVE-2022-3517 exploits exactly this. A specifically designed pattern can make the regex engine enter a backtracking loop that consumes CPU for seconds or minutes. On a web server, this means an attacker can take down your service by sending a request with a malicious pattern.
The solution: picomatch as internal engine
Picomatch is another glob matcher written by Jon Schlinkert. It uses a different approach to generate regular expressions that avoids catastrophic backtracking. Additionally, it has internal limits on brace expansion to prevent patterns like {1..1000000} from generating a million alternatives.
The problem with picomatch is that its API isn’t compatible with minimatch. If your code uses minimatch(path, pattern), you can’t simply switch to picomatch(pattern)(path). You’d have to rewrite all calls, update tests, and verify behavior is identical in all edge cases.
This is where minimatch-fast comes in. It’s a compatibility layer that exposes exactly the same API as minimatch but uses picomatch internally. You don’t have to change a single line of code. Install the package, update the import, and everything works the same but faster and without the vulnerability.
Installation and migration
Installation is standard:
npm install minimatch-fast
The simplest migration is changing imports:
// Before
import { minimatch } from 'minimatch';
// After
import { minimatch } from 'minimatch-fast';
If you don’t want to touch the code, you can use npm aliasing. This makes any package that imports minimatch receive minimatch-fast instead:
npm install minimatch@npm:minimatch-fast
With this option, all dependencies using minimatch automatically use minimatch-fast without knowing it. Useful when you have transitive dependencies you can’t control.
API identical to minimatch
The main function is minimatch(path, pattern, options). It receives a path, a glob pattern, and optional options. Returns true if the path matches the pattern.
import { minimatch } from 'minimatch-fast';
minimatch('src/index.ts', '**/*.ts'); // true
minimatch('src/index.ts', '**/*.js'); // false
minimatch('.gitignore', '*', { dot: true }); // true
minimatch('SRC/Index.ts', '**/*.ts', { nocase: true }); // true
To filter arrays of paths, use minimatch.match():
const files = ['app.js', 'app.ts', 'test.js', 'README.md'];
const jsFiles = minimatch.match(files, '*.js');
// ['app.js', 'test.js']
If you need a reusable filter for Array.filter():
const files = ['app.js', 'app.ts', 'test.js'];
const isTypeScript = minimatch.filter('*.ts');
const tsFiles = files.filter(isTypeScript);
// ['app.ts']
To get the compiled regular expression:
const regex = minimatch.makeRe('**/*.ts');
// Returns a RegExp object you can use directly
regex.test('src/index.ts'); // true
Brace expansion converts one pattern into multiple patterns:
minimatch.braceExpand('{src,lib}/*.js');
// ['src/*.js', 'lib/*.js']
minimatch.braceExpand('file{1..3}.txt');
// ['file1.txt', 'file2.txt', 'file3.txt']
To escape metacharacters when the pattern comes from user input:
const userInput = 'file[1].txt';
const escaped = minimatch.escape(userInput);
// 'file\\[1\\].txt'
minimatch('file[1].txt', escaped); // true (literal match)
The Minimatch class for reusable patterns
When you’re going to compare the same pattern against many paths, compiling the pattern once is more efficient:
import { Minimatch } from 'minimatch-fast';
const matcher = new Minimatch('**/*.{js,ts}');
const files = ['app.js', 'lib/utils.ts', 'README.md', 'src/index.tsx'];
const matches = files.filter(file => matcher.match(file));
// ['app.js', 'lib/utils.ts']
The class exposes useful properties:
const m = new Minimatch('!**/*.test.js');
m.pattern; // '!**/*.test.js'
m.negate; // true (pattern starts with !)
m.comment; // false (not a comment)
m.regexp; // compiled RegExp
The hasMagic() method indicates if the pattern has glob metacharacters:
new Minimatch('*.js').hasMagic(); // true
new Minimatch('index.js').hasMagic(); // false
Supported pattern syntax
Basic wildcards work as expected:
*matches any character except/**matches any character including/(crosses directories)?matches a single character[abc]matches any of the listed characters[a-z]matches a character range[!abc]matches any character except those listed
Braces expand alternatives:
minimatch('src/app.js', '{src,lib}/*.js'); // true
minimatch('lib/app.js', '{src,lib}/*.js'); // true
minimatch('dist/app.js', '{src,lib}/*.js'); // false
Numeric and alphabetic ranges also work:
minimatch('file3.txt', 'file{1..5}.txt'); // true
minimatch('file7.txt', 'file{1..5}.txt'); // false
minimatch('section-b.md', 'section-{a..d}.md'); // true
Extglob patterns add regular expression operators:
@(a|b)matches exactlyaorb?(a|b)matches zero or one occurrence ofaorb*(a|b)matches zero or more occurrences+(a|b)matches one or more occurrences!(a|b)matches anything exceptaorb
minimatch('foo.js', '*.+(js|ts)'); // true
minimatch('foo.jsx', '*.+(js|ts)'); // false
minimatch('test.spec.js', '!(*spec*).js'); // false (contains spec)
minimatch('app.js', '!(*spec*).js'); // true
POSIX classes provide portable character sets:
minimatch('file1.txt', 'file[[:digit:]].txt'); // true
minimatch('fileA.txt', 'file[[:alpha:]].txt'); // true
minimatch('FILE.txt', '[[:upper:]]*.txt'); // true
Negation inverts the match result:
minimatch('app.js', '!*.test.js'); // true (not a test)
minimatch('app.test.js', '!*.test.js'); // false (is a test)
Configuration options
Options control matching behavior:
dot: By default, * doesn’t match files starting with a dot. Enable this option to include them.
minimatch('.gitignore', '*'); // false
minimatch('.gitignore', '*', { dot: true }); // true
nocase: Case-insensitive matching.
minimatch('README.md', '*.MD'); // false
minimatch('README.md', '*.MD', { nocase: true }); // true
matchBase: Compare the pattern only against the filename, not the full path.
minimatch('src/lib/utils.js', '*.js'); // false
minimatch('src/lib/utils.js', '*.js', { matchBase: true }); // true
noglobstar: Treat ** as * (doesn’t cross directories).
minimatch('a/b/c.js', '**/*.js'); // true
minimatch('a/b/c.js', '**/*.js', { noglobstar: true }); // false
nobrace: Disable brace expansion.
minimatch('src/a.js', '{src,lib}/a.js'); // true
minimatch('src/a.js', '{src,lib}/a.js', { nobrace: true }); // false
noext: Disable extglob patterns.
minimatch('foo.js', '*.+(js|ts)'); // true
minimatch('foo.js', '*.+(js|ts)', { noext: true }); // false
nonegate: Disable negation with !.
minimatch('app.js', '!*.test.js'); // true
minimatch('app.js', '!*.test.js', { nonegate: true }); // false
To create a matcher with default options:
const mm = minimatch.defaults({ nocase: true, dot: true });
mm('README.MD', '*.md'); // true
mm('.env', '*'); // true
Benchmarks: between 1.3x and 26x faster
Benchmarks were run on Node.js 22 comparing minimatch 10.0.1 against minimatch-fast 1.0.0. Results vary by pattern type:
Simple patterns (*.js, **/*.ts): 1.35x faster. The improvement is modest because these patterns are already efficient in minimatch.
Negation patterns (!*.test.js): 1.50x faster.
Brace patterns ({src,lib}/**/*.{js,ts}): 6.5x faster. This is where picomatch shines because it avoids the combinatorial explosion of alternatives.
Complex brace patterns ({a,b,c}/{d,e,f}/**/*.{js,ts,jsx,tsx}): 26.6x faster. Minimatch generates a giant regular expression while picomatch maintains linear performance.
Precompiled Minimatch class: 1.16x faster on repeated use of the same pattern.
In practical terms, if your build takes 10 seconds processing globs with complex patterns, it can drop to less than 1 second. For simple patterns the improvement is smaller but it’s never slower than minimatch.
Security: protection against ReDoS
CVE-2022-3517 enables denial of service attacks through regular expressions. An attacker sends a pattern designed to cause catastrophic backtracking and your server freezes.
minimatch-fast mitigates this in two ways:
Safe regex engine: Picomatch generates regular expressions that don’t suffer exponential backtracking. Patterns that freeze minimatch execute instantly in minimatch-fast.
Limits on brace expansion: Range expansion like {1..1000} has limits to prevent a pattern from generating millions of alternatives. Original minimatch freezes with {1..100000}. minimatch-fast processes it in milliseconds with a reasonable limit.
// This freezes minimatch for seconds or minutes
// minimatch('file.js', '{1..100000}.js');
// minimatch-fast handles it without issues
minimatch('file.js', '{1..100000}.js'); // false, processed in ms
If your application receives patterns from external users, minimatch-fast is the safe choice.
Compatibility verified with 355 tests
The package includes 355 tests that verify exact compatibility with minimatch:
- 42 unit tests for basic functionality
- 42 edge case tests
- 22 security tests and protection against malicious patterns
- 196 compatibility tests derived from minimatch’s original test suite
- 53 verification tests for POSIX classes, Unicode, and regex
All original minimatch tests pass. If your code works with minimatch, it works with minimatch-fast without changes.
Full TypeScript support
The package includes complete type definitions:
import {
minimatch,
Minimatch,
type MinimatchOptions,
} from 'minimatch-fast';
const options: MinimatchOptions = {
nocase: true,
dot: true,
};
const matcher: Minimatch = new Minimatch('**/*.ts', options);
const result: boolean = matcher.match('src/index.ts');
Types are identical to @types/minimatch, so autocomplete and type checking work the same.
When to use minimatch-fast
Migration from minimatch: If you already use minimatch and want better performance or to eliminate the CVE-2022-3517 vulnerability, minimatch-fast is a one-line change.
New projects: If you’re going to use glob matching, start directly with minimatch-fast instead of minimatch.
Complex patterns: If you use patterns with many braces or ranges, the performance improvement is dramatic.
Applications receiving external patterns: If users can send glob patterns, minimatch-fast protects against ReDoS attacks.
Transitive dependencies: Use npm aliasing so all your dependencies use minimatch-fast automatically.
The code is on GitHub
The package is published on NPM as minimatch-fast. Source code is at github.com/686f6c61/minimatch-fast.
The license is MIT, same as original minimatch. You can use it in commercial projects without restrictions.
If you find incompatibilities with minimatch or have suggestions, open an issue on GitHub. The goal is to maintain 100% compatibility while improving performance and security.