Skip to content

feat(eol-normalizer): disclose line-ending rewrites on the user channel - #2458

Merged
kyle-sexton merged 3 commits into
mainfrom
cursor/eol-normalizer-mutation-disclosure-63e1
Aug 12, 2026
Merged

kyle-sexton merged 3 commits into
mainfrom
cursor/eol-normalizer-mutation-disclosure-63e1

Conversation

@kyle-sexton

@kyle-sexton kyle-sexton commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

No linked issue

Summary

  • Emit a systemMessage when the hook normalizes line endings to LF or CRLF
  • Skip and no-op paths stay silent

Partially addresses #1596 (eol-normalizer only; sibling autofix hooks remain tracked there).

Test plan

  • bash plugins/eol-normalizer/hooks/eol-normalizer.test.sh (36 passed)

Related

Refs #1596

@cursor

cursor Bot commented Aug 12, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@claude

claude Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 1m 20s —— View job


Security Review

  • Gather context (diff + changed files)
  • Review eol-normalizer.sh changes for injection / trust-boundary issues
  • Review eol-normalizer.test.sh changes
  • Review plugin.json / CHANGELOG.md changes
  • Post findings

Scope: plugins/eol-normalizer/hooks/eol-normalizer.sh, plus the accompanying test, plugin.json version bump, and CHANGELOG.md entry.

No security issues found.

Details on what I checked:

  • The new hook::emit_system_message call (L116‑126) interpolates $(basename "$FILE") into a plain string that is passed through the existing hook::emit_channelshook::json_escape pipeline (unchanged in this PR), which escapes \, ", and control characters before the string is embedded in the JSON stdout payload. A malicious/unusual file path (embedded quotes, newlines, backslashes) cannot break out of the JSON systemMessage field or corrupt hook output.
  • $FILE is passed to basename as a properly double-quoted argument ("$FILE"), not interpolated into a shell command string, so there's no command-injection surface from unusual file names.
  • The disclosure only fires on the lf/crlf action arms — skip/no-op paths stay silent, matching the stated intent; there's no path where the message could fire with stale/incorrect action data.
  • No new external input is consumed, no new file writes/permissions, no secrets/tokens handled, and no privilege or trust-boundary change — this is purely a new advisory message on an already-computed, already-mutated file.
  • plugin.json / CHANGELOG.md changes are a version bump and documentation only — no config or permission changes.
  • Test script changes only adjust assertions to expect the new systemMessage content; no security-relevant behavior there.

@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: ca98197e21

ℹ️ 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/eol-normalizer/hooks/eol-normalizer.sh Outdated
@kyle-sexton
kyle-sexton force-pushed the cursor/eol-normalizer-mutation-disclosure-63e1 branch from 3f54c3b to 5a5a997 Compare August 12, 2026 16:22
@claude

claude Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 1m 0s —— View job


Security Review

  • Gather context (diff + changed files)
  • Review eol-normalizer.sh changes for injection / trust-boundary issues
  • Review eol-normalizer.test.sh changes
  • Review plugin.json / CHANGELOG.md changes
  • Post findings

Scope: plugins/eol-normalizer/hooks/eol-normalizer.sh, plus the accompanying test, plugin.json version bump, and CHANGELOG.md entry.

No security issues found.

Details on what I checked:

  • The new hook::emit_system_message calls (L116‑128) interpolate $(basename "$FILE") into a plain string. $FILE is passed to basename as a properly double-quoted argument, not into a constructed shell command, so there's no command-injection surface from unusual file names (embedded spaces, $(), backticks, etc. are inert here).
  • That string is passed through the unchanged hook::emit_system_messagehook::emit_channelshook::json_escape pipeline (hook-utils.sh#L62-L107), which escapes \, ", and line-structure control bytes and strips the remaining C0 bytes before the string is embedded in the JSON stdout payload — a crafted file path (embedded quotes, newlines, backslashes) cannot break out of the JSON systemMessage field or inject additional JSON keys/values into the hook's stdout.
  • The disclosure only fires on the lf/crlf arms of $ACTION; the skip arm (and the added *) ;; ;; default) stay silent, so there's no path where stale/uninitialized action data could produce a message.
  • No new external input is consumed (still the same INPUT/FILE sourced earlier via the existing hook::buffer_stdin / hook::read_file_path path, both unchanged in this PR), no new file writes/permissions, no secrets/tokens/credentials handled, and no privilege or trust-boundary change — this is purely an advisory user-channel message describing a mutation the hook already performs.
  • plugin.json / CHANGELOG.md changes are a version bump and documentation only.
  • Test script changes only adjust assertions to expect the new systemMessage content; no security-relevant behavior there.

