Summary
trunk check applies shellcheck's suggested fixes (e.g. SC2250 "prefer braces around variable references") using byte offsets, while shellcheck reports character-based line/column coordinates. On any line containing a multibyte UTF-8 character before a fix site, the second and subsequent edits land at the wrong byte position and rewrite the script into invalid UTF-8.
Compounding this, when stdin is not a TTY (e.g. inside a git pre-commit hook) the Apply autofix (Y/n/all/none) prompt self-answers as "apply", so the corruption is written to the working tree silently. The immediately following re-check then flags the tool's own corruption as new issues (shfmt/parse: invalid UTF-8 encoding plus shellcheck SC1009/SC1073/SC1083) and the run exits 1.
Environment
- Trunk CLI 1.25.0 (macOS, Apple Silicon)
- plugins: trunk-io/plugins v1.7.3
- shellcheck 0.11.0 (darwin x86_64 build via Rosetta)
.trunk/configs/.shellcheckrc containing enable=all (enables the optional SC2250 require-variable-braces rule, which carries an autofix)
Reproduction
repro.sh — note the U+2192 arrow (3 bytes, 1 character) on line 6:
#!/bin/bash
set -euo pipefail
original="alpha"
new="beta"
echo "renamed: $original → $new"
Run non-interactively, as a git hook would:
trunk check --no-progress repro.sh < /dev/null
Observed
Two SC2250 autofixes are offered and self-applied. The first (before the arrow) is applied correctly. The second (after the arrow) is applied at the wrong offset:
echo "renamed: ${original} �${ $n}ew"
The re-check of the "fixed" file fails with shfmt/parse: invalid UTF-8 encoding plus shellcheck SC1009/SC1073/SC1083 parse errors, and the run exits 1. In a pre-commit hook this leaves the corrupted content in the working tree and staging area.
The intended edit was $new → ${new}. The mangled output is consistent with the insertion points being shifted by the byte-length difference of the preceding multibyte character (U+2192 is 3 bytes but 1 character, so both insertion points for the second fix land 2 bytes early, splitting the arrow's UTF-8 sequence).
Expected
Fix replacements honour shellcheck's character-based coordinates — decode the file to characters before patching, or convert the reported columns to byte offsets per line. Autofix application should be UTF-8-safe regardless of rule.
Notes
- The bug is rule-agnostic: any shellcheck rule whose JSON output carries
fix objects reproduces it on multibyte lines. Also reproduced with SC2248 (prefer double quoting) on the unquoted variant echo renamed: $original → $new, which was rewritten to echo renamed: "$original" �"� $n"ew.
- SC2250 under
enable=all is simply the most frequent trigger.
- Separately, the non-interactive default of
Apply autofix (Y/n/all/none) resolving to "apply" makes the corruption silent in hooks and CI wrappers; defaulting to "none" when stdin is not a TTY would reduce the blast radius of this class of bug. --no-fix is the workaround we now pass in our pre-commit hook.
Summary
trunk checkapplies shellcheck's suggested fixes (e.g. SC2250 "prefer braces around variable references") using byte offsets, while shellcheck reports character-based line/column coordinates. On any line containing a multibyte UTF-8 character before a fix site, the second and subsequent edits land at the wrong byte position and rewrite the script into invalid UTF-8.Compounding this, when stdin is not a TTY (e.g. inside a git pre-commit hook) the
Apply autofix (Y/n/all/none)prompt self-answers as "apply", so the corruption is written to the working tree silently. The immediately following re-check then flags the tool's own corruption as new issues (shfmt/parse: invalid UTF-8 encodingplus shellcheck SC1009/SC1073/SC1083) and the run exits 1.Environment
.trunk/configs/.shellcheckrccontainingenable=all(enables the optional SC2250 require-variable-braces rule, which carries an autofix)Reproduction
repro.sh— note the U+2192 arrow (3 bytes, 1 character) on line 6:Run non-interactively, as a git hook would:
trunk check --no-progress repro.sh < /dev/nullObserved
Two SC2250 autofixes are offered and self-applied. The first (before the arrow) is applied correctly. The second (after the arrow) is applied at the wrong offset:
The re-check of the "fixed" file fails with
shfmt/parse: invalid UTF-8 encodingplus shellcheck SC1009/SC1073/SC1083 parse errors, and the run exits 1. In a pre-commit hook this leaves the corrupted content in the working tree and staging area.The intended edit was
$new→${new}. The mangled output is consistent with the insertion points being shifted by the byte-length difference of the preceding multibyte character (U+2192 is 3 bytes but 1 character, so both insertion points for the second fix land 2 bytes early, splitting the arrow's UTF-8 sequence).Expected
Fix replacements honour shellcheck's character-based coordinates — decode the file to characters before patching, or convert the reported columns to byte offsets per line. Autofix application should be UTF-8-safe regardless of rule.
Notes
fixobjects reproduces it on multibyte lines. Also reproduced with SC2248 (prefer double quoting) on the unquoted variantecho renamed: $original → $new, which was rewritten toecho renamed: "$original" �"� $n"ew.enable=allis simply the most frequent trigger.Apply autofix (Y/n/all/none)resolving to "apply" makes the corruption silent in hooks and CI wrappers; defaulting to "none" when stdin is not a TTY would reduce the blast radius of this class of bug.--no-fixis the workaround we now pass in our pre-commit hook.