GitHub Actions invalid pattern paths filter, and the quoting rule
A GitHub Actions invalid pattern paths filter is most often not a filter problem at all: the documentation states that a pattern beginning with certain characters must be quoted, and that an unquoted one creates a parse error which prevents the workflow from running. That is YAML refusing the file, before any filter has been consulted.

What this error means
Either the workflow stops running entirely, which means the file was refused, or it runs at the wrong times, which means the file was accepted and a pattern is not matching what you think. Those are different problems and the first thing to do is decide which one you have, by looking at whether any run exists for the commit at all. A workflow that has simply stopped appearing is the refused case, and no amount of reasoning about glob semantics will help with it.
# documented as invalid: creates a parse error that prevents the workflow running
paths:
- **/README.md
# valid
paths:
- '**/README.md'The quoting rule, quoted
The documentation is unusually direct about this. It says the asterisk, the opening bracket and the exclamation mark are special characters in YAML, and that if you start a pattern with any of them you must enclose the pattern in quotes. It adds that if you use a flow sequence with a pattern containing brackets, the pattern must be quoted there too.
It then shows the failure by example, with a pattern beginning with two asterisks written unquoted and labeled as invalid, adding that it creates a parse error which prevents your workflow from running. That is the sentence that matters, and it tells you the consequence is total rather than partial. The workflow does not misbehave, it stops.
The reason is ordinary YAML. An asterisk at the start of a plain scalar begins an alias, an opening bracket begins a flow sequence, and an exclamation mark begins a tag. None of those is what you meant, and two of them produce a document that cannot be parsed at all.
So the first move on any pattern problem is to quote every pattern in the block, which costs nothing and removes the entire category. Quoting a pattern that did not need it changes nothing about how it matches.
| Leading character | What YAML starts reading | Must be quoted |
|---|---|---|
* | an alias to an anchor | yes |
[ | a flow sequence | yes |
! | a tag | yes |
| A letter, a digit or a dot | a plain scalar | no, but harmless |
Common causes
A pattern starts with an unquoted special character
The asterisk, the opening bracket and the exclamation mark all begin something else in YAML. The documentation labels an unquoted leading double asterisk as invalid and says it creates a parse error that prevents the workflow running.
A negation appears before the patterns it should subtract from
Negation acts on previous positive patterns only. A list that opens with an exclusion excludes nothing, and the result is indistinguishable from the exclusion being ignored.
The pattern does not match the whole path
Path patterns match the whole path starting from the repository root. A pattern written as though it were relative to a subdirectory matches nothing, quietly.
A single asterisk was used where a double was needed
A single asterisk does not cross a slash. This is the difference between matching files directly inside a directory and matching everything beneath it, and in our experience it accounts for most filters that match less than expected.
A question mark was read as a single character wildcard
It matches zero or one of the preceding character rather than any one character. A pattern carried over from shell globbing therefore means something different here than it did there.
How to fix it
Establish whether the file was refused
- Look at the Actions tab for the commit and see whether any run exists.
- If nothing exists and the workflow has simply stopped appearing, the file was refused.
- If runs exist but at the wrong times, the file parsed and a pattern is not matching.
Quote every pattern in the block
Quoting is unconditional and free. A block in which every pattern is quoted cannot fail on a leading special character, and quoting a pattern that did not need it changes nothing about how it matches.
on:
push:
branches:
- 'main'
- 'release/**'
paths:
- 'src/**'
- '!src/**/*.md'Put negations after the patterns they subtract from
Order the list positives first, negations last. Since an exclamation mark only negates what precedes it, a negation at the top of the list has no effect at all.
Write patterns from the repository root
Path patterns match the whole path. Begin each one at the root rather than at the directory you happen to be thinking about, and use the double asterisk wherever the match needs to cross a slash.
Verify with test pushes rather than by reasoning
Push one commit that should trigger the workflow and one that should not, and read the Actions tab after each. Since the deciding component is not published, an observed result is the only reliable answer.
Two operators that do not mean what glob users expect
If the file is accepted and the filter matches the wrong things, the pattern semantics are worth reading carefully, because two of them differ from ordinary shell globbing in a way that reverses expectations.
The question mark matches zero or one of the preceding character, and the plus matches one or more of the preceding character. In most glob dialects a question mark matches exactly one character of any kind. Here it is a repetition operator applied to whatever came before it, which is why the documented example for matching both a JavaScript and a JSX file name puts the question mark after the letter it makes optional.
The bracket class matches one alphanumeric character listed in the brackets or included in a range, and the documentation limits ranges to lower case letters, upper case letters and digits. A range outside those is not something to rely on.
The two asterisk forms follow the usual convention and are the two worth memorising: a single asterisk matches any characters except a slash, and a double asterisk matches any characters including slashes. The documentation also states that path patterns must match the whole path and start from the repository root, which is the rule behind most patterns that quietly match nothing.
on:
push:
paths:
- '**.js' # every .js file, at any depth
- 'docs/*' # files directly in docs, not in its subdirectories
- 'docs/**' # docs and everything beneath it
- '*.jsx?' # page.js and page.jsxNegation, and the layer that decides it
The exclamation mark negates previous positive patterns when it is the first character of a pattern, and the documentation adds that it has no special meaning if it is not the first character. Both halves matter. A negation placed before the patterns it is meant to subtract from removes nothing, because there is nothing before it to subtract from, and this is the commonest reason a filter appears to ignore an exclusion.
Now the scoping, which is the part this page will not guess at. The event filters are not evaluated by anything published. The workflow schema that ships in the runner reaches the event mapping with loose keys, so the filters inside it are never validated there, and the same is true of the editor copy. The component that decides whether a push matches your patterns is a GitHub service whose source is not available.
That means we can tell you the documented pattern semantics, which are published and quoted above, and we can tell you that an unquoted pattern breaks the file, which is also published. We are not going to tell you what a malformed but parseable pattern does, because that is decided somewhere we cannot read. Validate patterns by pushing test commits, not by reasoning about an implementation nobody outside GitHub can see.
# excludes nothing: the negation has no preceding pattern to subtract from
paths:
- '!docs/**'
- 'src/**'
# excludes the markdown files under src
paths:
- 'src/**'
- '!src/**/*.md'Why there is no recorded run on this page
In the refused case there is nothing to record, because the file does not parse and no run is created. In the accepted case the observable artifact is a run that exists or does not exist for a given push, and a recording of ours would be a list of our commits against our patterns.
The more important reason is the scoping above. Recording what our repository did with an unusual pattern would produce a confident looking demonstration of behavior we have just said is decided by a component we cannot read. That is exactly how an observation gets mistaken for a contract, and this page would rather be short on that point than wrong about it.
Everything that is published is quoted instead: the quoting requirement and its consequence, the pattern semantics including the two operators that act on the preceding character, the negation rule, and the requirement that path patterns match the whole path from the repository root. And there is nothing to repair, since no runner is ever involved in a trigger that did not fire.
How to prevent it
- Quote every filter pattern, unconditionally, as a style rule.
- Keep negated patterns at the end of the list, after the positives.
- Write path patterns from the repository root and prefer the double asterisk when crossing directories.
- Confirm any filter change with two test pushes before relying on it.