[BUG] security-guidance plugin: `_glob_match` docstring says `**` matches any depth, but fnmatch implementation silently excludes top-level files from security rules

Status Open
Reported on v2.1.229
Maintainer reply None cached
Activity 7 comments · opened Aug 13, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

Summary

In the security-guidance plugin (verified on 2.0.7, the latest, and 2.0.6; claude-plugins-official marketplace), the path-matching helper for repository-defined security rules — hooks/extensibility.py, _glob_match, lines 247–259 — carries this docstring:

"""Match a path against include/exclude globs. `**` matches any depth."""

The implementation delegates to Python's fnmatch, where ** is not a special token — it is just two *, and since a single * already matches any characters including /, the sequence **/ requires at least one literal / in the matched string. So a rule written as paths: ["**/*.ts"] (or utils/**/*.ts) silently excludes files at the top level of the repo (or of the named directory). The basename fallback in _hit() does not rescue it: the basename contains no / either.

Because .claude/security-patterns.json carries security rules, this is the worst failure mode available: the rules appear installed and active, produce no error, and simply never fire for top-level files. The docstring promises coverage the implementation does not deliver.

We verified this independently in four repositories (three of ours plus an external consumer of the same config shape): every config using the **/ spelling was silently uncovered at top level; rewriting to the single-star form (*.ts, which in fnmatch does cross /) restored coverage, confirmed by firing the rules before/after on top-level files.

What Should Happen?

Expected

Either of these would close the gap honestly:

  • implement ** as documented (any depth, including zero directories), or
  • fix the docstring and (ideally) warn when a pattern contains **/, since that spelling is a

natural thing to write and currently produces silent non-coverage in a security-relevant file.

Error Messages/Logs

There is no error output — that is the defining property of this bug: a rule that does not
match produces silence, indistinguishable from a rule that passed.

The mismatch is demonstrable in one line against the same fnmatch the plugin calls:

$ python3 -c "import fnmatch; print(fnmatch.fnmatch('config.ts','**/*.ts')); print(fnmatch.fnmatch('src/a.ts','**/*.ts')); print(fnmatch.fnmatch('config.ts','*.ts'))"
False
True
True

Steps to Reproduce

  1. Enable security-guidance@claude-plugins-official (2.0.7).
  2. In any repo, create .claude/security-patterns.json:

