Skip to content

fix(markdown-formatter): harden hook + simplify plumbing#11

Merged
kyle-sexton merged 5 commits into
mainfrom
fix/markdown-formatter-hook-correctness
Jun 27, 2026
Merged

fix(markdown-formatter): harden hook + simplify plumbing#11
kyle-sexton merged 5 commits into
mainfrom
fix/markdown-formatter-hook-correctness

Conversation

@kyle-sexton

Copy link
Copy Markdown
Contributor

Improvements to the markdown-formatter hook from a /simplify pass and a whole-repo /code-review (each finding adversarially verified before inclusion). Two commits, by concern.

fix: correctness (verified bugs)

Area Bug Fix
hook-utils.sh read_file_path Project-membership check != "$proj"* is a prefix glob with no boundary — a sibling like repo-backup/ is admitted as in-project, so the hook would format .md files outside the consumer repo Anchor on a path-segment boundary (exact root or root/*, trailing slash stripped)
hook-utils.sh normalize_path Only the drive letter is case-folded; the byte-exact compare false-negatives on case-insensitive Windows when file_path/CLAUDE_PROJECT_DIR differ only in body casing → in-repo file silently skipped Case-fold the whole Windows path for the comparison; POSIX paths unchanged (case-sensitive)
hook-utils.test.sh "jq absent" test shadowed jq with a function — command -v jq still succeeded, so the absence guard was never exercised (false coverage); dead readonly-unset/re-source Simulate real absence via empty PATH; remove the dead lines
both test files Fire-and-forget sink assertions raced a fixed sleep → intermittent failures on Windows Bounded wait_for_sink poll (slow-sink timing test untouched)
hook-utils.test.sh UTC-hour tolerance used linear distance → false-fail once/day at the 23→00 wrap Circular hour distance

refactor: simplify (behavior-neutral)

  • Collapse the triplicated data_json builder into one helper.
  • Build FINDINGS_JSON in a single jq pass (was O(n) processes / O(n²) re-serialization).
  • Remove a dead TOOL default.
  • Derive the ci-status RESULTS from the needs graph (join(needs.*.result, ' ')) instead of a hand-synced second list.

Validation

hook-utils.test.sh 27/0; markdown-format.test.sh 40 pass (the 1 "fail" is a pre-existing slow-sink timing assertion that fails on Windows Git Bash before any of these changes). Shellcheck clean. Membership/normalize fixes verified against live temp-dir scenarios.

🤖 Generated with Claude Code
https://claude.ai/code/session_01KPusuNL65RexZQQQ4SqL7t

kyle-sexton and others added 2 commits June 27, 2026 14:59
…list

- Collapse the triplicated data_json envelope builder into one
  build_data_json helper (skipped / ok-clean / ok-findings paths).
- Build FINDINGS_JSON in a single jq pass instead of re-spawning jq per
  matched line (was O(n) processes and O(n^2) re-serialization).
- Drop a dead TOOL default that the line above always assigns.
- Derive the ci-status RESULTS check from the needs graph
  (join(needs.*.result, ' ')) instead of a hand-maintained second list
  that had to be kept in sync by hand.

Behavior-neutral; test suites unchanged and green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KPusuNL65RexZQQQ4SqL7t
Correctness fixes from an adversarially-verified review:

- read_file_path: anchor the project-membership check on a path-segment
  boundary so a sibling directory that merely shares the prefix (e.g.
  repo-backup/ vs repo/) is no longer admitted as in-project.
- normalize_path: case-fold the whole Windows path (not just the drive
  letter) so a case-only difference between file_path and
  CLAUDE_PROJECT_DIR doesn't skip a genuinely in-repo file. POSIX paths
  pass through unchanged (case-sensitive).
- tests: exercise the jq-absence guard via an empty PATH — a shell-function
  shadow left command -v jq succeeding, so the guard was never hit; remove
  the dead readonly-unset/re-source.
- tests: replace racy fixed sleeps with a bounded wait_for_sink poll so the
  fire-and-forget sink assertions stop racing process-spawn latency.
- tests: use a circular hour distance for the UTC alignment check so it
  doesn't false-fail once per day at the 23->00 boundary.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KPusuNL65RexZQQQ4SqL7t
@kyle-sexton
kyle-sexton force-pushed the fix/markdown-formatter-hook-correctness branch from 06a63f3 to f859176 Compare June 27, 2026 18:59

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 06a63f369b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread plugins/markdown-formatter/hooks/hook-utils.sh Outdated
kyle-sexton and others added 3 commits June 27, 2026 16:36
The unconditional ${rest,,} fold collapsed /c/Repo and /c/repo to the same
C:/repo on case-sensitive POSIX hosts, so hook::read_file_path could admit a
markdown file in a sibling directory outside CLAUDE_PROJECT_DIR. Gate the
drive-letter fold on OSTYPE (msys/cygwin/win32) so it applies only where the
filesystem is actually case-insensitive; POSIX paths pass through unchanged.

Correct the function comment, which wrongly claimed the drive regex never
matches on macOS/Linux. Add OSTYPE-aware unit tests covering both host classes
and the membership-guard regression.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HR4FCVWrioJ1dvfLJk1mcV
The /home/u/Pj test literal tripped the machine-specific-paths hygiene lane
(flagged as a Linux user path). Swap it for /opt/App/Sub — still a mixed-case
absolute POSIX path proving the no-fold pass-through, without a user-home shape.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HR4FCVWrioJ1dvfLJk1mcV
@kyle-sexton
kyle-sexton merged commit d726791 into main Jun 27, 2026
14 checks passed
@kyle-sexton
kyle-sexton deleted the fix/markdown-formatter-hook-correctness branch June 27, 2026 20:42
kyle-sexton added a commit that referenced this pull request Jul 20, 2026
…ss when git clean failed (#395) (#590)

## Summary

`git-tree-reset.sh --apply` printed `AppliedClean: git clean -fdx …`
**unconditionally**, regardless of whether `git clean` actually
succeeded — a false success signal that misleads any operator or
automation keying off that line to conclude the tree reached a
known-good state.

Scope note: the sibling `AppliedReset` false-success and the
unresolvable-`@{u}` gate that issue #395 references were already closed
by #460 (reset-failure aborts clean, exit 5) and #542
(upstream-unresolved block, exit 6). This PR fixes the one remaining
unguarded case — `AppliedClean` — so #395's acceptance criteria are
fully met.

## Fix

- Capture `git clean -fdx`'s exit status (`CLEAN_RC`), read on the line
immediately after the capture so `$?` isn't clobbered (the script runs
`set -uo pipefail`, no `-e`).
- Gate the `AppliedClean` success line on the real outcome. A non-zero
clean exit whose cause is **locked/in-use files** is the expected
non-fatal case — `git clean` returns non-zero when it fails to remove
any path, and that case is already reported honestly via `Unremovable:`
— so it is **not** treated as failure (avoids regressing the deliberate
locked-file handling on Windows). Only a non-zero exit with **no**
`failed to remove` warnings is a genuine failure: it emits an explicit
`FAILED:` line, `AppliedClean: failed`, and exits **7** instead of a
success line.
- Emit the `AppliedReset:` success line as soon as `reset --hard`
genuinely succeeds — **before** `clean` — so a subsequent clean failure
still surfaces the truthful reset outcome rather than swallowing it.
- Run the reparse-point restore guard on the failure path too (data-loss
guard: a clean that errored mid-run may still have deleted tracked files
first; safe because `reset --hard` ran first).
- Documented exit 7 in the script header, `usage()`, and the `clean`
skill's `context/git-tree-reset.md` gates list.

## Verification

New test case #11 (`git-tree-reset.test.sh`) forces a genuine clean
failure via a `PATH` shim that intercepts only `git clean` (delegating
every other subcommand — crucially `reset` — to the real git, so the
reset genuinely succeeds). The full suite passes, including the 6 new
assertions:

```
PASS: [38] clean failure exits 7
PASS: [39] clean failure reports failure
PASS: [40] clean failure emits AppliedClean: failed
PASS: [41] clean failure emits no clean success line
PASS: [42] clean failure still reports the successful reset
PASS: [43] clean failure leaves untracked intact (clean did not silently succeed)
OK: git-tree-reset.sh tests passed
```

(All 43 tests pass; the pre-existing reset-failure suite #22–27 and
upstream-unresolved suite #28–37 continue to pass — no regression.
`shellcheck` is clean on both files.)

Live `--apply` run against a repo where `git clean` fails after a
successful reset — the report is now honest and exits non-zero:

```
AppliedReset: git reset --hard main
FAILED: git clean -fdx exited 1 (non-locked-file cause) — untracked removal incomplete; reset --hard already applied.
fatal: simulated git clean failure
AppliedClean: failed
RestoredTracked: 0
EXIT=7
scratch.txt still present (clean did NOT silently succeed): YES
```

Before this fix the same run printed `AppliedClean: git clean -fdx` and
exited `0`.

Closes #395

## Related

- #395 — this PR.
- #396 — sibling issue on the same script (dry-run does not validate
`@{u}` upstream). **Not addressed here** to avoid a merge collision on
the same lines; a separate cycle picks it up.

🤖 Generated with a Claude Code implementation subagent (issue #395)

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
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