GitHub Actions paths and paths-ignore together on one event
Writing GitHub Actions paths and paths-ignore together on one event is documented as unsupported, but no published component rejects it, so the file compiles and the workflow runs. What decides which of the two filters wins is a service GitHub does not publish, which is why the honest answer is to stop writing both rather than to learn the precedence.

What this error means
A trigger fires when you expected it to be skipped, or skips when you expected it to fire, and the event block on it carries both a paths list and a paths-ignore list. Nothing is annotated, the workflow file is accepted, and other events in the same file behave normally. The failure mode is a run that either exists or does not, with no message to attach to either outcome, which makes it very hard to tell an unsupported combination from a glob that does not match what you think it matches.
on:
push:
paths: ['src/**']
paths-ignore: ['docs/**']The rule is published, the enforcement is not
The workflow schema that ships in the runner carries a description for each filter, and both descriptions say the same thing. The one on paths says to use it when you want to include patterns or when you want to both include and exclude, and it adds that you cannot use both the paths and paths-ignore filters for the same event in a workflow. The description on paths-ignore repeats the sentence. So the rule is not folklore, it is written down in the schema itself.
The schema does not act on it. Follow the definitions down from the workflow root and the on key resolves to a mapping whose only named property is workflow_call, with loose keys and loose values for everything else. That means the push and pull request event mappings, and the path filters inside them, are never validated by the published schema at all. There is no rule in it that could make the two keys mutually exclusive, and there is no branch that could raise an error.
What this leaves you with is a documented rule with no published enforcement. Deciding whether an event fires for a given push is done by a GitHub service whose source is not available, so we can tell you what GitHub publishes and we cannot tell you what its service does with a file that breaks the published rule. Anyone who tells you the precedence confidently is describing observed behavior, not a contract, and observed behavior here is not something to build on.
| Layer | Sees the path filters | Can reject the combination |
|---|---|---|
| The YAML parser | as ordinary keys | no, both are valid YAML |
| The published workflow schema | no, the event mapping is loose | no |
| The editor schema in languageservices | no, the same loose mapping | no |
| GitHub event filtering | yes | not published |
Common causes
The two keys were added by different people at different times
Overwhelmingly the usual origin. Somebody adds paths to narrow a noisy workflow, somebody else later adds paths-ignore to skip documentation, and neither diff contains the other key, so neither review sees a conflict.
Include and exclude were assumed to compose
They read like a pair, and in most tools they would be. Here the documentation says to express both intents with one key, and the negation syntax inside paths is the mechanism that was provided for it.
A negation was written before the pattern it should subtract from
The documentation says an exclamation mark negates previous positive patterns. A list that starts with an exclusion excludes nothing, which looks identical to the filter being ignored.
The pattern does not match what it appears to match
Patterns must match the whole path from the repository root, and a single asterisk does not cross a slash. In our experience a filter that looks broken is more often a pattern that is slightly wrong than a precedence problem.
How to fix it
Decide which single intent the event has
- Write down in one sentence what should trigger this event.
- If the sentence contains only exclusions, keep
paths-ignoreand deletepaths. - If it contains any inclusion at all, keep
pathsand deletepaths-ignore. - Move any exclusion into the
pathslist as a negated pattern placed after the positive ones.
Express include and exclude in one paths list
Put the positive patterns first and the negations after them, because negation acts on what precedes it. Quote any pattern that begins with an exclamation mark, an asterisk or a bracket, since all three are special to YAML.
on:
pull_request:
paths:
- 'src/**'
- 'package.json'
- '!src/**/*.md'
- '!src/fixtures/**'Split one workflow into two when the intents really differ
Two files with one filter each are predictable, and a workflow that does not trigger costs nothing. This is usually the right answer when a file has accumulated filters that pull in different directions.
Test the filter with a real push rather than by reading it
- Create a branch and commit one file that should trigger the workflow.
- Commit a second file that should not, in a separate push.
- Read the Actions tab after each push and confirm which runs appeared.
What to write instead, which is documented
The way to express include and exclude in one event is a single paths list containing negated patterns, and the documentation is precise about how negation behaves. An exclamation mark at the start of a pattern makes it negate the previous positive patterns, and it has no special meaning anywhere else in the pattern. Both halves of that sentence are load bearing.
The first half means order matters. A negation only acts on the patterns that came before it, so a list that opens with an exclusion excludes nothing, because there is nothing yet to subtract from. This is the mistake that most often survives review, because the list reads as a set of rules rather than as a sequence.
The second half means an exclamation mark in the middle of a path is a literal character, not an operator. That rarely bites, but it does explain why a pattern that looks like it should exclude something does nothing at all.
One more thing the documentation is explicit about: path patterns must match the whole path and start from the repository root. A pattern that matches a file name without a leading directory component will not match a file inside a directory.
on:
push:
paths:
- 'src/**'
- '!src/**/*.md'
# excludes nothing: the negation precedes every positive pattern
on:
push:
paths:
- '!src/**/*.md'
- 'src/**'Choosing one filter deliberately
If the intent is only to skip some paths, paths-ignore alone is the right key and it is simpler to read than a positive list with subtractions. If the intent involves any inclusion at all, paths alone is the right key and everything else is negation inside it. There is no case where both are needed, which is presumably why the documentation forbids the combination rather than defining a precedence.
Where a repository has genuinely grown two sets of rules that want different things, the usual cause is that one workflow is doing two jobs. Two workflow files, each with its own single filter, are easier to reason about than one file with a filter nobody can predict, and they cost nothing since a skipped workflow consumes no minutes.
When you make the change, do not test it by reasoning about the globs. Push a branch that touches one file from each category and read which runs appear. Filters are cheap to test and expensive to guess at.
Why there is no recorded run on this page
A page about an unsupported combination could only record what our repository happened to do on the day we pushed, and that is precisely the thing we have just argued is not evidence. Recording it and presenting it as behavior would turn an observation of a closed service into an apparent contract, which is the error this page exists to avoid making.
What can be established is established here: the published schema text forbidding the combination is quoted from the schema, and the absence of any enforcement is traced through the definitions from the workflow root to the event mapping. The negation rules come from the documented filter pattern reference.
There is also no failure for a runner to repair, because no runner is involved. A trigger that does not fire produces no job, so there is nothing to retry and nothing to heal.
How to prevent it
- Allow one path filter per event, and make that a review rule rather than a convention.
- Put negated patterns after the positive patterns they subtract from, always.
- Quote every pattern beginning with an exclamation mark, an asterisk or a bracket.
- Test a changed filter with two pushes before relying on it.
Frequently asked questions
Can I use paths and paths-ignore on the same event?
Which one wins if both are set?
How do I include a directory but exclude part of it?
paths list, put the positive patterns first, and add negated patterns after them. An exclamation mark at the start of a pattern negates the previous positive patterns, and it has no special meaning anywhere else in the pattern.