Note: Codex flagged that the disclosure can fire even when the file's bytes didn't actually change (idempotent lf/crlf arms in normalize_eol_file, normalize-eol.sh#L76-L89) — that's a real correctness/accuracy issue with the disclosure message, but it has no security impact (no injection, no unauthorized disclosure, no trust-boundary effect), so it's out of scope for this security-focused review; deferring to that existing review thread rather than duplicating it here.

@claude

claude Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 1m 49s —— View job


Code Review

  • Gather context (diff + changed files)
  • Review eol-normalizer.sh changes for correctness / error handling
  • Review eol-normalizer.test.sh coverage
  • Review plugin.json / CHANGELOG.md
  • Post findings

Scope: plugins/eol-normalizer/hooks/eol-normalizer.sh, eol-normalizer.test.sh, plugin.json, CHANGELOG.md. (Security is covered by the dedicated security lane per REVIEW.md and already posted above — not duplicated here.)

🔴 Finding: disclosure fires on no-op writes, contradicting the PR's own "stay silent on skip/no-op paths" contract

Posted inline on eol-normalizer.sh:116-128. Summary: ACTION reflects which normalization arm was selected (the file's eol= attribute), not whether bytes actually changed — normalize_eol_to_lf/normalize_eol_to_crlf in normalize-eol.sh:94-132 are idempotent by design. So a write that already uses the correct line ending still gets the new systemMessage claiming a normalization occurred. This is a correctness bug against the PR's own stated intent and the CHANGELOG's claim, and it's also a test-coverage gap: every fixture in eol-normalizer.test.sh seeds the wrong ending before invoking the hook, so the false-positive path (already-correct content) is never exercised. I independently traced the library code and test fixtures to confirm this — it matches the root cause the Codex review comment already flagged on the same line, but I verified it myself rather than taking that comment at face value.

Other areas checked, no issues found

  • plugin.json version bump (0.6.70.6.8) matches the new CHANGELOG.md entry.
  • The case "$ACTION" default arm (added in the follow-up commit) correctly satisfies shellcheck SC2249 and is a harmless no-op for the skip path.
  • Telemetry emission (emit_tel) is unchanged by this PR and unaffected by the finding above (it already worked off the same ACTION semantics before this PR).
  • Note: I could not run git diff origin/main...HEAD in this sandbox (network fetch for origin/main history required approval that wasn't available) — I reviewed the current state of the four changed files directly instead, which is sufficient since the whole feature is new/additive. The PR description's own test-plan claim (36 passed) is author-reported and not independently re-run by me.

Comment thread plugins/eol-normalizer/hooks/eol-normalizer.sh Outdated
@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Claude has reviewed this PR 5 times. The lane skips further automatic reviews after 5; deleting this comment resets the count.

@kyle-sexton
kyle-sexton force-pushed the cursor/eol-normalizer-mutation-disclosure-63e1 branch from 5a5a997 to e596a81 Compare August 12, 2026 16:52
@claude

claude Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 59s —— View job


Security Review

  • Gather context (diff + changed files)
  • Review eol-normalizer.sh changes for injection / trust-boundary issues
  • Review eol-normalizer.test.sh changes
  • Review plugin.json / CHANGELOG.md changes
  • Post findings

Scope: plugins/eol-normalizer/hooks/eol-normalizer.sh:116-128 (new hook::emit_system_message calls), plus eol-normalizer.test.sh, plugin.json, CHANGELOG.md.

No security issues found.

What I checked:

  • Injection via the interpolated file name. $(basename "$FILE") is passed to basename as a quoted argument, not built into a shell command string — no command-injection surface from crafted file names (embedded $(), backticks, spaces, etc. are inert here).
  • JSON-breakout / stdout-corruption. The resulting string flows through the unchanged hook::emit_system_messagehook::emit_channelshook::json_escape pipeline, which escapes \, ", \n, \r, \t and strips residual C0 control bytes before the string is embedded in the "systemMessage":"..." field. A file path with embedded quotes/backslashes/newlines cannot break out of the JSON string or inject additional keys into the hook's stdout payload.
  • Prompt-injection via the agent-context channel. This change only calls hook::emit_system_message, which composes with ctx="" — i.e. it populates systemMessage (user-facing) and never additionalContext (the channel fed back into the agent's context). So even a maliciously-named file (e.g. one containing instruction-like text) can't inject content into the agent's context through this code path — worst case is a misleading string shown to the human user.
  • New external input / trust boundary. No new input source is introduced — FILE is the same value already produced by hook::read_file_path/hook::buffer_stdin earlier in the script (unchanged by this PR), and the disclosure only reads it for display, never re-uses it in a command.
  • plugin.json / CHANGELOG.md — version bump and documentation only, no config/permission changes.
  • Test script changes — assertion updates to expect the new systemMessage content; introduce no new execution paths or security-relevant behavior.

Note: there's a known correctness issue (already reported by both Codex and a prior Claude review on this same line) where the disclosure can fire on idempotent/no-op writes where bytes didn't actually change. That's a functional accuracy bug against the PR's stated "stay silent on no-op paths" intent, not a security issue (no injection, no unauthorized disclosure, no trust-boundary effect), so it's out of scope for this security-focused pass.

@kyle-sexton
kyle-sexton force-pushed the cursor/eol-normalizer-mutation-disclosure-63e1 branch from 845dc3d to 321c6d0 Compare August 12, 2026 17:14
@claude

claude Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 3m 7s —— View job


Security Review

  • Gather context (diff + changed files)
  • Review eol-normalizer.sh changes for injection / trust-boundary issues
  • Review eol-normalizer.test.sh changes
  • Review plugin.json / CHANGELOG.md changes
  • Post findings

Scope: plugins/eol-normalizer/hooks/eol-normalizer.sh (new before/after mktemp compare + systemMessage disclosure), plus eol-normalizer.test.sh, plugin.json, CHANGELOG.md.

🟡 Finding: idempotency-check temp copy is written to a shared /tmp with the source file's permissions, and isn't cleaned up if the hook is killed

Severity: IMPORTANT · Confidence: Medium

eol-normalizer.sh:105-113:

_eol_tmp="$(mktemp "${TMPDIR:-/tmp}/eol-normalizer.XXXXXX")"
cp -p "$FILE" "$_eol_tmp"
ACTION=$(normalize_eol_file "$REPO_ROOT" "$FILE")
...
rm -f "$_eol_tmp"

mktemp creates _eol_tmp with the safe default mode 0600, but cp -p immediately overwrites that mode with $FILE's own permission bits (that's what -p is for: preserve mode/ownership/timestamps from the source). If the file being edited has group/world-readable permissions — the common case for most repo files — the temp copy in ${TMPDIR:-/tmp} (world-listable/traversable on a typical Linux host, drwxrwxrwt) inherits that same readability. Unlike the original file, which is only reachable by another local user if every parent directory up to it is also traversable (e.g. a repo under a 700 home directory is protected), the /tmp copy sits in a directory every local user can list and enter, so this briefly re-exposes the full content of whatever file is being edited (which can include .env files, checked-in credentials, private config, etc.) to any other local user on a shared/multi-tenant host, bypassing whatever directory-permission boundary protected the original.

This isn't just a microsecond race: the hook has a hard 15s kill timeout (hooks.json:10). If normalize_eol_file (or its perl/awk fallback path) runs long on a large file and gets killed, execution never reaches rm -f "$_eol_tmp" on line 113, so the copy is orphaned in /tmp with the source's original permissions and persists until the OS/tmp-cleanup policy clears it — not just for the duration of one hook invocation.

Notably, the neighboring library code in this same plugin already solves an equivalent problem correctly: normalize_eol_to_lf/normalize_eol_to_crlf in normalize-eol.sh:103-105 explicitly stage their temp file in the same directory as the target file (mktemp "${file}.XXXXXX") with a comment crediting this as the CWE-377-safe pattern, rather than moving a copy into a shared global temp directory. The new code in eol-normalizer.sh doesn't follow that established pattern.

Suggested fix: stage _eol_tmp next to $FILE (same directory, same permission boundary — matching normalize-eol.sh's own pattern) instead of ${TMPDIR:-/tmp}, and/or add trap 'rm -f "$_eol_tmp"' EXIT so a killed hook doesn't leak the copy. If a same-directory temp file isn't desired, at minimum drop -p (or chmod 600 "$_eol_tmp" after the copy) so the mode isn't widened past mktemp's safe default.

Fix this →

Other areas checked, no issues found

  • Injection via $(basename "$FILE") in the new hook::emit_system_message calls (L128-136): $FILE is passed to basename as a quoted argument, not interpolated into a constructed shell command — no command-injection surface. The resulting string flows through the unchanged hook::emit_system_message → hook::emit_channels → hook::json_escape pipeline (not touched by this PR), which escapes \, ", and control bytes before embedding in the JSON systemMessage field, so a crafted file name can't break out of the JSON payload. Already covered by prior review rounds on this same PR; re-verified against the current head, unchanged.
  • EFFECTIVE_ACTION / idempotency compare logic (cmp -s "$FILE" "$_eol_tmp"): no new external input, no new privilege boundary; this is the fix for the previously-flagged correctness bug (disclosure firing on no-op writes) and is out of scope for a security-focused pass beyond the temp-file handling noted above.
  • plugin.json / CHANGELOG.md: version bump and documentation only.
  • eol-normalizer.test.sh: assertion updates only, no new execution paths.

@claude

claude Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 3m 44s —— View job


Code Review

  • Gather context (diff + changed files)
  • Review eol-normalizer.sh changes for correctness / error handling
  • Review eol-normalizer.test.sh coverage
  • Review plugin.json / CHANGELOG.md
  • Post findings

Scope: plugins/eol-normalizer/hooks/eol-normalizer.sh, eol-normalizer.test.sh, normalize-eol.sh (read for context, unchanged by this PR), plugin.json, CHANGELOG.md at HEAD 321c6d0. Security is covered by the dedicated security lane per REVIEW.md — not duplicated here.

Latest commit (321c6d0, "stay silent on idempotent normalization") is a genuine fix: it correctly resolves the no-op-disclosure bug flagged by Codex and a prior Claude review on this same PR, by snapshotting the file and comparing bytes before/after normalize_eol_file rather than trusting which arm was selected.

🔴 Test regression: fixing that bug appears to have broken a pre-existing test

Posted inline on eol-normalizer.test.sh:228-234. The "mutation disclosure: LF normalization names target ending" case reruns the hook on $REPO/tel2.sh, which the immediately-preceding block already normalized to LF — so this second run is now a true idempotent no-op. With the 321c6d0 fix in place, that should make EFFECTIVE_ACTION="skip" and suppress the systemMessage, so the assertion expecting one should fail. This test block predates the fix (added in the original feature commit) and wasn't touched by either follow-up commit. I traced this by reading the code; I could not execute the suite myself in this sandbox (no interactive approval available for Bash), so this needs to be run locally to confirm — the PR's "36 passed" test-plan entry likely predates this fix or wasn't re-run against it.

🟡 Perf/architecture: the new snapshot runs unconditionally on every write, not just eligible ones

Posted inline on eol-normalizer.sh:105-113. The mktemp/cp -p/cmp/rm sequence now runs for every write the hook handles, including the skip paths (unspecified eol=, -text, binary) where the file is provably untouched — 4 extra subprocess spawns on what the hook's own comments call "the hottest path in the fleet." Suggest moving the snapshot/compare inside normalize_eol_file's lf/crlf arms so the cost only applies to files actually eligible for normalization.

Other areas checked, no issues found

  • plugin.json (0.6.70.6.8) and the CHANGELOG.md [0.6.8] entry are consistent with each other, and — now that the idempotency fix landed — the entry's "Skip and no-op paths stay silent" claim is accurate against the code.
  • The case "$EFFECTIVE_ACTION" default arm satisfies shellcheck SC2249 and is a harmless no-op.
  • emit_tel continues to key its status/data.action fields correctly relative to the new EFFECTIVE_ACTION/ACTION split; no inconsistency found there.

Comment thread plugins/eol-normalizer/hooks/eol-normalizer.test.sh
Comment thread plugins/eol-normalizer/hooks/eol-normalizer.sh
@cursor
cursor Bot force-pushed the cursor/eol-normalizer-mutation-disclosure-63e1 branch from 321c6d0 to a216239 Compare August 12, 2026 18:54
@claude

claude Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Claude finished @cursor[bot]'s task in 1m 58s —— View job


Security Review

  • Gather context (diff + changed files)
  • Review eol-normalizer.sh changes for injection / trust-boundary / temp-file issues
  • Review eol-normalizer.test.sh changes
  • Review plugin.json / CHANGELOG.md changes
  • Post findings

Scope: plugins/eol-normalizer/hooks/eol-normalizer.sh at HEAD a216239, plus eol-normalizer.test.sh, plugin.json, CHANGELOG.md.

🟡 IMPORTANT — idempotency-check temp copy still lands in shared /tmp with the source file's permission bits, no cleanup on kill

Posted inline on eol-normalizer.sh:105-113. This was flagged on an earlier revision of this same commit and is unchanged at the current head:

_eol_tmp="$(mktemp "${TMPDIR:-/tmp}/eol-normalizer.XXXXXX")"
cp -p "$FILE" "$_eol_tmp"

mktemp creates the temp copy with the safe default mode 0600, but cp -p immediately widens that to $FILE's own mode. Most repo files are group/world-readable, so the copy sitting in ${TMPDIR:-/tmp} (drwxrwxrwt — every local user can list/enter it) inherits that readability, briefly exposing the full content of whatever file was just written (potentially a .env, checked-in credential, or private config) to any other local user on a shared host — a boundary the original file's own directory permissions may not have granted. The hook also has a hard 15s kill timeout (hooks.json:10); if killed mid-run, rm -f "$_eol_tmp" on line 113 never executes and the copy is orphaned in /tmp.

The sibling library code in this same plugin already solves the equivalent problem correctly — normalize-eol.sh:104 stages its temp file in the same directory as the target file (mktemp "${file}.XXXXXX"), citing CWE-377, instead of a shared global temp dir. Suggested fix: stage _eol_tmp next to $FILE and/or add trap 'rm -f "$_eol_tmp"' EXIT; at minimum drop -p (or chmod 600 after the copy) so the mode isn't widened past mktemp's safe default.

Other areas checked, no issues found

  • Injection via $(basename "$FILE") in the hook::emit_system_message calls (L128-136): $FILE is passed to basename as a quoted argument, not interpolated into a constructed command — no command-injection surface. The result flows through the unchanged hook::emit_system_message → hook::emit_channels → hook::json_escape pipeline, which escapes \, ", and control bytes before embedding in the JSON systemMessage field, so a crafted file path can't break out of the JSON payload or inject additional keys.
  • Prompt-injection via the agent-context channel. hook::emit_system_message composes with ctx="", so this only ever populates systemMessage (user-facing), never additionalContext (the agent-context channel) — a maliciously-named file can't inject content back into the agent's context this way.
  • EFFECTIVE_ACTION idempotency-compare logic itself (cmp -s): no new external input, no new privilege boundary — this is the fix for a previously-flagged correctness bug (disclosure firing on no-op writes), and outside the temp-file handling above it introduces no new security-relevant behavior.
  • plugin.json / CHANGELOG.md: version bump and documentation only, no config/permission changes.
  • eol-normalizer.test.sh: assertion updates only, no new execution paths or security-relevant behavior. (Note: there is a likely test-correctness regression at line 228-234 reusing an already-normalized fixture — already reported by a prior code-review pass on this PR; no security impact, so not duplicated here.)
    · branch cursor/eol-normalizer-mutation-disclosure-63e1

Comment thread plugins/eol-normalizer/hooks/eol-normalizer.sh
@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Last security-reviewed head: 0d51678840c633c224e043d26104eb684eb3e855. On the next push, the relevance gate compares only the commits since this SHA; delete this comment to force a full re-review.

@claude

claude Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Claude finished @cursor[bot]'s task in 2m 13s —— View job


Code Review

  • Gather context (diff + changed files)
  • Review eol-normalizer.sh changes for correctness / error handling
  • Review eol-normalizer.test.sh coverage
  • Review plugin.json / CHANGELOG.md
  • Post findings

Scope: plugins/eol-normalizer/hooks/eol-normalizer.sh, eol-normalizer.test.sh, normalize-eol.sh (read for context, unchanged by this PR), plugin.json, CHANGELOG.md at HEAD a216239. Security is covered by the dedicated security lane per REVIEW.md — not duplicated here.

I confirmed the current HEAD's eol-normalizer.sh/eol-normalizer.test.sh content is byte-identical to what earlier review rounds on this PR examined (the SHA changed but the diff didn't), so the two correctness issues flagged in those earlier rounds are still open at this SHA. I independently re-traced both against the current files before re-posting them inline, rather than assuming they still applied:

🔴 Test regression (posted inline on eol-normalizer.test.sh:228-234)

The "mutation disclosure: LF normalization names target ending" case reruns the hook on $REPO/tel2.sh, which the immediately-preceding stub-sink block already normalized to LF. That makes this second run a true idempotent no-op, so with the EFFECTIVE_ACTION/cmp fix in eol-normalizer.sh:105-113 it should resolve to skip and emit no systemMessage — but the assertion still expects one. This block predates the idempotency fix and wasn't updated alongside it.

🟡 Perf: idempotency snapshot runs unconditionally on every write (posted inline on eol-normalizer.sh:105-113)

The new mktemp/cp -p/cmp/rm sequence runs for every write this hook handles, including the guaranteed-skip paths (unspecified eol=, -text, sniffed binaries) where the file can never change. That's 4 extra subprocess spawns on what the hook's own comments call "the hottest path in the fleet," for cases that can never benefit from the check. Suggest moving the snapshot/compare inside normalize_eol_file's lf/crlf arms so the cost is confined to files actually eligible for normalization.

Other areas checked, no issues found

  • plugin.json (0.6.70.6.8) and the CHANGELOG.md [0.6.8] entry are consistent with each other and, with the idempotency fix in place, the "Skip and no-op paths stay silent" claim matches the code (modulo the test-coverage gap above).
  • The case "$EFFECTIVE_ACTION" default arm satisfies shellcheck SC2249 and is a harmless no-op.
  • emit_tel continues to key its status/data.action fields correctly off EFFECTIVE_ACTION/ACTION; no inconsistency found.
  • I could not execute git diff origin/main...HEAD or the test suite in this sandbox (Bash execution required interactive approval that wasn't available) — findings above are based on direct reading of the current file contents, and I verified the diff stat (+45/-6 across 4 files) matches the PR's reported totals, confirming I reviewed the right state. The PR's own "36 passed" test-plan entry is author-reported and not independently re-run by me; per the test regression above, it likely predates the idempotency fix or wasn't re-run against it.
    · branch cursor/eol-normalizer-mutation-disclosure-63e1

Comment thread plugins/eol-normalizer/hooks/eol-normalizer.test.sh
Comment thread plugins/eol-normalizer/hooks/eol-normalizer.sh
@kyle-sexton
kyle-sexton force-pushed the cursor/eol-normalizer-mutation-disclosure-63e1 branch from d87b45a to 8d0f005 Compare August 12, 2026 19:53
@claude

claude Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 1m 37s —— View job


Security Review

  • Gather context (diff at HEAD 8d0f005, changed files)
  • Check skip gate
  • Review eol-normalizer.sh changes (temp-file handling, injection, idempotency logic)
  • Review eol-normalizer.test.sh changes
  • Review plugin.json / CHANGELOG.md changes
  • Post findings

Scope: plugins/eol-normalizer/hooks/eol-normalizer.sh at HEAD 8d0f005, plus eol-normalizer.test.sh, plugin.json, CHANGELOG.md. hook-utils.sh and normalize-eol.sh are unchanged by this PR (verified via git diff origin/main...HEAD), so their behavior from prior rounds still applies as-is.

🟡 IMPORTANT — idempotency-check temp copy still lands in shared /tmp with the source file's permission bits, no cleanup if the hook is killed

Still present, unchanged, at eol-normalizer.sh:104-112:

_eol_tmp="$(mktemp "${TMPDIR:-/tmp}/eol-normalizer.XXXXXX")"
cp -p "$FILE" "$_eol_tmp"
ACTION=$(normalize_eol_file "$REPO_ROOT" "$FILE")
...
rm -f "$_eol_tmp"

This was flagged on multiple earlier revisions of this same commit and is still open at the current head:

  • mktemp creates _eol_tmp with the safe default mode 0600, but cp -p immediately widens that to $FILE's own mode. Most repo files are group/world-readable, so the copy sitting in ${TMPDIR:-/tmp} (drwxrwxrwt — every local user can list/enter it) inherits that readability, briefly exposing the full content of whatever file was just written (potentially a .env, checked-in credential, or private config) to any other local user on a shared host — a boundary the original file's own directory permissions may not grant.
  • The hook has a hard 15s kill timeout (hooks.json:10). If normalize_eol_file runs long and gets killed, execution never reaches rm -f "$_eol_tmp", so the copy is orphaned in /tmp with the source's permissions until OS/tmp-cleanup policy clears it.

The sibling library code in this same plugin already solves the equivalent problem correctly — normalize_eol_to_lf/normalize_eol_to_crlf in normalize-eol.sh:104,124 explicitly stage their temp file in the same directory as the target file (mktemp "${file}.XXXXXX"), citing CWE-377, rather than a shared global temp dir. This new snapshot in eol-normalizer.sh still doesn't follow that established pattern.

Suggested fix: stage _eol_tmp next to $FILE (same directory / same permission boundary, matching normalize-eol.sh's own pattern) instead of ${TMPDIR:-/tmp}, and add trap 'rm -f "$_eol_tmp"' EXIT so a killed hook doesn't leak the copy. If a same-directory temp file isn't desired, at minimum drop -p (or chmod 600 "$_eol_tmp" right after the copy) so the mode isn't widened past mktemp's safe default.

Fix this →

Other areas checked, no issues found

  • Injection via $(basename "$FILE") in the hook::emit_system_message calls (L128-136): $FILE is passed to basename as a quoted argument, not interpolated into a constructed command — no command-injection surface. The result flows through the unchanged hook::emit_system_message → hook::emit_channels → hook::json_escape pipeline, which escapes \, ", and control bytes before embedding in the JSON systemMessage field, so a crafted file path can't break out of the JSON payload or inject additional keys.
  • Prompt-injection via the agent-context channel. hook::emit_system_message composes with ctx="", so this only ever populates systemMessage (user-facing), never additionalContext (the agent-context channel) — a maliciously-named file can't inject content back into the agent's context this way.
  • EFFECTIVE_ACTION idempotency-compare logic itself (cmp -s): no new external input, no new privilege boundary — outside the temp-file handling above, it introduces no new security-relevant behavior.
  • plugin.json / CHANGELOG.md: version bump and documentation only, no config/permission changes.
  • eol-normalizer.test.sh: assertion updates only, no new execution paths or security-relevant behavior. (There is a separately-reported test-correctness issue at line ~229 reusing an already-normalized fixture — not a security concern, so out of scope for this lane.)
    · branch cursor/eol-normalizer-mutation-disclosure-63e1

@claude

claude Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 1m 23s —— View job


Security Review

  • Gather context (diff origin/main...HEAD, changed files)
  • Check skip gate
  • Review eol-normalizer.sh changes (temp-file handling, injection, idempotency logic)
  • Review eol-normalizer.test.sh changes
  • Review plugin.json / CHANGELOG.md changes
  • Post findings

Skipping — no new security-relevant content since the last full review.

I diffed 8d0f005 (the SHA the tracking comment records as last security-reviewed) against the current HEAD (1f428a4, a merge of main in). The four PR files (eol-normalizer.sh, eol-normalizer.test.sh, plugin.json, CHANGELOG.md) are byte-identical between the two — the merge only pulled in unrelated commits to other plugins (education, powershell-format, rate-limit-guard, session-flow, work-items), none of which are part of this PR's diff against origin/main. Skip-gate condition 3 applies (already-reviewed head, still current).

For the record, the one still-open item from the prior rounds remains unchanged:

  • 🟡 IMPORTANTeol-normalizer.sh:105-113: the idempotency-check temp copy is created via mktemp "${TMPDIR:-/tmp}/eol-normalizer.XXXXXX" (safe 0600) but then cp -p "$FILE" "$_eol_tmp" widens its mode to match the source file, so a world/group-readable source file produces a similarly-readable copy in the shared, world-listable ${TMPDIR:-/tmp}. The hook's 15s kill timeout (hooks.json:10) also means a killed run skips the rm -f "$_eol_tmp" cleanup, orphaning the copy. normalize-eol.sh already solves this correctly elsewhere by staging its temp file next to the target (mktemp "${file}.XXXXXX", normalize-eol.sh:104, citing CWE-377) — the new snapshot in eol-normalizer.sh doesn't follow that pattern. Not re-verifying in depth this round since the code hasn't changed since it was last confirmed open.

No new findings this round.
· branch cursor/eol-normalizer-mutation-disclosure-63e1

@claude

claude Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 2m 30s —— View job


Code Review

  • Gather context (diff/files at HEAD 1f428a4)
  • Review eol-normalizer.sh changes for correctness / error handling
  • Review eol-normalizer.test.sh coverage
  • Review plugin.json / CHANGELOG.md
  • Post findings

Scope: plugins/eol-normalizer/hooks/eol-normalizer.sh, eol-normalizer.test.sh, plugin.json, CHANGELOG.md, plus normalize-eol.sh (read for context, unchanged by this PR). Security is covered by the dedicated security lane per REVIEW.md — not duplicated here (e.g. the /tmp temp-file permission issue on eol-normalizer.sh:105-113 is that lane's finding, already posted separately).

HEAD 1f428a4 is a merge of main into this branch — no new commits touch the eol-normalizer files since 8d0f005 ("stay silent on idempotent normalization"). I re-traced the two open correctness/maintainability issues from earlier review rounds against the current file contents to confirm they're still live, then posted them as inline comments:

🔴 Test regression — eol-normalizer.test.sh:228-234

The "mutation disclosure: LF normalization names target ending" case reruns the hook on $REPO/tel2.sh, which the immediately-preceding stub-sink block already normalized to LF — so this second run is a true idempotent no-op. With the EFFECTIVE_ACTION/cmp fix in eol-normalizer.sh:105-113, that resolves to skip and emits no systemMessage, so the assertion expecting one should fail. Posted inline with a suggested fix (seed a fresh CRLF fixture instead of reusing the already-normalized file).

🟡 Efficiency — eol-normalizer.sh:105-113

The new snapshot/compare (mktemp + cp -p + cmp + rm) runs unconditionally on every write this hook handles, including the guaranteed-skip paths (unspecified eol= attr, -text, sniffed binaries) where the file can never change — 4 extra subprocess spawns on what the hook's own comments call "the hottest path in the fleet." Posted inline with a suggested fix (move the snapshot/compare inside normalize_eol_file's lf/crlf arms so the cost is confined to files actually eligible for normalization).

Other areas checked, no issues found

  • plugin.json (0.6.8) and the CHANGELOG.md [0.6.8] entry are consistent with each other, and — with the idempotency fix in place — the entry's "Skip and no-op paths stay silent" claim matches the code's intent (modulo the test-coverage gap above).
  • The case "$EFFECTIVE_ACTION" default arm satisfies shellcheck SC2249 and is a harmless no-op.
  • emit_tel continues to key its status/data.action fields correctly off EFFECTIVE_ACTION/ACTION; no inconsistency found.
  • I could not run git diff origin/main...HEAD or execute the test suite in this sandbox (the repo checkout is shallow with no merge-base to origin/main, and git fetch/bash <script> both require interactive approval that wasn't available) — findings above are based on direct reading of the current file contents, cross-checked line-by-line against the logic in normalize-eol.sh. The PR's own "36 passed" test-plan entry is author-reported and likely predates the idempotency fix or wasn't re-run against it, per the test regression above.
    · branch cursor/eol-normalizer-mutation-disclosure-63e1

Comment thread plugins/eol-normalizer/hooks/eol-normalizer.test.sh
Comment thread plugins/eol-normalizer/hooks/eol-normalizer.sh
@claude

claude Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Claude finished @cursor[bot]'s task in 1m 12s —— View job


Security Review

  • Gather context (diff origin/main...HEAD, changed files)
  • Check skip gate (compare against last-reviewed SHA)
  • Review eol-normalizer.sh changes (temp-file handling, injection, idempotency logic)
  • Review eol-normalizer.test.sh changes
  • Review plugin.json / CHANGELOG.md changes
  • Post findings

Skipping — no new security-relevant content since the last full review.

I diffed the last security-reviewed head (8d0f005, still current at 1f428a4) against the current HEAD (b3a406b, "merge: forward main into cursor/eol-normalizer-mutation-disclosure-63e1"). eol-normalizer.sh and eol-normalizer.test.sh — the two files with any executable logic in this PR — are byte-identical between those two SHAs. The merge only pulled in an unrelated hook-utils.sh sync (temp-file handling for emit_telemetry payloads, #1595) plus documentation-only CHANGELOG.md/plugin.json entries for that sync (0.6.9–0.6.11); hook-utils.sh isn't part of this PR's diff against origin/main (verified via git diff origin/main...HEAD --stat, which shows only the same 4 files / +45/-6 the PR has reported throughout). Skip-gate condition applies (already-reviewed logic, still current).

For the record, the one still-open item from prior rounds remains unchanged at eol-normalizer.sh:105-113:

  • 🟡 IMPORTANT — the idempotency-check temp copy is created via mktemp "${TMPDIR:-/tmp}/eol-normalizer.XXXXXX" (safe 0600) but cp -p "$FILE" "$_eol_tmp" immediately widens its mode to match the source file, so a world/group-readable source file produces a similarly-readable copy in the shared, world-listable ${TMPDIR:-/tmp}. The hook's 15s kill timeout (hooks.json:10) also means a killed run skips the rm -f "$_eol_tmp" cleanup, orphaning the copy. normalize-eol.sh already solves this correctly elsewhere by staging its temp file next to the target (mktemp "${file}.XXXXXX", normalize-eol.sh:104, citing CWE-377) — this snapshot doesn't follow that pattern. Not re-verifying in depth this round since the code hasn't changed since it was last confirmed open.

No new findings this round.
· branch cursor/eol-normalizer-mutation-disclosure-63e1

@claude

claude Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Claude finished @cursor[bot]'s task in 1m 54s —— View job


Code Review (/review:code-review)

  • Check skip gate
  • Confirm reviewed-file state at current HEAD vs. last lane review
  • Post findings

Skipping — this head already has a successful review from this lane that still applies (skip gate condition 4).

HEAD is b3a406b (merge of main forward). I diffed 1f428a4 — the SHA this lane already reviewed at 2026-08-12T20:26:29Z, posting two inline findings — against current HEAD for the four PR files. Only plugins/eol-normalizer/.claude-plugin/plugin.json (version bump 0.6.80.6.11, carried in by the merge) and plugins/eol-normalizer/CHANGELOG.md (unrelated 0.6.9/0.6.10 entries from other synced changes) differ. The two files with actual review surface — plugins/eol-normalizer/hooks/eol-normalizer.sh and plugins/eol-normalizer/hooks/eol-normalizer.test.sh — are byte-identical to what was already reviewed at 1f428a4.

Both findings from that prior round remain open at the current head and were already posted as inline comments on this PR (not re-posted here to avoid duplicates):

  • 🔴 eol-normalizer.test.sh:234 — the "mutation disclosure: LF normalization names target ending" case reruns the hook on $REPO/tel2.sh, which the preceding stub-sink block already normalized to LF. That makes this a true idempotent no-op, so with the EFFECTIVE_ACTION/cmp fix in eol-normalizer.sh:105-113 it now resolves to skip and should emit no systemMessage — but the assertion at line 230 still expects one. Fix: seed a fresh CRLF fixture (e.g. tel2b.sh) instead of reusing the already-normalized tel2.sh.
  • 🟡 eol-normalizer.sh:105-113 — the new mktemp/cp -p/cmp/rm snapshot runs unconditionally on every write this hook handles, including the guaranteed-skip paths (unspecified eol= attr, -text, sniffed binaries) where the file can never change — 4 extra subprocess spawns on what the hook's own comments call "the hottest path in the fleet." Suggest moving the snapshot/compare inside normalize_eol_file's lf/crlf arms so the cost is confined to files actually eligible for normalization.

Per REVIEW.md, security-relevant findings for this PR (the /tmp temp-file permission issue on the same eol-normalizer.sh:105-113 block) are scoped to the dedicated claude-security-review lane and already tracked there — not duplicated here.

No new correctness/maintainability findings this round.
· branch cursor/eol-normalizer-mutation-disclosure-63e1

…el (#1256)

Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
@cursor
cursor Bot force-pushed the cursor/eol-normalizer-mutation-disclosure-63e1 branch from b3a406b to 0f9ed1c Compare August 12, 2026 22:33
Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
@claude

claude Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Claude finished @cursor[bot]'s task in 1m 17s —— View job


Security Review

  • Gather context (diff origin/main...HEAD, changed files)
  • Check skip gate (compare against last-reviewed SHA)
  • Review eol-normalizer.sh changes (temp-file handling, injection, idempotency logic)
  • Review eol-normalizer.test.sh changes
  • Review plugin.json / CHANGELOG.md changes
  • Post findings

Skipping — no new security-relevant content since the last full review (skip gate condition 3).

I diffed the last security-reviewed head (b3a406b) against the current HEAD (8c6efe3). plugins/eol-normalizer/hooks/eol-normalizer.sh and plugins/eol-normalizer/hooks/eol-normalizer.test.sh — the only files in this PR with executable logic — are byte-identical between the two. The only change since then is the top commit 8c6efe3 ("align CHANGELOG version with plugin.json"), which rewords the [0.6.11] entry in plugins/eol-normalizer/CHANGELOG.md (documentation only, no behavior change):

- **Content-mutation disclosure on the user channel (#1596).** When the hook normalizes line
-  endings to LF or CRLF, it now emits a `systemMessage` naming the file and the target ending.
-  Skip and no-op paths stay silent.
+ **Line-ending rewrites are disclosed on the user channel** when the hook normalizes
+  CRLF/LF mismatches, so agents see the mutation instead of silently rewriting (#1256).

plugin.json is unchanged since b3a406b. No new security-relevant surface.

For the record, the one still-open item from prior rounds remains unchanged at eol-normalizer.sh:105-113:

  • 🟡 IMPORTANT — the idempotency-check temp copy is created via mktemp "${TMPDIR:-/tmp}/eol-normalizer.XXXXXX" (safe 0600) but cp -p "$FILE" "$_eol_tmp" immediately widens its mode to match the source file, so a world/group-readable source file produces a similarly-readable copy in the shared, world-listable ${TMPDIR:-/tmp}. The hook's 15s kill timeout (hooks.json:10) also means a killed run skips the rm -f "$_eol_tmp" cleanup, orphaning the copy. normalize-eol.sh already solves this correctly elsewhere by staging its temp file next to the target (mktemp "${file}.XXXXXX", normalize-eol.sh:104, citing CWE-377) — this snapshot doesn't follow that pattern. Not re-verifying in depth this round since the code hasn't changed since it was last confirmed open.

No new findings this round.
· branch cursor/eol-normalizer-mutation-disclosure-63e1

tel2.sh is normalized in the telemetry stub-sink case; re-running the hook
on it is a no-op and correctly omits disclosure. Use tel2b.sh instead.

Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
@claude

claude Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Claude finished @cursor[bot]'s task in 56s —— View job


Security Review

  • Gather context (diff since last-reviewed head, changed files)
  • Check skip gate (compare against last-reviewed SHA)
  • Review eol-normalizer.sh changes (temp-file handling, injection, idempotency logic)
  • Review eol-normalizer.test.sh changes
  • Review plugin.json / CHANGELOG.md changes
  • Post findings

Skipping — no new security-relevant content since the last full review (skip gate condition 2/3).

I diffed the last security-reviewed head (8c6efe3) against the current HEAD (0d51678). Only one file changed:

plugins/eol-normalizer/hooks/eol-normalizer.test.sh | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
- OUT_LF=$(run_hook_env "$REPO/tel2.sh" CLAUDE_PLUGIN_OPTION_EOL_NORMALIZER_ENABLED=true)
+ # Fresh file: tel2.sh was already normalized in the telemetry stub-sink case above.
+ printf 'echo r\r\n' >"$REPO/tel2b.sh"
+ OUT_LF=$(run_hook_env "$REPO/tel2b.sh" CLAUDE_PLUGIN_OPTION_EOL_NORMALIZER_ENABLED=true)

This is a test-only fixture fix (seeds a fresh CRLF file, tel2b.sh, instead of reusing the already-normalized tel2.sh), resolving the test-regression finding raised by prior code-review rounds on this PR. It introduces no new execution path, no new external input, and no security-relevant behavior — plugins/eol-normalizer/hooks/eol-normalizer.sh, plugin.json, and CHANGELOG.md are byte-identical to what was already reviewed at 8c6efe3.

For the record, the one still-open item from prior rounds remains unchanged at eol-normalizer.sh:105-113:

  • 🟡 IMPORTANT — the idempotency-check temp copy is created via mktemp "${TMPDIR:-/tmp}/eol-normalizer.XXXXXX" (safe 0600) but cp -p "$FILE" "$_eol_tmp" immediately widens its mode to match the source file, so a world/group-readable source file produces a similarly-readable copy in the shared, world-listable ${TMPDIR:-/tmp}. The hook's 15s kill timeout (hooks.json:10) also means a killed run skips the rm -f "$_eol_tmp" cleanup, orphaning the copy. normalize-eol.sh already solves this correctly elsewhere by staging its temp file next to the target (mktemp "${file}.XXXXXX", normalize-eol.sh:104, citing CWE-377) — this snapshot doesn't follow that pattern. Not re-verifying in depth this round since the code hasn't changed since it was last confirmed open.

No new findings this round.
· branch cursor/eol-normalizer-mutation-disclosure-63e1

@kyle-sexton
kyle-sexton merged commit 3c9351d into main Aug 12, 2026
38 checks passed
@kyle-sexton
kyle-sexton deleted the cursor/eol-normalizer-mutation-disclosure-63e1 branch August 12, 2026 22:56
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.

2 participants