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 L178 → detectGitAuthor). 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).
Observed
Running
fixHeaders()in any consumer repo on a machine whosegit config user.namediffers from the name recorded in the existing headers rewrites essentially every file: the@Last modified byname is swapped to the current identity and@Last modified timeis restamped to now, with zero content change. In slothlet,npm run fix:headers -- --dry-runcurrently reports 1551 of 1565 files would be updated, purely becauseuser.nameon the machine isShinraiwhile the committed headers recordNate Corcoran <CLDMV>(the email is identical — the one-word name difference alone drives all 1551). Under slothlet'sprecommitthis 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 timeverbatim (comparisonLastModifiedAt = existingLastModifiedAt || …, src/core/fix-headers.mjs L271) and preserves the created-by identity unlessforceAuthorUpdateis set (L285–L286). But the last-modified pair is unconditionally the current identity (L287, and again in the rewrite branch at L322):fileMetadata.authorNamefalls through togit config user.namewhen the caller doesn't passauthorName(src/detect/project.mjs L178 →detectGitAuthor). So any identity drift makescomparisonReplacement.changedtrue for every file that already has a header, andneedsUpdatethen also restamps@Last modified timeto 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 — extendforceAuthorUpdateto 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()alongsideextractHeaderAuthorIdentity()(parsing the@Last modified by:line), and build the comparison header withlastModifiedByName/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-headers1.3.7 (slothlet's installed version); the cited lines are unchanged onnextat7f55738(v1.3.9).