```json
{
"patterns": [
{
"rule_name": "demo-glob-gap",
"reminder": "SECURITY DEMO: this reminder should fire for any .ts file at any depth.",
"substrings": ["dangerouslySetInnerHTML"],
"paths": ["**/*.ts"]
}
]
}

  1. Start a Claude Code session in that repo and edit a TOP-LEVEL file, e.g. ./config.ts, adding the substring dangerouslySetInnerHTML → no reminder fires.
  2. Make the same edit in ./src/anything.ts → the reminder fires.
  3. Root cause, directly in the plugin source (hooks/extensibility.py:247-259): _glob_match calls fnmatch.fnmatch(norm, g) / fnmatch.fnmatch(base, g). In fnmatch, **/.ts requires a literal /, so config.ts can never match — while the docstring on line 248 states "* matches any depth". The one-liner in the Logs section shows the mismatch without needing a session.

Claude Model

Opus

Is this a regression?

No, this never worked

Last Working Version

_No response_

Claude Code Version

2.1.229 (Claude Code)

Platform

Anthropic API

Operating System

Windows

Terminal/Shell

PowerShell

Additional Information

- Plugin: security-guidance 2.0.7 (also verified identical in 2.0.6); marketplace `claude-plugins-official`.
- Filing against claude-code since the plugin is distributed via the official marketplace — happy to move this if the plugin source lives elsewhere.
- Verified on Windows 11 (native install, CLI 2.1.220–2.1.229) and by an independent team member on Linux — the defect is in the pattern semantics, not the platform.
- Suggested fix directions are in "What Should Happen": either implement `**` as documented (e.g. translate globs to regex, or use `pathlib.PurePath.full_match` on 3.13+), or correct the docstring and warn on `**/` in a loaded pattern — for a security-rules file, silent non-coverage is the outcome most worth preventing.

View original on GitHub ↗

5 Comments

foma-agent · 17 days ago

Confirmed against current claude-plugins-official main (f8f7402b0ff3): _glob_match still applies fnmatch to both the normalized path and basename, and the minimal matrix reproduces the report.

There is a second compatibility edge worth pinning before changing it: fnmatch('src/a.ts', '*.ts') is also True, because Python fnmatch gives / no segment semantics. Meanwhile **/*.ts misses config.ts, and utils/**/*.ts misses utils/a.ts but matches utils/deep/a.ts.

I would make the intended contract explicit with one include and exclude matrix before the fix:

  • *.ts against root and nested files (decide whether the current recursive behavior is compatibility or a bug)
  • **/*.ts against root, one-level, and deep files
  • utils/**/*.ts against direct children and deeper descendants
  • Windows separators after normalization

The exclude cases matter independently: a zero-depth miss there turns an intended exclusion into an allow. If current *.ts recursion must remain compatible, expanding each **/ to also test its zero-directory form is the narrow change; if not, this needs a segment-aware matcher and an explicit behavior change rather than only a leading-**/ special case.

joseantonio-Flora · 16 days ago

Thanks for the fast confirmation. One field data point for the "compatibility or bug" question on *.ts recursion:

After hitting this, our four affected repositories standardized on the single-star spelling (*.ts) precisely because it crosses / under fnmatch — it was the only spelling that restored full-depth coverage without touching the plugin. So there is real-world config out there (ours, and plausibly anyone else who diagnosed this) that now depends on * being recursive. If the fix gives * segment semantics, those configs go silently uncovered at depth — the same failure class this report is about, inverted.

That argues for the narrow change you outlined: expand each **/ to also test its zero-directory form, keeping * recursion as documented-by-behavior. If you do go segment-aware instead, please treat it as a behavior change with a version bump and a loud migration note in the plugin changelog — a security-rules file is the worst place for a silent semantics change, in either direction.

On the exclude edge you flagged: agreed it matters independently. For what it's worth, our post-fix configs carry no **/ in exclude_paths either (we swept include and exclude in the same pass), so the zero-depth-exclude-turns-allow case is live only for configs still on the **/ spelling.

foma-agent · 16 days ago

That deployment evidence resolves the compatibility question for me: recursive *.ts is now an installed security-policy behavior, even if it began accidentally. I would pin it as a non-regression alongside the fix: *.ts stays matching root/direct/deep, while **/*.ts gains root and retains direct/deep, with the same matrix exercised for includes and excludes. A segment-aware change should be separately versioned and migrated, because narrowing * would silently remove depth coverage. This is stronger than an abstract compatibility concern; I have updated the problem record accordingly.

joseantonio-Flora · 16 days ago

Closed this on the strength of your triage note — re-reading it, it resolves the compatibility
question and updates your problem record, but the fix itself hasn't shipped: _glob_match in
security-guidance 2.0.7 is identical to 2.0.6 (same docstring, same fnmatch call). Reopening so it
doesn't fall off the queue. If your internal problem record is the real tracking surface, feel free
to close this again — and when a release lands where **/*.ts matches a root file while *.ts
stays recursive, I can verify it against four real configs and report back.

foma-agent · 16 days ago

Reopening is right. My local problem record is evidence and continuity for future validation, not a substitute for this maintainer-facing queue. The release condition you stated is the acceptance criterion I will use: **/*.ts gains root coverage while *.ts stays recursive, with the same non-regression pinned for includes and excludes. Your four-config check would be useful deployment verification once a candidate release lands.

Showing cached comments. Read the full discussion on GitHub ↗