feat: add eol-normalizer plugin#38
Conversation
Symmetric working-tree line-ending normalization on Write|Edit, driven by the consuming repo's own .gitattributes via git check-attr. LF arm runs on every OS; CRLF arm self-gates to Windows. Advisory (exit 0), kill switch HOOK_EOL_NORMALIZER_ENABLED, opt-in telemetry envelope to HOOK_TELEMETRY_SINK. Bundles normalize-eol.sh in-plugin; hook-utils.sh is a byte-identical copy of the marketplace lib. Ports the medley hook's black-box contract tests as a self-contained, cwd-independent suite.
Argument-injection hardening (CWE-88): add `--` option terminators before
the filename in the perl, mv, and rm invocations so a path beginning with `-`
cannot be parsed as an option.
Predictable temp file (CWE-377): replace fixed `.lf.tmp`/`.crlf.tmp` staging
in the non-perl tr/awk fallback arms with a same-dir `mktemp "${file}.XXXXXX"`,
preserving the atomic same-filesystem mv.
EOL normalization semantics unchanged (contract test suite 27/27 green).
Intentionally diverges from the medley source copy (retained for lefthook use);
the medley-side fix is queued for the Phase 4 cutover boy-scout pass.
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_7d8d27dd-cdcf-4832-9149-dbffecfc82dc) |
|
Claude finished @kyle-sexton's task in 3m 9s —— View job PR Review: feat/add eol-normalizer pluginTasks
SummaryWell-constructed plugin. The architecture is sound: FindingsMedium —
|
There was a problem hiding this comment.
Stale comment
Risk: medium. Cursor Bugbot did not complete (check skipped due to usage limit), so I cannot approve without its automated review signal. Human review is needed; no reviewers were assigned because no non-author reviewers are available in this repository.
Sent by Cursor Approval Agent: Pull Request Router and Approver
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0b83dc1a8e
ℹ️ 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".
…lizer # Conflicts: # .claude-plugin/marketplace.json
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_090d216a-7c06-441a-bf07-9d38e0bed955) |
|
Claude finished @kyle-sexton's task in 3m 6s —— View job PR Review: feat/add eol-normalizer pluginTasks
This is a re-review triggered by the sync merge commit (389aaf4) that brought in the SummarySolid architecture. The core design is sound:
FindingsMedium —
|
| Criterion | Status |
|---|---|
| Repo-agnostic (no hardcoded paths/names) | ✓ |
| Configurable without editing the plugin | ✓ (.gitattributes is the sole authority; kill-switch via env) |
Plugin-form-safe (${CLAUDE_PLUGIN_ROOT}, no ../ reach-outs) |
✓ |
| No PII/secrets | ✓ |
Versioned ("version": "0.1.0" in plugin.json) |
✓ |
| Security-reviewed (arg terminators, mktemp staging, jq --arg, exit 0) | ✓ |
| marketplace.json entry consistent | ✓ |
Positives
- Advisory contract — always exits 0; the hook is self-described as best-effort throughout.
- Security hardening —
--arg terminators onperl -pi,mv,rm,git check-attr;mktempstaging;jq --argthroughout. - Stdin consumed once —
INPUT=$(cat)avoids pipe double-drain on Windows Git Bash. - OS gating is internally consistent with the documented design: LF arm unconditional, CRLF arm Windows-only.
- Test coverage — 27 assertions including kill-switch, telemetry envelope schema (7 fields), action/status parity.
hook-utils.shis in sync — confirmed bit-for-bit identical tolib/hook-utils.sh.plugin.json,hooks.json,marketplace.json— structurally consistent with existing plugins.
Recommended action before merge: (1) Get a clear yes/no from the author on whether the CRLF cross-platform gap is intentional design (and if so, document it in the README behavior matrix). (2) Fix the GNU awk portability issue in normalize_eol_to_crlf. The remaining findings are low/informational.
There was a problem hiding this comment.
Stale comment
Risk: medium. Cursor Bugbot did not complete successfully (check skipped due to usage limit), so I cannot approve without its automated review signal. Human review is needed; no reviewers were assigned because no non-author reviewers are available in this repository.
Sent by Cursor Approval Agent: Pull Request Router and Approver
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 389aaf4e1e
ℹ️ 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".
…mode preservation Address Codex review findings on #38: - P1: eol=lf alone is not proof of text — under '* text=auto eol=lf', check-attr reports lf for binaries too. Mirror gitattributes semantics: trust explicit 'text', skip '-text', NUL-sniff the first 8000 bytes for text=auto (git's own detection window). - P2: run the LF->CRLF arm on every OS, not just Windows — an LF write to an eol=crlf path bypasses smudge and violates policy on any platform. - P2: preserve the original file mode in the no-perl mktemp fallbacks via cp -p before staging (chmod --reference is GNU-only).
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_71ad7fea-fc17-47e3-b824-580a76369708) |
|
Claude finished @kyle-sexton's task in 3m 32s —— View job PR Review: feat/add eol-normalizer plugin (re-review after dfb3e29)Tasks
SummaryThe latest commit (dfb3e29 — "binary guard, cross-platform crlf arm, fallback mode preservation") addresses every substantive concern from prior reviews and Codex. The three structural problems are now fixed cleanly and tested. Four low/medium findings from prior reviews remain open; the awk portability finding has increased in severity with the cross-platform change. Fixed in dfb3e29 ✅
All three fixes are clean and the corresponding tests (Cases 4–7 in the updated suite) provide good coverage of the new behavior. Remaining FindingsMedium (severity increased) — GNU awk CRLF fallback now reachable on macOS/Linuxif awk 'BEGIN{RS="\r?\n"; ORS="\r\n"} {print}' "$file" >"$tmp"; thenThis finding from prior reviews is now higher severity because of the cross-platform CRLF fix. Previously the CRLF arm was Windows-only, and Git for Windows ships GNU awk — so Perl is practically universal (macOS bundles it; most distros do too), so this is still a narrow failure path. But the comment calls this the "portable fallback," and it is not portable. The Suggested fix — replace with a POSIX # Portable: strip lone CR, re-add CR before each LF.
if sed 's/\r$//' "$file" | sed 's/$/\r/' >"$tmp"; then
mv -- "$tmp" "$file"
else
rm -f -- "$tmp"
fiOr gate on elif command -v gawk >/dev/null 2>&1; then
gawk 'BEGIN{RS="\r?\n"; ORS="\r\n"} {print}' "$file" >"$tmp"
# ...
fiLow — Test JSON construction fragile for paths with shell-special characters (unaddressed)printf '{"tool_input":{"file_path":"%s"},"tool_name":"Write"}' "$file_path"
jq -nc --arg p "$file_path" '{"tool_input":{"file_path":$p},"tool_name":"Write"}'Low —
|
| Criterion | Status |
|---|---|
| Repo-agnostic | ✓ |
| Configurable without editing | ✓ |
| Plugin-form-safe | ✓ |
| No PII/secrets | ✓ |
| Versioned | ✓ |
| Security-reviewed | ✓ |
Summary
Three of the four prior medium/P1/P2 findings are cleanly resolved. The implementation is well-structured: proper gitattributes semantics (text + eol together), NUL binary detection matching git's own window, idiomatic cp -p mode preservation, and symmetric coverage in the test suite. The one remaining blocker-candidate is the GNU awk portability issue in normalize_eol_to_crlf — the cross-platform change that correctly makes the CRLF arm symmetric also widens the blast radius of this fallback path to macOS/Linux.
Recommended before merge: Fix the awk fallback in normalize_eol_to_crlf (replace with POSIX sed pipeline or gate on gawk). The three low findings can be addressed at any time.
There was a problem hiding this comment.
Risk: medium. Cursor Bugbot did not complete successfully (check skipped), so I cannot approve without its automated review signal. Human review is needed; no reviewers were assigned because no non-author reviewers are available in this repository.
Sent by Cursor Approval Agent: Pull Request Router and Approver
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: dfb3e295b0
ℹ️ 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".
| normalize_eol_to_lf() { | ||
| local file="$1" | ||
| if command -v perl >/dev/null 2>&1; then | ||
| perl -pi -e 's/\r\n/\n/g' -- "$file" |
There was a problem hiding this comment.
Preserve symlinks when rewriting EOLs
If a PostToolUse event names a symlink inside the project, the guard accepts it because -f follows symlinks, but this in-place rewrite replaces the symlink path with a regular file instead of updating the linked target; I verified this with a link.sh -> target.sh fixture, where the link was broken and the target kept its CRLF bytes. This can silently change repository topology for any symlinked text file governed by eol=lf (and the CRLF arm/fallback use the same replace-over-path pattern), so the hook should either skip symlinks or normalize the resolved target without replacing the link.
Useful? React with 👍 / 👎.
| local tmp | ||
| tmp=$(mktemp "${file}.XXXXXX") || return 0 | ||
| cp -p -- "$file" "$tmp" 2>/dev/null || true | ||
| if awk 'BEGIN{RS="\r?\n"; ORS="\r\n"} {print}' "$file" >"$tmp"; then |
There was a problem hiding this comment.
Preserve missing EOF newline in CRLF fallback
When perl is not on PATH, this fallback uses awk print, which terminates every output record with ORS; an eol=crlf file that intentionally has no final newline, such as printf 'abc', is rewritten as abc\r\n even though Git's own CRLF checkout preserves the missing EOF newline. That is a content change beyond line-ending normalization in the documented no-perl path, so the fallback should preserve whether the input ended with a newline.
Useful? React with 👍 / 👎.
PR #38 added the marketplace.json entry but not the root README catalog row; every other plugin PR carries both.
…ss lint (#203) ## Summary Closes decisions #38, #71, #72 (Decisions Log: https://claude.ai/code/artifact/232ecdce-8316-4880-8c0a-dc3c7dcf3a63). - **#38** `formatting-create-md-attribution-footer-drift`: `create.md`'s PR-body heredoc now emits the `🤖 Generated with Claude Code` attribution footer it documents elsewhere but previously didn't actually emit. - **#71** `naming-work-items-checklist-template-staleness`: `templates/checklist.md`'s `add` action reconciled to `add.md`'s actual current behavior — `--force` skip, `--type` native-Issue-Type resolution (org repos) vs. `type:` label (personal/non-org repos), `--agent-ready` body template, `--recurring` title prefix — none of which the checklist previously mentioned. - **#72** `tooling-gov-commit-skill-composition-lint`: new `guardrails` plugin hook `flag-commit-pr-skill-bypass.sh` — advisory `PreToolUse`/`Bash` guard that flags direct `git commit` (missing the canonical `-F -` + `--trailer` shape) or any `gh pr create`, nudging toward this marketplace's own `/commit` / `/pull-request create` skills. Gated on the consuming project actually having `source-control` enabled in its own `.claude/settings.json` (fails quiet on uncertain state, per the plugin's existing advisory-guard posture). Mirrors `block-hook-bypass.sh`'s literal-stripping detection and the shared telemetry envelope. ## Verification - New hook's own test suite: 19/19 passed. - `shellcheck` on both the hook and its test — clean. - `guardrails` version bumped 0.3.2 → 0.4.0 (new toggleable guard), README's guard table/kill-switch table/consumer-seams section updated to match. --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>


Migrates the EOL-normalization PostToolUse hook into a repo-agnostic plugin. Bundles normalize-eol.sh via ${CLAUDE_PLUGIN_ROOT}; consumer conventions from .gitattributes/.editorconfig; kill switch HOOK_EOL_NORMALIZER_ENABLED; opt-in telemetry. Security-hardened (-- arg terminators + mktemp staging). 27 contract tests; plugin validate clean. Clean-tier wave Phase 1.
Note
Medium Risk
The hook mutates files on disk immediately after agent edits; guards and best-effort behavior limit damage, but wrong attribute resolution could still rewrite content outside intent.
Overview
Introduces a new eol-normalizer formatting plugin and lists it in the marketplace catalog.
On PostToolUse for
Write|Edit, the hook reads the edited path from stdin (with Windows Git Bash–safe single read), resolves the consuming repo root, and runsnormalize_eol_fileso working-tree endings match.gitattributes(eol=lf→ CRLF→LF,eol=crlf→ LF→CRLF on all platforms). Paths withouteol=,-text, or text=auto binaries (NUL sniff) are left alone. The hook is advisory (always exit 0), disable viaHOOK_EOL_NORMALIZER_ENABLED, and optionally emits telemetry through the sharedhook-utilscopy bundled in the plugin.The plugin ships
normalize-eol.sh(perl withtr/awk+mktempfallbacks that preserve file mode),hooks.jsonwiring, README, and a black-boxeol-normalizer.test.shsuite covering LF/CRLF arms, kill switch, binary/-textguards, and telemetry envelopes.Reviewed by Cursor Bugbot for commit dfb3e29. Bugbot is set up for automated code reviews on this repo. Configure here.