Skip to content

typos-format: hook silently corrupts abbreviated git SHAs, including inside backticks #1257

Description

@kyle-sexton

This was generated by AI during a session in github-iac, where the defect was hit live.

Defect

The typos-format PostToolUse hook silently rewrote an abbreviated git SHA in a file I had just
written, corrupting it. 534caf5 became 534calf5caf "corrected" to calf.

I only caught it because I re-read the file. The hook reports its rewrite as "likely a formatter"
via the PostToolUse notice, which does not say what changed, so an agent that writes and moves on
ships the corruption. That is the actual severity here: silent data corruption in agent-authored
files
, not a cosmetic annoyance.

Reproduction — minimal and verified

typos-cli 1.44.0, on this file:

short `534caf5`
long `534caf5d6ebab90faf8c1932c5d8007cba3710b9`
bare 534caf5
other short sha `1a2caf9`
error: `caf` should be `calf`   typotest.md:1:11    (inside backticks)
error: `caf` should be `calf`   typotest.md:3:9     (bare)
error: `caf` should be `calf`   typotest.md:4:21    (different SHA, also inside backticks)

Two findings worth separating:

  1. Backticks are not respected. Two of the three hits are inside inline code spans. A code span
    is the canonical way to mark "this is a literal, do not language-process it", and the hook writes
    through it anyway. Any identifier, hash, hex color, or flag inside backticks is exposed.
  2. The 40-character SHA is NOT flagged — only the 7-character abbreviated form. typos has a
    heuristic that skips long hex strings and recognizes them as hashes; a 7-char abbreviated SHA is
    below that threshold and gets word-split instead. So the corruption specifically targets the
    short SHA form, which is exactly the form used in prose, changelogs, handoffs, ADRs, and commit
    references.

It is not limited to caf. Any abbreviated SHA whose hex happens to contain a token in the typos
dictionary is a candidate — 1a2caf9 above is a second example, and the dictionary is large.

Why the existing config gate does not cover this

#884 established that the hook must not be a no-op on zero-config repos. Correct — but the
consequence is that it now runs with stock typos defaults on repositories that never opted in and
have no _typos.toml to add an extend-ignore-re
. github-iac is exactly that case. So the
population most exposed to this is the one with no mechanism to defend against it.

Suggested direction (not a decision — the maintainer's call)

Options, roughly in increasing order of effort:

  1. Ship a baseline extend-ignore-re with the hook covering abbreviated git SHAs
    (\b[0-9a-f]{7,40}\b) and inline code spans, so zero-config repos inherit a safe floor rather
    than stock defaults.
  2. Report-only on zero-config repos, auto-fix only where a repository opted in by shipping a
    config. Preserves typos-format: opt-in config-gate makes the hook a silent no-op on zero-config repos, defeating the plugin's own auto-fix purpose #884's intent (not a silent no-op — it still surfaces findings) without
    write-through on repos that never asked for it.
  3. Make the PostToolUse notice name the rewrite — a diff, or at minimum the corrections applied.
    Worth doing regardless of which of the above is chosen: a silent rewrite an agent cannot see is
    the part that turns a false positive into shipped corruption.

Acceptance

  • An abbreviated git SHA survives the hook unchanged, in prose and inside backticks
  • Zero-config repositories do not get destructive stock-default autocorrection, or the floor they
    get is explicitly chosen rather than inherited
  • When the hook rewrites a file, what it changed is visible to the caller

Related

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs-humanHuman-in-the-loop required; autonomous sessions must not resolve items carrying this.priority: highSignificant impact, or blocks an imminent release; staff this cycle.status: readyTriaged, unblocked, and fully specified; eligible to pick up.work-class: scopedA briefed fix or small feature; blast radius bounded by the brief, tests exist.

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions