Skip to content

Existing headers are rewritten tree-wide when the runner's git identity differs — @Last modified by refresh must be opt-in, not the default #24

Description

@Shinrai

Observed

Running fixHeaders() in any consumer repo on a machine whose git config user.name differs from the name recorded in the existing headers rewrites essentially every file: the @Last modified by name is swapped to the current identity and @Last modified time is restamped to now, with zero content change. In slothlet, npm run fix:headers -- --dry-run currently reports 1551 of 1565 files would be updated, purely because user.name on the machine is Shinrai while the committed headers record Nate Corcoran <CLDMV> (the email is identical — the one-word name difference alone drives all 1551). Under slothlet's precommit this has repeatedly turned a scoped few-file fix into a 700–1500-file diff that has to be manually reset before committing (observed 2026-07-09 at 721 files and again 2026-08-08).

Cause

The update decision is otherwise conservative: the comparison header reuses the existing @Last modified time verbatim (comparisonLastModifiedAt = existingLastModifiedAt || …, src/core/fix-headers.mjs L271) and preserves the created-by identity unless forceAuthorUpdate is set (L285–L286). But the last-modified pair is unconditionally the current identity (L287, and again in the rewrite branch at L322):

lastModifiedByName: fileMetadata.authorName,
lastModifiedByEmail: fileMetadata.authorEmail,

fileMetadata.authorName falls through to git config user.name when the caller doesn't pass authorName (src/detect/project.mjs L178detectGitAuthor). So any identity drift makes comparisonReplacement.changed true for every file that already has a header, and needsUpdate then also restamps @Last modified time to the current time (current-time-on-change, L304–L306).

There is also no extractor for the existing last-modified identity: extractHeaderAuthorIdentity (L121) parses only @Author:/@Email:, so the comparison has nothing to preserve even if it wanted to.

Expected

On a file that already has a header, the tool's job is structural: verify the header exists and its stable fields are correct. The current identity should only be written when creating a header for a file that lacks one, or when a real difference elsewhere already forces a rewrite (a fresh last-modified stamp is being written anyway in that case, which stays correct). A bare identity difference on an otherwise-correct header must not count as needsUpdate. Refreshing the recorded identity across a tree should require an explicit opt-in — extend forceAuthorUpdate to cover the last-modified pair, or add a dedicated force flag — mirroring how the created-by pair is already gated.

Suggested fix

Add an extractHeaderLastModifiedIdentity() alongside extractHeaderAuthorIdentity() (parsing the @Last modified by: line), and build the comparison header with lastModifiedByName/Email: existingLastModifiedIdentity.* || fileMetadata.author* — the same preserve-unless-forced shape L285–L286 already uses for created-by. The rewrite branch (L320+) can keep stamping the current identity, since reaching it means the file genuinely needed an update.

Environment

Reproduced with @cldmv/fix-headers 1.3.7 (slothlet's installed version); the cited lines are unchanged on next at 7f55738 (v1.3.9).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions