Skip to content

feat(bash-format): disclose shfmt rewrites on the user channel - #2471

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

feat(bash-format): disclose shfmt rewrites on the user channel#2471
kyle-sexton merged 3 commits into
mainfrom
cursor/bash-format-mutation-disclosure-63e1

Conversation

@kyle-sexton

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

Copy link
Copy Markdown
Contributor

No linked issue

Partially addresses #1596

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 3m 54s —— View job


Security Review

  • Gather context (PR diff, changed files)
  • Review plugins/bash-format/hooks/bash-format.sh
  • Review plugins/bash-format/.claude-plugin/plugin.json
  • Post findings

Scope: only the two files changed in this PR (plugin.json version bump; the bash-format.sh diff adding a mktemp-based before/after snapshot around the shfmt write, and a new hook::emit_system_message notice when the file changes).

No injection, auth/access-control, secret-handling, or supply-chain issues found. All new variables ($_fmt_target, $_fmt_before, $FILE) are quoted throughout and only passed as arguments to cp/cmp/rm/basename — no eval, no unquoted expansion, no new externally-controlled input. plugin.json is a version-only change.

One logic-flaw finding with a security-relevant consequence (loss of visibility into ShellCheck's security-relevant lint output):


IMPORTANT — new code can cause two JSON documents to be printed to stdout in one run, silently dropping the hook's ShellCheck output
Confidence: Medium (verified by static read-through of the control flow and the file's own documented invariant; I could not execute the hook end-to-end in this sandbox — shfmt isn't installed here and script execution required approval I couldn't obtain — so I can't confirm exactly how the Claude Code hook-runner reacts to the malformed stdout, only that the contract is violated).

plugins/bash-format/hooks/bash-format.sh#L236-L241 adds:

if [[ -n "$_fmt_before" ]]; then
  if ! cmp -s "$_fmt_before" "$_fmt_target" 2>/dev/null; then
    hook::emit_system_message "bash-format: reformatted $(basename "$FILE") via shfmt (structural layout only)."
  fi
  rm -f "$_fmt_before"
fi

hook::emit_system_message prints a complete JSON document to stdout immediately (hook-utils.sh#L105-L107). But the script doesn't exit after this — it falls through to the ShellCheck lint pass and then the single end-of-run emission point at bash-format.sh#L289-L299, which unconditionally calls hook::emit_channels again whenever ShellCheck findings ($CTX) or a missing-tool notice ($NOTICE) exist.

That end-of-run call is documented as the file's single emission point ("composed into one JSON document emitted at the end", L289-290), and hook::emit_channels itself is documented as relying on the guarantee that "CC parses the hook's whole stdout as a single JSON doc" (hook-utils.sh#L77-L82), and hook::emit_system_message's own doc comment says "the caller must exit 0 right after unless it composes via hook::emit_channels itself" (hook-utils.sh#L96-L98).

The new code does neither: it doesn't exit, and it doesn't compose the reformat notice into NOTICE/CTX for the single end-of-run emission. So whenever shfmt actually changes a file and ShellCheck also has findings (or a missing-tool notice is pending) on that same run — a common combination, since both passes run on every matching edit — the hook prints two separate top-level JSON objects on two lines instead of one. That's not valid input for a parser expecting one JSON document, and by the file's own stated contract this is the exact failure mode the single-emission-point design exists to prevent.

Why this matters for security review scope (not just correctness): the practical effect is that ShellCheck's findings — which are the mechanism by which this hook surfaces security-relevant shell issues (unquoted expansions/SC2086, unsafe eval, word-splitting bugs, etc.) to the agent/user — can be silently dropped from the agent-visible channel exactly in the run where a file was also reformatted. A hook whose job is partly to flag security-relevant shell mistakes losing its own output undermines that guarantee without any visible error.

Suggested fix: don't call hook::emit_system_message mid-script. Instead capture the reformat outcome in a variable and fold it into NOTICE/CTX before the existing single hook::emit_channels call at the end of the script, preserving the "one JSON document per run" invariant.

Fix this →


Per instructions, unpinned-action / supply-chain / trigger / permissions concerns are left to zizmor's static lane — not applicable here anyway, since this PR touches no workflow files.

Branch: cursor/bash-format-mutation-disclosure-63e1

@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: 23ed248392

ℹ️ 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/bash-format/hooks/bash-format.sh
Comment thread plugins/bash-format/hooks/bash-format.sh
@kyle-sexton
kyle-sexton force-pushed the cursor/bash-format-mutation-disclosure-63e1 branch from d93c220 to d74f9a5 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 32s —— View job


Security Review

  • Gather context (PR diff, changed files)
  • Review plugins/bash-format/hooks/bash-format.sh
  • Review plugins/bash-format/.claude-plugin/plugin.json
  • Post findings

Scope: the two files changed in this PR — plugin.json (version-only bump, no security relevance) and the bash-format.sh diff, which adds a mktemp-based before/after snapshot around the shfmt write and a new hook::emit_system_message call when the file changes.

No injection, authorization/access-control, secret-handling, or supply-chain issues in this diff. The new _fmt_target/_fmt_before/FILE variables are quoted throughout and only passed to cp/cmp/rm/basename — no eval, no unquoted expansion, no new externally-controlled input, and _fmt_before is a mktemp-created file owned by the same user (no cross-trust-boundary exposure).

One logic flaw with a security-relevant consequence:


IMPORTANT — the new code can make the hook print two top-level JSON documents to stdout in one run, which silently drops ShellCheck's (security-relevant) findings
Severity: IMPORTANT · Confidence: High (fully verifiable by static read-through of the control flow and both files' own documented contracts — no execution needed, since the double-emission is deterministic given the code paths, not input-dependent).

plugins/bash-format/hooks/bash-format.sh#L236-L241 adds:

if [[ -n "$_fmt_before" ]]; then
  if ! cmp -s "$_fmt_before" "$_fmt_target" 2>/dev/null; then
    hook::emit_system_message "bash-format: reformatted $(basename "$FILE") via shfmt (structural layout only)."
  fi
  rm -f "$_fmt_before"
fi

hook::emit_system_message prints a complete JSON document to stdout immediately (hook-utils.sh#L105-L107, which is hook::emit_channels "" "" "$1"). The script does not exit after this call — it falls through to the ShellCheck lint pass and then reaches the single end-of-run emission point at bash-format.sh#L289-L299, which unconditionally calls hook::emit_channels again whenever ShellCheck findings ($CTX) or a missing-tool notice ($NOTICE) are non-empty.

That end-of-run call is documented as the file's single emission point ("composed into one JSON document emitted at the end", L289-290), hook::emit_channels itself documents that "CC parses the hook's whole stdout as a single JSON doc" (hook-utils.sh#L77-L82), and hook::emit_system_message's own doc comment states "the caller must exit 0 right after unless it composes via hook::emit_channels itself" (hook-utils.sh#L96-L98). The new code does neither: it doesn't exit, and it doesn't fold the reformat notice into NOTICE/CTX for the single end-of-run emission.

So whenever shfmt actually changes a file and ShellCheck also has findings (or a missing-tool notice is pending) on that same run — a common combination, since both passes run on every matching edit — the hook prints two separate top-level JSON objects on stdout instead of one. That is not valid input for a parser expecting a single JSON document, and per the file's own stated invariant this is exactly the failure mode the single-emission design exists to prevent.

Why this is security-relevant, not just a correctness bug: ShellCheck's findings are how this hook surfaces security-relevant shell issues (unquoted expansions / SC2086, unsafe eval, word-splitting bugs, etc.) to the agent. A malformed multi-document stdout means the agent-visible channel can silently lose that lint output exactly in the run where a file was also reformatted — undermining the hook's own security-relevant guarantee with no visible error to either the agent or the user.

Suggested fix: don't call hook::emit_system_message mid-script. Capture the reformat outcome in a variable and fold it into NOTICE/CTX before the existing single hook::emit_channels call at the end of the script, preserving the "one JSON document per run" invariant.

Fix this →


Per instructions, unpinned-action / supply-chain / trigger / permissions concerns are left to zizmor's static lane — not applicable here anyway, since this PR touches no workflow files.

Branch: cursor/bash-format-mutation-disclosure-63e1

@kyle-sexton
kyle-sexton force-pushed the cursor/bash-format-mutation-disclosure-63e1 branch from d74f9a5 to 9f5c718 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 1m 19s —— View job


Security Review

  • Gather context (PR diff, changed files)
  • Review plugins/bash-format/hooks/bash-format.sh
  • Review plugins/bash-format/.claude-plugin/plugin.json
  • Post findings

Scope: the two files changed in this PR — plugin.json (version-only bump, no security relevance) and the bash-format.sh diff, which adds a mktemp-based before/after snapshot around the shfmt write and a new hook::emit_system_message call when the file is actually rewritten.

No injection, authorization/access-control, secret-handling, or supply-chain issues in this diff. The new _fmt_before/_fmt_target/FILE variables are quoted throughout and only ever passed to cp/cmp/rm/basename — no eval, no unquoted expansion, no new externally-controlled input. _fmt_before is a mktemp-created file (default 600 perms) owned by the invoking user, so there's no cross-trust-boundary exposure of the snapshot.

One logic flaw with a security-relevant consequence, confirmed by reading the full control flow of both files at HEAD:


IMPORTANT — the hook can print two top-level JSON documents to stdout in one run, silently dropping ShellCheck's (security-relevant) findings
Confidence: High — deterministic given the code paths (not input-dependent), fully verifiable by static read-through; no execution needed.

bash-format.sh#L236-L241 adds:

if [[ -n "$_fmt_before" ]]; then
  if ! cmp -s "$_fmt_before" "$_fmt_target" 2>/dev/null; then
    hook::emit_system_message "bash-format: reformatted $(basename "$FILE") via shfmt (structural layout only)."
  fi
  rm -f "$_fmt_before"
fi

hook::emit_system_message is hook::emit_channels "" "" "$1" (hook-utils.sh#L105-L107) and prints a complete JSON document to stdout immediately. The script does not exit after this call — it falls through to the ShellCheck lint pass and then reaches the single end-of-run emission point at bash-format.sh#L289-L299, which unconditionally calls hook::emit_channels again whenever $CTX (ShellCheck findings) or $NOTICE (missing-tool notice) is non-empty:

CTX="${CTX%$'\n'}"
if [[ -n "$NOTICE" ]]; then
  AGENT_CTX="$CTX"
  [[ -n "$AGENT_CTX" ]] && AGENT_CTX+=$'\n'
  AGENT_CTX+="$NOTICE"
  hook::emit_channels PostToolUse "$AGENT_CTX" "$NOTICE"
else
  hook::emit_channels PostToolUse "$CTX" ""
fi

hook::emit_channels returns early only when both its ctx and sysmsg args are empty (hook-utils.sh#L83-L94), so this second call is a no-op only when there are no ShellCheck findings and no missing-tool notice on that run. hook::emit_channels is itself documented as relying on the guarantee that "CC parses the hook's whole stdout as a single JSON doc" (hook-utils.sh#L77-L82), and hook::emit_system_message's own doc comment states "the caller must exit 0 right after unless it composes via hook::emit_channels itself" (hook-utils.sh#L96-L98). The new code does neither: it doesn't exit, and it doesn't fold the reformat notice into NOTICE/CTX before the final emission.

So whenever shfmt actually rewrites a file and ShellCheck also has findings (or a missing-tool notice is pending) on that same run — a routine combination, since both passes run on every matching edit — the hook writes two separate top-level JSON objects to stdout instead of one, violating the file's own documented single-emission invariant (L289-290: "composed into one JSON document emitted at the end").

Why this is security-relevant, not just correctness: ShellCheck's findings are this hook's mechanism for surfacing security-relevant shell mistakes (unquoted expansions / SC2086, unsafe eval, word-splitting bugs, etc.) to the agent. A malformed multi-document stdout means the agent-visible channel can silently lose that lint output — exactly in the run where a file was also reformatted — with no visible error to the agent or the user.

Suggested fix: don't call hook::emit_system_message mid-script. Capture the reformat outcome in a variable and fold it into NOTICE/CTX before the existing single hook::emit_channels call at the end of the script (L292-299), preserving the "one JSON document per run" invariant.

Fix this →


This matches the finding already raised by the two prior review runs on this PR — re-verified here against the current HEAD (9f5c718) and it still applies; nothing in this push addressed it.

Per instructions, unpinned-action / supply-chain / trigger / permissions concerns are left to zizmor's static lane — not applicable here anyway, since this PR touches no workflow files.

Branch: cursor/bash-format-mutation-disclosure-63e1

@kyle-sexton
kyle-sexton force-pushed the cursor/bash-format-mutation-disclosure-63e1 branch from 9f5c718 to 6070223 Compare August 12, 2026 17:13
@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 (PR diff, changed files)
  • Review plugins/bash-format/hooks/bash-format.sh
  • Review plugins/bash-format/.claude-plugin/plugin.json
  • Post findings

Scope: the two files changed in this PR — plugin.json (version-only bump, 0.7.70.7.8, no security relevance) and the bash-format.sh diff, which adds a mktemp-based before/after snapshot around the shfmt write and a new hook::emit_system_message call when the file is actually rewritten. Verified directly against current HEAD 6070223.

No injection, authorization/access-control, secret-handling, or supply-chain issues in this diff. The new _fmt_before/_fmt_target/FILE variables are quoted throughout and only ever passed to cp/cmp/rm/basename — no eval, no unquoted expansion, no new externally-controlled input. _fmt_before is a mktemp-created temp file owned by the invoking user, so there's no cross-trust-boundary exposure of the snapshot.

One logic flaw with a security-relevant consequence:


IMPORTANT — the hook can print two top-level JSON documents to stdout in one run, silently dropping ShellCheck's (security-relevant) findings
Confidence: High — deterministic given the code paths (not input-dependent), fully verifiable by static read-through of both files; no execution needed.

bash-format.sh#L236-L241 adds:

if [[ -n "$_fmt_before" ]]; then
  if ! cmp -s "$_fmt_before" "$_fmt_target" 2>/dev/null; then
    hook::emit_system_message "bash-format: reformatted $(basename "$FILE") via shfmt (structural layout only)."
  fi
  rm -f "$_fmt_before"
fi

hook::emit_system_message is hook::emit_channels "" "" "$1" (hook-utils.sh#L105-L107) and prints a complete JSON document to stdout immediately. The script does not exit after this call — it falls through to the ShellCheck lint pass and then reaches the single end-of-run emission point at bash-format.sh#L289-L299, which unconditionally calls hook::emit_channels again whenever $CTX (ShellCheck findings) or $NOTICE (missing-tool notice) is non-empty:

CTX="${CTX%$'\n'}"
if [[ -n "$NOTICE" ]]; then
  AGENT_CTX="$CTX"
  [[ -n "$AGENT_CTX" ]] && AGENT_CTX+=$'\n'
  AGENT_CTX+="$NOTICE"
  hook::emit_channels PostToolUse "$AGENT_CTX" "$NOTICE"
else
  hook::emit_channels PostToolUse "$CTX" ""
fi

hook::emit_channels returns early only when both its ctx and sysmsg args are empty (hook-utils.sh#L83-L94), so the second call is a no-op only when there are no ShellCheck findings and no missing-tool notice on that run. hook::emit_channels is itself documented as relying on the guarantee that "CC parses the hook's whole stdout as a single JSON doc" (hook-utils.sh#L77-L82), and hook::emit_system_message's own doc comment states "the caller must exit 0 right after unless it composes via hook::emit_channels itself" (hook-utils.sh#L96-L98). The new code does neither: it doesn't exit, and it doesn't fold the reformat notice into NOTICE/CTX before the final emission.

So whenever shfmt actually rewrites a file and ShellCheck also has findings (or a missing-tool notice is pending) on that same run — a routine combination, since both passes run on every matching edit — the hook writes two separate top-level JSON objects to stdout instead of one, violating the file's own documented single-emission invariant (L289-290: "composed into one JSON document emitted at the end").

Why this is security-relevant, not just correctness: ShellCheck's findings are this hook's mechanism for surfacing security-relevant shell mistakes (unquoted expansions / SC2086, unsafe eval, word-splitting bugs, etc.) to the agent. A malformed multi-document stdout means the agent-visible channel can silently lose that lint output — exactly in the run where a file was also reformatted — with no visible error to the agent or the user.

Suggested fix: don't call hook::emit_system_message mid-script. Capture the reformat outcome in a variable and fold it into NOTICE/CTX before the existing single hook::emit_channels call at the end of the script (L292-299), preserving the "one JSON document per run" invariant.

Fix this →


This is the same finding raised by the three prior review runs on this PR (23ed248, d74f9a5, 9f5c718) — re-verified here against current HEAD 6070223 and it still applies; nothing in the latest push (the plugin.json version bump) addressed it.

Per instructions, unpinned-action / supply-chain / trigger / permissions concerns are left to zizmor's static lane — not applicable here anyway, since this PR touches no workflow files.

Branch: cursor/bash-format-mutation-disclosure-63e1

cursor Bot pushed a commit that referenced this pull request Aug 12, 2026
Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
cursoragent and others added 2 commits August 12, 2026 18:54
Partially addresses #1596

Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
@cursor
cursor Bot force-pushed the cursor/bash-format-mutation-disclosure-63e1 branch from c56786d to 28b02ea 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 16s —— View job


Security Review

  • Gather context (PR diff, changed files)
  • Review plugins/bash-format/hooks/bash-format.sh
  • Review plugins/bash-format/.claude-plugin/plugin.json and CHANGELOG.md
  • Post findings

Scope: the three files changed in this PR, verified against current HEAD 28b02ea. The latest push (28b02ea) only bumps plugin.json (0.7.80.7.9) and adds a CHANGELOG.md entry — no security relevance, and it does not touch bash-format.sh. The security-relevant diff is unchanged from the prior four review runs on this PR: a mktemp-based before/after snapshot around the shfmt write, plus a new hook::emit_system_message call when the file is actually rewritten.

No injection, authorization/access-control, secret-handling, or supply-chain issues in this diff. The _fmt_before/_fmt_target/FILE variables are quoted throughout and only ever passed to cp/cmp/rm/basename — no eval, no unquoted expansion, no new externally-controlled input. _fmt_before is a mktemp-created temp file owned by the invoking user, so there's no cross-trust-boundary exposure.

One logic flaw with a security-relevant consequence, still present at HEAD 28b02ea — unchanged since it was first flagged on 23ed248:


IMPORTANT — the hook can print two top-level JSON documents to stdout in one run, silently dropping ShellCheck's (security-relevant) findings
Confidence: High — deterministic given the code paths (not input-dependent), fully verifiable by static read-through of both files; no execution needed.

bash-format.sh#L236-L241:

if [[ -n "$_fmt_before" ]]; then
  if ! cmp -s "$_fmt_before" "$_fmt_target" 2>/dev/null; then
    hook::emit_system_message "bash-format: reformatted $(basename "$FILE") via shfmt (structural layout only)."
  fi
  rm -f "$_fmt_before"
fi

hook::emit_system_message is hook::emit_channels "" "" "$1" (hook-utils.sh#L105-L107) and prints a complete JSON document to stdout immediately. The script does not exit after this call — it falls through to the ShellCheck lint pass and then reaches the single end-of-run emission point at bash-format.sh#L289-L299, which unconditionally calls hook::emit_channels again whenever $CTX (ShellCheck findings) or $NOTICE (missing-tool notice) is non-empty:

CTX="${CTX%$'\n'}"
if [[ -n "$NOTICE" ]]; then
  AGENT_CTX="$CTX"
  [[ -n "$AGENT_CTX" ]] && AGENT_CTX+=$'\n'
  AGENT_CTX+="$NOTICE"
  hook::emit_channels PostToolUse "$AGENT_CTX" "$NOTICE"
else
  hook::emit_channels PostToolUse "$CTX" ""
fi

hook::emit_channels returns early only when both its ctx and sysmsg args are empty (hook-utils.sh#L83-L94), so the second call is a no-op only when there are no ShellCheck findings and no missing-tool notice on that run. hook::emit_channels is itself documented as relying on the guarantee that "CC parses the hook's whole stdout as a single JSON doc" (hook-utils.sh#L77-L82), and hook::emit_system_message's own doc comment states "the caller must exit 0 right after unless it composes via hook::emit_channels itself" (hook-utils.sh#L96-L98). The new code does neither: it doesn't exit, and it doesn't fold the reformat notice into NOTICE/CTX before the final emission.

So whenever shfmt actually rewrites a file and ShellCheck also has findings (or a missing-tool notice is pending) on that same run — a routine combination, since both passes run on every matching edit — the hook writes two separate top-level JSON objects to stdout instead of one, violating the file's own documented single-emission invariant (L289-290: "composed into one JSON document emitted at the end").

Why this is security-relevant, not just correctness: ShellCheck's findings are this hook's mechanism for surfacing security-relevant shell mistakes (unquoted expansions / SC2086, unsafe eval, word-splitting bugs, etc.) to the agent. A malformed multi-document stdout means the agent-visible channel can silently lose that lint output — exactly in the run where a file was also reformatted — with no visible error to the agent or the user.

Suggested fix: don't call hook::emit_system_message mid-script. Capture the reformat outcome in a variable and fold it into NOTICE/CTX before the existing single hook::emit_channels call at the end of the script (L292-299), preserving the "one JSON document per run" invariant.

Fix this →


This is the same finding raised by all four prior review runs on this PR (23ed248, d74f9a5, 9f5c718, 6070223) — re-verified here against current HEAD 28b02ea. The intervening push only bumped the plugin version and changelog; bash-format.sh is byte-for-byte unchanged, so the finding still applies without modification.

Per instructions, unpinned-action / supply-chain / trigger / permissions concerns are left to zizmor's static lane — not applicable here anyway, since this PR touches no workflow files.

Branch: cursor/bash-format-mutation-disclosure-63e1

@github-actions

Copy link
Copy Markdown
Contributor

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

Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
@kyle-sexton
kyle-sexton merged commit f5b8a3d into main Aug 12, 2026
38 checks passed
@kyle-sexton
kyle-sexton deleted the cursor/bash-format-mutation-disclosure-63e1 branch August 12, 2026 19:10
@claude

claude Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Claude finished @cursor[bot]'s task in 4m 14s —— View job


Code Review

  • Gather context (PR diff, changed files, CLAUDE.md/REVIEW.md)
  • Review plugins/bash-format/hooks/bash-format.sh diff
  • Review plugins/bash-format/.claude-plugin/plugin.json and CHANGELOG.md
  • Post findings

This is the code-review lane (correctness/design/conventions/error-handling/observability/tests/docs). Per REVIEW.md, this repo also runs a dedicated security lane, and the double-JSON-document / hook::emit_channels stdout-contract issue on this same diff has already been reported there in detail across five prior review runs on this PR — that finding is intentionally not duplicated here.

Scope: the three changed files, verified against HEAD 883f6b2e. plugin.json is a version-only bump (no review surface); CHANGELOG.md addition matches the code change. The reviewable surface is the bash-format.sh diff: a mktemp-based before/after snapshot around the shfmt write, and a new hook::emit_system_message call when the write actually changes the file.

Two findings posted as inline review comments:

  1. [Nit] bash-format.sh#L224-L227 — if mktemp succeeds but the following cp fails, _fmt_before is cleared to "" so the later rm -f "$_fmt_before" never fires, leaking the temp file mktemp created. Low impact (narrow failure window, empty stray temp file), easy fix: capture the mktemp path separately and rm -f it explicitly on cp failure.
  2. [Important] bash-format.sh#L236-L241 — this PR adds a new hook::emit_channels call path with no corresponding test. bash-format.test.sh already tests the "single JSON document" invariant for an analogous combination (shfmt-absent-notice + ShellCheck findings, lines 569-606), but nothing exercises shfmt actually reformatting a file together with ShellCheck findings — exactly the combination that exercises the new code path. Suggested a test modeled on the existing "mix" case.

Nothing else in the diff raised correctness, design, or documentation concerns — the .editorconfig/opt-out framing Codex's P2 comment raised is consistent with this file's existing convention of using basename "$FILE" in user-facing notices (see the ShellCheck-findings message at line 273), so it isn't a new gap introduced by this PR.

[[ -f "$_fmt_target" ]] || _fmt_target="$FILE"
_fmt_before=""
if _fmt_before=$(mktemp 2>/dev/null); then
cp "$_fmt_target" "$_fmt_before" 2>/dev/null || _fmt_before=""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Nit: leftover temp file when cp fails after a successful mktemp

_fmt_before=""
if _fmt_before=$(mktemp 2>/dev/null); then
  cp "$_fmt_target" "$_fmt_before" 2>/dev/null || _fmt_before=""
fi

mktemp creates the temp file on disk and returns its path; the if only guards on mktemp's own exit status. If the subsequent cp then fails (permission hiccup, disk full, or $_fmt_target disappearing in the same race window this file already guards against elsewhere — see the "Re-check existence immediately before mutating" comment a few lines below), _fmt_before is reset to "" so the later rm -f "$_fmt_before" (line 240) never runs. The file mktemp created is orphaned — it's never cleaned up.

Low impact (an empty stray file in the OS temp dir on a narrow failure path), but easy to close: capture the path before clearing the variable, e.g.

_fmt_before=""
if _fmt_before_tmp=$(mktemp 2>/dev/null); then
  if cp "$_fmt_target" "$_fmt_before_tmp" 2>/dev/null; then
    _fmt_before="$_fmt_before_tmp"
  else
    rm -f "$_fmt_before_tmp"
  fi
fi

Fix this →

Comment on lines +236 to +241
if [[ -n "$_fmt_before" ]]; then
if ! cmp -s "$_fmt_before" "$_fmt_target" 2>/dev/null; then
hook::emit_system_message "bash-format: reformatted $(basename "$FILE") via shfmt (structural layout only)."
fi
rm -f "$_fmt_before"
fi

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 Important: new mid-script hook::emit_channels call site has no test coverage

This adds a second call path to hook::emit_channels (via hook::emit_system_message) that fires mid-script, before the file's existing single end-of-run emission at lines 289-299. bash-format.test.sh already has a dedicated test for the "single JSON document" invariant on an analogous combination — the shfmt-absent-notice + ShellCheck-findings case (bash-format.test.sh:569-606, asserting jq -s 'length' == 1 on stdout) — which shows the project treats that invariant as something worth a regression test whenever a new notice source is added.

Nothing in this PR adds the equivalent test for the new path: a run where shfmt actually reformats the file (so this new mid-script emission fires) and ShellCheck also produces findings (or the missing-tool notice is pending) on the same run. That's exactly the combination that would need to hold this invariant, and it currently has no automated check — a regression here (e.g. two top-level JSON objects on stdout instead of one) would ship silently.

Suggested addition: a case modeled on the existing "mix" test at bash-format.test.sh:569-606, but with shfmt present and actually rewriting the fixture file (rather than absent) plus a ShellCheck finding present, asserting jq -s 'length' on stdout is still 1.

Fix this →

@github-actions

Copy link
Copy Markdown
Contributor

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

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