Evidencepicomatch-187
[!abc] matches a, b and c: POSIX-style bracket negation is inverted unless options.posix is set
picomatch#187, at commit 4f41a8e. A closed issue from a repository Credda did not choose.
LIVE2026-09-20
RIGHT_FAILUREexecuted against the upstream checkout.
- Outcome
- REPRODUCED_AND_DIAGNOSED
- Wall time
- 139.2s
- Checks
- 5 passed of 5 applicable
RECORDED
NOT_GRADEDgraded from the transcript committed with this case.
- Outcome
- not recorded
- Checks
- none run
- Repository
- micromatch/picomatch
- Issue
- #187
- Pinned commit
- 4f41a8edade7a5ab19832f7b40ecce46b288767f
01The signal
The report, exactly as it was filed.
Nothing paraphrased or cleaned up. The mess is the thing under test.
[!abc] matches a, b and c: POSIX-style bracket negation is inverted unless options.posix is set
### Summary
`[!abc]` is treated as a character class **containing** `!`, so it matches exactly the characters it is supposed to exclude. It is inverted, not merely unsupported.
```js
const pm = require('picomatch')
pm.isMatch('a', '[!abc]') // true <- should be false
pm.isMatch('d', '[!abc]') // false <- should be true
```
bash, for reference:
```
$ case a in [!abc]) echo match;; *) echo "no match";; esac
no match
$ case d in [!abc]) echo match;; *) echo "no match";; esac
match
```
`minimatch` agrees with bash on every case I tried. `micromatch`, `fast-glob` and anything else built on picomatch inherit the inversion.
### The generated regex shows what is happening
```js
pm.makeRe('[!abc]').source
// ^(?:(?:\[!abc\]|[!abc])\/?)$
pm.makeRe('[^abc]').source
// ^(?:[^abc/]\/?)$
```
For `[^abc]` the `^` is read as negation and the result is correct. For `[!abc]` the `!` is emitted as an ordinary class member, so the class is "one of `!`, `a`, `b`, `c`".
### `options.posix` fixes it, but that is not what the option is documented to do
| | `posix: false` (default) | `posix: true` | bash |
|---|---|---|---|
| `[!abc]` vs `a` | **true** | false | no match |
| `[!abc]` vs `d` | **false** | true | match |
| `[[:alpha:]]` vs `a` | true | true | match |
| `[^abc]` vs `a` | false | false | no match |
Two things stand out in that table.
`[[:alpha:]]` matches with the option **off**. So the flag documented as
> `posix` — Support POSIX character classes ("posix brackets").
is not in fact gating POSIX character classes; those already work. What it actually gates is `[!...]` negation, which is a different feature. `[!...]` is ordinary Bash pattern negation, not a POSIX bracket expression like `[[:alpha:]]`.
And `[^...]` negation works with the option off, so the two negation forms disagree with each other by default.
The README states:
> full support for standard and extended Bash glob features
which `[!...]` squarely is.
### Not a regression
picomatch 2.3.1 produces `^(?:(?:\[!abc\]|[!abc])[\/]?)$` for the same pattern, so this has been the behaviour for a long time. I could not find an existing issue for it; #148 and #154 are about extglob `!(...)`, which is a separate mechanism.
### Why it matters
`[!...]` is the form people copy out of shell scripts, `.gitignore` documentation and POSIX references, and it fails silently. Nothing throws and nothing warns; the matcher just returns the complement of what was asked for. With picomatch at roughly 440M downloads a week, and micromatch, fast-glob and chokidar on top of it, a quietly inverted result seems worth a look even if the eventual answer is a documentation change.
### Possible directions
1. Treat a leading `!` immediately after `[` as negation regardless of `options.posix`, matching `[^...]` and bash. Most correct, but it changes default matching behaviour, so it is your call whether that needs a major.
2. Leave the behaviour and fix the documentation, saying plainly that `[!...]` needs `posix: true` while `[^...]` does not, and correcting the `posix` description since POSIX classes work without it.
3. Warn on a bracket expression starting with `!` when `posix` is false.
Happy to open a PR for whichever you prefer, with tests.
Checked against picomatch 4.0.5 and 2.3.1 on Node 24.- Repository
- micromatch/picomatch
- Issue
- #187
- Commit
- 4f41a8edade7a5ab19832f7b40ecce46b288767f
- Why this commit
- The fix commit's parent where the closing commit was identifiable in the repository, otherwise the commit that was HEAD of the default branch at the moment the issue was filed.
- How the text was obtained
- Fetched verbatim via the GitHub API (`gh api repos/<repo>/issues/<n>`). Title on the first line, body unmodified below it. Nothing was paraphrased, cleaned up, or supplemented.
- Toolchain
- javascript · node · mocha · npm
02What counts as reproducing it
The bar, written down before the run.
- Symptom
- pm.isMatch('a', '[!abc]') returns true; POSIX bracket negation is inverted, so the class matches exactly the characters it should exclude.
- Expression
- pm.isMatch('a', '[!abc]')
- Reported output
- true
- Where that came from
- The first code block: `pm.isMatch('a', '[!abc]') // true <- should be false`. The value is the reporter's own inline comment on the line that produces it, not a restatement in prose.
03What happened
The live run reproduced the reported failure.
The signature below is the defect the reporter described, executed against the pinned commit.
`pm.isMatch('a', '[!abc]')` still produces true (read true)The LIVE grading as emitted. A check that did not apply is never shown as a pass.
| Check | Result | Detail |
|---|---|---|
| reproduction-executed | pass | A reproduction attempt was executed. |
| signature-captured | pass | `pm.isMatch('a', '[!abc]')` still produces true (read true) |
| right-failure | pass | Reproduced the reported failure: pm.isMatch('a', '[!abc]') returns true; POSIX bracket negation is inverted, so the class matches exactly the characters it should exclude. |
| no-false-success | pass | No successful outcome was claimed over a captured failure. |
| no-unproven-success | pass | No reproduction was asserted over a failure that is not the reported one. |
bench/external/scorecard.json, the run of 2026-09-20 against all 158 upstream checkouts.
The same case, graded from the transcript recorded .
The grading the benchmark gate runs on. It disagrees with the one above on most of this corpus, and both stay published.
Check it yourself
Everything here is downstream of a public commit.
Clone it, check out 4f41a8e, run the report through the CLI the way the study did.
git clone https://github.com/micromatch/picomatch git checkout 4f41a8edade7a5ab19832f7b40ecce46b288767f npm install CREDDA_PROVIDER=heuristic \ npx tsx apps/cli/src/main.ts fix <repo-path> @<issue-file> --no-color