Grunt File Globbing: The Minimatch Rules Behind src and dest
Grunt resolves file globs with minimatch relative to your Gruntfile, and negation patterns only work when they follow positive matches. Learn the ordering rule, cwd overrides, and version pitfalls.
26 Aug 2026, 17:28 UTC

You add !tmp/** to a Grunt task expecting it to skip temporary files, yet the build still copies them. Or a custom task picks up files from a sibling directory. The culprit is rarely the wildcard itself; it is how Grunt orders and roots those patterns. Grunt delegates glob interpretation to the minimatch library, resolves paths relative to the Gruntfile unless told otherwise, and treats negation as a filter that only subtracts from matches already found.
The useful takeaway: a negation pattern in Grunt only works when it follows the positive patterns it is meant to exclude, and every relative glob is anchored to the directory containing the Gruntfile unless a cwd option moves that root.
How Grunt resolves globs with minimatch
Grunt does not implement its own file-matching engine. It passes the strings in src and dest arrays to minimatch, a glob library that understands wildcards such as *, **, character ranges, and negation with a leading !. Because every official contrib task uses the same expansion logic, the behavior you observe in grunt-contrib-copy or grunt-contrib-uglify is identical to what you get from grunt.file.expand in a custom task.
By default, relative patterns resolve against the directory that holds the Gruntfile, not the current working directory of your shell. If you need to target a different root without rewriting patterns, pass a cwd option in the task configuration. This keeps the pattern syntax stable while shifting the base directory.
The negation ordering rule
Minimatch processes patterns in array order. Positive patterns build the initial set of matched files. A pattern beginning with ! then removes matching entries from that set. If you place a negation first, there is nothing to subtract yet, so the later positive patterns simply add everything back.
For example, the array ['!src/lib/*', 'src/**/*.js'] will match all JavaScript files, including those under src/lib, because the exclusion runs before the inclusion. Reversing the order to ['src/**/*.js', '!src/lib/*'] applies the filter after the positive match, which is almost always what you want.
Version differences in minimatch
Grunt 0.4.x shipped with an older minimatch release that lacked support for brace expansion ({a,b}) and some advanced POSIX features. Grunt 1.x upgraded minimatch, so a pattern using braces may produce different match sets depending on the major version. Before relying on brace syntax, confirm which version you are running. In a terminal, from your project root, run grunt --version to see the CLI version and npm list grunt to inspect the locally installed package and its dependency tree. Neither command requires elevated permissions. The expected check is that the reported Grunt version tells you which minimatch behavior to expect; the risk is low, but npm list can show extraneous packages if dependencies are not installed cleanly.
Worked example: inspect matches before you build
Rather than guessing whether a pattern works, register a diagnostic task that prints the expanded list. The following Gruntfile defines a debug:files task using grunt.file.expand:
module.exports = function(grunt) {
grunt.registerTask('debug:files', 'Log expanded file list', function() {
var files = grunt.file.expand([
'src/**/*.js',
'!src/lib/*'
]);
grunt.log.writeln('Matched files:', files);
});
};
Create a test tree with these files: src/a.js, src/b.ts, src/lib/c.js, and tmp/ignore.txt. From the directory containing the Gruntfile, run grunt debug:files. No special permissions are required. The expected check is that the logged array contains src/a.js and excludes src/lib/c.js and tmp/ignore.txt, because **/*.js matches JavaScript files recursively and !src/lib/* removes direct children of src/lib. If excluded files still appear, verify that the negation is last and that no cwd option has shifted the root.
The same ordering rule applies to contrib tasks. A grunt-contrib-copy target configured with src: ['assets/**/*', '!assets/tmp/**'] and dest: 'build/' will exclude the temporary directory because the negation follows the positive pattern. After running grunt copy from the project root, list the contents of build/ to confirm that files under assets/tmp/ are absent.
The synchronous expansion trade-off
Grunt expands file lists synchronously, which means the pattern-matching work runs on the main thread and blocks the Node.js event loop—the runtime mechanism that processes asynchronous callbacks—until it finishes. On very large projects, extensive globbing across deep directory trees can make task startup feel sluggish. You can reduce the cost by narrowing the search scope with cwd or by using more specific patterns instead of broad ** sweeps. If your build routinely processes tens of thousands of files, a streaming build tool may be a better fit, but that is a larger architectural change than adjusting globs.
Actionable next steps
- Before wiring a new
srcarray into a real task, log it withgrunt.file.expandto confirm the exact file set. - Keep negation patterns after the positive patterns they filter.
- If you use brace expansion, verify your Grunt major version with
grunt --versionandnpm list grunt. - Use
cwdto anchor patterns to a subdirectory instead of duplicating path prefixes.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.