Skip to content

TDD 0068: format-and-lint honors the repo's configuration (fixes #180) - #189

Merged
cahenesy merged 8 commits into
masterfrom
build/20261006-131111/0068-format-and-lint-honors-config
Oct 6, 2026
Merged

cahenesy merged 8 commits into
masterfrom
build/20261006-131111/0068-format-and-lint-honors-config

Conversation

@cahenesy

@cahenesy cahenesy commented Oct 6, 2026 •

Copy link
Copy Markdown
Owner

Implements TDD 0068 (PRD FR-21, issue #180). It supersedes only the hook component of TDD 0001.

Fixes #180.

What changed

hooks/format-and-lint.sh:

  • Config discovery starts from the edited file's directory and walks up to its git toplevel. It no longer uses $PWD.
  • Python:
    • Ruff config precedence is ruff.toml > .ruff.toml > pyproject.toml ([tool.ruff…]).
    • Formatting runs only when the config opts in with [tool.ruff.format] / [format].
    • Linting is report-only: ruff check --no-fix --output-format concise. Diagnostics go to stderr, with exit 2.
  • JS/TS:
    • Prettier and eslint run only when configured. Config can be a top-level package.json key; a devDependencies entry does not count.
    • Tools come from the local node_modules/.bin (no npx --version probes). eslint runs without --fix.
    • No JS tools run for a file outside git.
  • Rust/Go:
    • Formatting follows the language convention, guarded by "HEAD copy already clean": exit 0 and empty stdout. New files are formatted; files outside git are never formatted.
    • rustfmt runs via stdin from the file's directory, so it never reformats child modules.
    • golangci-lint runs only when a .golangci.* config exists at or above the module root.
  • Debounce:
    • Keyed by project root.
    • Markers live in a per-user 0700 directory. Their content must be digits only, and symlinks are never followed.
    • A failing linter removes its marker, so the next edit re-lints.
  • Unchanged: a missing jq/python3 still exits 2 loudly.

Also:

  • /bootstrap-project writes the formatter opt-in on every path that creates a formatter config.
  • README.md has a hook section and an upgrade note: Python repos bootstrapped before 3.50.0 must add [tool.ruff.format] to keep auto-formatting.

Security (pre-existing, fixed here)

Review found three issues; all are fixed in 102b45a and test-covered in [S]:

  • The old debounce marker at a predictable path in shared /tmp was evaluated in bash arithmetic, so a planted now[$(cmd)] ran cmd as the developer. It is fixed and test-covered.
  • A JS file outside git could run a planted /tmp/.prettierrc + node_modules/.bin.
  • A failing clippy or golangci-lint run left its marker, so the next edit within 30 s passed silently.

Post-review fix (aa44ab3, test 46710e3): a follow-up security scan found a problem this PR introduced. Config discovery now walks up from the edited file, so for a .rs / .go file outside git the walk could reach a shared ancestor such as a planted /tmp/Cargo.toml (or go.mod + .golangci.yml). It then ran cargo clippy (which executes build.rs and proc macros) or golangci-lint there. The rs and go branches now exit early outside git, like the JS branch. New cases S/(g) cover this. format-and-lint is now 62/0, and ci-checks PASS.

Gates

Gate Result
test-first PASS (986ac38 red 41 fail → 323556d; security ae393dc red 5 fail → 102b45a)
ci-checks PASS (format-and-lint 62/0 after the post-review fix; all evals green)
runtime-verify (sonnet) PASS: 13/13, real ruff 0.16.10 / gofmt go1.27.1 / rustfmt 1.10.0. All of issue #180's controls hold: lint-only config → untouched; no config → imports kept; unfixable error → rc 2 with the diagnostic on stderr. Dirty-HEAD files are untouched, and lib.rs formatting leaves foo.rs byte-identical
review PASS: full review at 5a48e5c, then a focused security re-review of the fix pass with planted payloads

Follow-ups (nits, not blocking)

  • A marker value with a leading zero (08) is read as octal and errors, silently skipping the linter until the marker is removed. It can't be planted: the directory is 0700 and the hook only writes date +%s. Fix with 10#$last.
  • Add a golangci-lint copy of the "failing lint drops its marker" test case.

Version 3.49.0 → 3.50.0, plus a releases.json entry.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Ki3TDnP2bNqqpEkFoZaEXq

cahenesy and others added 6 commits October 6, 2026 09:15
Stub-tool eval of hooks/format-and-lint.sh (TDD 0068 observations 1-12),
registered in tests/implement-gate.test.sh. Red against the current hook.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Ki3TDnP2bNqqpEkFoZaEXq
Fixes #180. Config is discovered from the edited file's directory up to its
git toplevel, never $PWD. Lint runs only when configured and is report-only
(no --fix; diagnostics on stderr, exit 2). Formatting is opt-in for Python
([tool.ruff.format] / [format]) and JS/TS (Prettier config + local bin);
Rust/Go format only a git file whose HEAD copy is formatter-clean or new,
Rust via stdin so child modules are untouched. golangci-lint needs a
.golangci.* config; debounce is keyed by project root. Bootstrap writes the
format opt-in on every formatter-writing path; README gains the hook
paragraph and the 3.50.0 Python upgrade note.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Ki3TDnP2bNqqpEkFoZaEXq
Review pass 1 findings 1-3: hostile debounce marker content (arithmetic
subscript injection) and symlinked markers, JS/TS tools outside any git
work tree, and a failing clippy run masked by the debounce marker.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Ki3TDnP2bNqqpEkFoZaEXq
…review)

- Debounce markers move to a per-user 0700 dir
  (${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}/throughline-lint-<uid>); a dir that is
  a symlink or not ours, or a symlinked marker, is never read or written (the
  linter runs). Marker content is used only when all digits, so `x[$(cmd)]`
  can no longer execute via bash arithmetic.
- A failing clippy/golangci-lint run removes its marker, so the next edit
  re-runs and re-reports instead of passing silently for the window.
- JS/TS: a file outside any git work tree runs no prettier/eslint, so a
  planted ancestor config + node_modules/.bin can't execute.
- rustfmt temp file is removed by an EXIT trap; README states clippy runs for
  any Cargo project and the debounce behaviour.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Ki3TDnP2bNqqpEkFoZaEXq
cahenesy and others added 2 commits October 6, 2026 16:06
Outside a git work tree the Cargo.toml / go.mod walk has no repo
toplevel to stop at and can reach a shared ancestor such as /tmp, where
another user can plant a crate (build.rs, proc macros) or a golangci
config that loads plugins. The rs and go branches now exit early outside
git, matching the JS branch. Formatting was already skipped there.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NpGG21Y2VGGhTQaFr4oshB
@cahenesy
cahenesy merged commit 678a022 into master Oct 6, 2026
1 check passed
@cahenesy
cahenesy deleted the build/20261006-131111/0068-format-and-lint-honors-config branch October 6, 2026 20:08
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.

format-and-lint.sh formats and --fix-es files in repos that never configured a formatter (contradicts FR-21 / TDD 0001)

1 participant