Skip to content

shellcheck autofix applies fixes at byte offsets, corrupting files containing multibyte UTF-8 characters #1150

Description

@BenjaminDSmithy

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions