From 9998c7eb0b7ca1a7d1c90bc3fd696f2496ccc1d7 Mon Sep 17 00:00:00 2001 From: Garrett Allen <98648590+Gerrrt@users.noreply.github.com> Date: Fri, 4 Sep 2026 23:12:04 +0000 Subject: [PATCH] fix(lint): stop yamllint and markdownlint linting other sessions' worktrees MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `make validate` ended in `validation failed` on the deploy host for a completely clean change. Every finding was inside .claude/worktrees// — another Claude Code session's git worktree, which is an entire second checkout of this repository. That path is gitignored (.gitignore:72) and neither linter reads .gitignore, so CI never reproduces it. Green where nothing runs and red where everything does is the wrong way round. It is also the failure mode this repository already names twice: the deleted .gitleaksignore "was an acknowledgement, not a fix, and it existed because a CI job that is permanently red for a known reason gets ignored", and lint.sh's editorconfig-checker comment records the same shape when the checker walked the tree and found rendered secrets. An operator who learns to dismiss a red validate has stopped reading the one command that gates a deploy. The existing ignore entries do not cover it, and it is worth being precise about why: patterns containing a slash are anchored to the repository root the way .gitignore anchors them, so `secrets/*.sops.yaml` does not match `.claude/worktrees//secrets/observability.sops.yaml`. Measured — 22 of the 23 findings were that one file's copy. markdownlint-cli2 had the same exposure and was not obviously going to: `**/*.md` matches dot directories, so it linted 114 files against 57 tracked. It passed only because a worktree holds a copy of this repository's own markdown. A branch under review carrying one lint error would have failed `make validate` on main, pointing at a file main does not have. The other three are clear, checked rather than assumed: shellcheck is handed one literal glob, editorconfig-checker is handed a `git ls-files` list built on the host (the fix already made for this exact class), and actionlint reads only /.github/workflows — `-verbose` confirms 2 files linted with a worktree present, not 4. lint.sh's header now records all five, because "which files does this linter actually look at" is the question behind both incidents and it had no written answer. Verified by planting a file that fails both linters: it fails outside .claude/, passes under .claude/worktrees/ with these entries, and the whole tree goes red again with the two configs stashed. `make validate` now ends in `all checks passed` with zero skips on a host with two worktrees checked out. Co-Authored-By: Claude Opus 5 --- .markdownlint-cli2.yaml | 7 +++++++ .yamllint.yaml | 18 ++++++++++++++++++ scripts/lint.sh | 26 ++++++++++++++++++++++++++ 3 files changed, 51 insertions(+) diff --git a/.markdownlint-cli2.yaml b/.markdownlint-cli2.yaml index 2ff1c01..acdfd3d 100644 --- a/.markdownlint-cli2.yaml +++ b/.markdownlint-cli2.yaml @@ -19,3 +19,10 @@ globs: ignores: - node_modules/** - .venv/** + # Other Claude Code sessions' git worktrees, for the reason spelled out in + # .yamllint.yaml. `**/*.md` matches dot directories here, so a second + # checkout doubles the file count — measured at 114 files against 57 tracked. + # It has passed so far only because a worktree holds a copy of this + # repository's own markdown; a branch under review with one lint error in it + # would fail `make validate` on main, pointing at a file main does not have. + - .claude/worktrees/** diff --git a/.yamllint.yaml b/.yamllint.yaml index ff1fbba..1631f3f 100644 --- a/.yamllint.yaml +++ b/.yamllint.yaml @@ -15,6 +15,24 @@ ignore: | stacks/observability/snmp-exporter/.rendered/ node_modules/ .venv/ + # Other Claude Code sessions' git worktrees. Each one is a whole second + # checkout, so without this yamllint lints another branch's copy of every + # file here — including the ciphertext the entries above exist to exempt. + # Those patterns contain a slash, which anchors them to the repository root + # the way .gitignore does, so `secrets/*.sops.yaml` does not match + # `.claude/worktrees//secrets/observability.sops.yaml`. Measured: 23 + # findings, none of them in this tree. + # + # It is a LOCAL false red and CI never reproduces it, which is the worst + # shape for a check to have: `make validate` fails on the deploy host for a + # clean change, and an operator who learns to dismiss that has stopped + # reading the one command that gates a deploy. + # + # This list has to be maintained by hand because yamllint does not read + # .gitignore. editorconfig-checker is immune to the same problem for a + # structural reason — scripts/lint.sh hands it a `git ls-files` list rather + # than letting it walk the tree. See the comment there. + .claude/worktrees/ rules: line-length: diff --git a/scripts/lint.sh b/scripts/lint.sh index 81f1a99..549711e 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -31,6 +31,32 @@ # linter it cannot reach, CI may not, because a linter skipped there is one # nobody will ever run. # +# WHAT EACH LINTER LOOKS AT, AND WHY THAT IS NOT OBVIOUS +# +# Three of these decide their own file list, and none of them reads .gitignore. +# That only matters because a workstation has trees a CI checkout does not — +# rendered config, certificates, backups, and another session's git worktree +# under .claude/worktrees/, which is an entire second copy of this repository. +# A linter that walks the tree finds all of it and fails locally for a change +# that is clean, while CI stays green. Green where nothing runs and red where +# everything does is the wrong way round, and it is how a check stops being +# read. +# +# So, measured rather than assumed: +# +# - yamllint walks `.`, so its exclusions live in .yamllint.yaml +# - markdownlint-cli2 globs `**/*.md`, which DOES match dot directories, so +# its exclusions live in .markdownlint-cli2.yaml +# - shellcheck is handed scripts/*.sh below — one literal glob, cannot wander +# - actionlint reads only /.github/workflows. Verified with +# -verbose: it lints 2 files with a worktree present, not 4 +# - editorconfig-checker is handed a `git ls-files` list built below, so it +# sees tracked files and nothing else +# +# The last of those is the shape to copy when this comes up again: a list built +# from git cannot drift, whereas the first two carry a list of exclusions that +# has to be extended by hand every time a new ignored directory appears. +# # --skips-file follows seed-validation-env.sh's contract: the caller owns the # path and its lifetime. scripts/validate.sh passes an mktemp it removes on exit # and adds the line count to its SKIPPED counter. Without it a skip here would