Skip to content

fix: anchor .claude ignore patterns to the project root - #206

Merged
7nohe merged 1 commit into
mainfrom
claude/frosty-thompson-65dffa
Aug 11, 2026
Merged

fix: anchor .claude ignore patterns to the project root#206
7nohe merged 1 commit into
mainfrom
claude/frosty-thompson-65dffa

Conversation

@7nohe

@7nohe 7nohe commented Aug 11, 2026

Copy link
Copy Markdown
Owner

Problem

Both ignore patterns matched any ancestor path segment named .claude, not just a .claude directory inside the project. When the repo is checked out under a .claude/ path — which is how Claude Code worktree sessions run (.claude/worktrees/<name>) — this silently disabled both quality gates.

Coverage (the dangerous one). Vitest matches coverage.exclude against absolute paths with picomatch's contains option:

pm.isMatch(filename, glob, { contains: true, dot: true, ignore: this.options.exclude })

so .claude/** matched every source file. npm test reported:

All files          |   0 |   0 |   0 |   0
Statements   : Unknown% ( 0/0 )

The configured thresholds (95% lines/functions/statements, 90% branches) then passed vacuously — a green npm test measuring nothing.

Lint. Biome resolved !**/.claude against an ancestor directory, so biome check . reported Checked 0 files / "No files were processed in the specified paths" instead of linting.

Fix

  • vitest.config.ts: build the exclude from the config file's own absolute directory (${projectRoot}.claude/**). Since the pattern is matched against absolute paths in contains mode, an absolute prefix is the only available anchor. Backslashes are normalized because vitest slashes the file path but not the pattern, and Windows is in the CI matrix.
  • biome.json: !**/.claude!.claude, which is anchored to the project root. !/.claude and !./.claude were tested and do not work — Biome descends into .claude/worktrees/ with either.

test.exclude deliberately keeps .claude/**: it is globbed by tinyglobby relative to the project root, so it is already anchored and never broke. It is also the load-bearing pattern that stops a main-checkout run from collecting .claude/worktrees/*/tests/*.test.ts, and an absolute pattern is not reliably supported by tinyglobby's ignore. That picomatch/tinyglobby asymmetry is why only coverage.exclude changes.

Verification

Main checkout (/…/openapi-react-query-codegen), before → after:

before after
biome check . 109 files 109 files
coverage (stmts/branch/funcs/lines) 98.36 / 90.07 / 99.29 / 99.05 98.36 / 90.07 / 99.29 / 99.05
test files collected 16 16 (0 paths under .claude)

Throwaway worktree under .claude/worktrees/, before → after:

before after
biome check . 0 files (No files were processed) 108 files
coverage 0/0, Unknown% 98.36 / 90.07 / 99.29 / 99.05
tests 16 files, 194 passed

Both patterns also narrow from any-depth to root-only, so a hypothetical nested .claude (e.g. examples/foo/.claude) would no longer be ignored. The identical main-checkout file count (109) and identical coverage numbers show no such directory exists today.

The repo's own .claude/ directory is still ignored:

  • Biome — proven by the main-checkout count staying at exactly 109 with 5 nested worktree copies of src/*.mts present under .claude/worktrees/; had the anchor stopped suppressing descent, that number would explode. The biome report contains 0 .claude paths.
  • Coverage — proven by the picomatch assertion below (own .claude (want false): false), not by a run. With no coverage.include set, getUntestedFiles early-returns, so coverage only ever sees files the tests actually load and nothing under .claude/ is ever loaded; the pattern is belt-and-braces there. (The vitest list --filesOnly result above exercises test.exclude, which is unchanged.)

The picomatch call vitest actually makes was also asserted directly for posix and Windows-style roots:

old  <worktree root>   own src (want true): false   <- the bug
new  <worktree root>   own src (want true): true  | own .claude (want false): false
new  C:/a/…            own src (want true): true  | own .claude (want false): false

Both patterns matched any ancestor path segment, so checking the repo out
under a `.claude/` path (e.g. a git worktree in `.claude/worktrees/`)
silently disabled the quality gates:

- vitest matches `coverage.exclude` against absolute paths with picomatch's
  `contains` option, so `.claude/**` excluded every source file. Coverage
  reported `0/0` and the 95%/90% thresholds passed vacuously while measuring
  nothing. Anchor it with the config's own absolute directory.
- biome resolved `!**/.claude` against an ancestor directory, so `biome check .`
  reported "Checked 0 files". `!.claude` is anchored to the project root.

`test.exclude` is globbed by tinyglobby relative to the project root, so it is
already anchored and stays relative.
@vercel

vercel Bot commented Aug 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
openapi-react-query-codegen Ready Ready Preview Aug 11, 2026 10:40am

@github-actions

Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🟢 Lines 99.22% (🎯 95%) 509 / 513
🟢 Statements 98.49% (🎯 95%) 524 / 532
🟢 Functions 99.24% (🎯 95%) 132 / 133
🟢 Branches 90.57% (🎯 90%) 221 / 244
File CoverageNo changed files found.
Generated in workflow #440 for commit 85f0c29 by the Vitest Coverage Report Action

@7nohe
7nohe merged commit 7875139 into main Aug 11, 2026
5 checks passed
@7nohe
7nohe deleted the claude/frosty-thompson-65dffa branch August 11, 2026 11:02
7nohe added a commit that referenced this pull request Aug 11, 2026
`coverage.exclude` patterns are matched against absolute paths with
picomatch's `contains` option, so every relative spelling of a `.claude`
ignore also matches the project's own root when the repo is checked out
under a `.claude/` path, as agent git worktrees in `.claude/worktrees/`
are. That excluded every source file and let the thresholds pass against
0/0. All four relative forms were measured to fail open:

  .claude/**  ./.claude/**  /.claude/**  **/.claude/**

#206 fixed this by building an absolute pattern from `import.meta.url`.
`coverage.include` is anchored to the project root instead, so scoping by
include removes the failure mode structurally rather than working around
it, and drops the `node:url` import, the derived root and five exclude
entries along with it.

`test.exclude` keeps its `.claude/**` entry: it is globbed relative to the
project root and is load-bearing at the repo root, where dropping it
collects the nested worktrees too (75 test files instead of 15).

Coverage output is unchanged in both locations: 187/187 tests, 529/537
statements, no `.claude` paths in the report.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant