About this article
In a development environment spanning Windows, Git, Linux, and an AI Agent, I ran into a case where the SHA-256 of text files that should have had identical content didn't match. The cause was the difference between CRLF and LF — a story familiar to anyone who uses Git.
What actually became a problem this time was that there was no contract anywhere specifying how an AI Agent should interpret that raw hash mismatch. Normalization on the Repository side was working as intended. What was missing was semantics on the Agent side.
This article is a record of how an initial hypothesis I formed was disproved by a Git history investigation, how I redefined the problem, and how I implemented it as a minimal Agent contract.
1. Introduction
If you use Git regularly, you've probably run into diffs getting noisy or hashes changing because of CRLF versus LF at least once. In most cases, the story ends with normalizing line endings via .gitattributes.
This time, though, the problem that came up in an environment mixing Windows, Git, Linux, and an AI coding agent went one step further.
Even though LF normalization via .gitattributes itself was working correctly, there was no agreement on how an AI Agent should interpret the fact that a hash didn't match — leaving room for unnecessary re-checks and branching investigations to occur.
This article isn't a general explanation of CRLF versus LF. It's a record of an operational problem in AI-assisted development — how to handle this kind of "observed difference" — and the countermeasure taken.
2. What happened
A case came up where the raw SHA-256 hash didn't match for text files I believed had the same content.
Looked at as raw byte sequences, LF and CRLF are different. It's expected behavior for SHA-256 not to match even when the content looks identical — nothing technically surprising so far.
The problem wasn't the hash mismatch itself — it was that its meaning hadn't been defined on the AI Agent side.
3. Why a hash mismatch can be costly in AI development
When a human sees a hash mismatch, experience can let them narrow the candidates early — "ah, this might just be a line-ending difference."
An AI Agent, on the other hand, doesn't necessarily rule out a line-ending difference first if there's no explicit decision rule. It may treat the observed fact as having several possibilities that all warrant further checking.
The fact of a hash mismatch alone can't distinguish between:
- the content actually changed
- remote and local have drifted apart
- the checkout is stale
- it's looking at a different file entirely
- only the line endings differ
- a re-fetch is needed
Without an explicit decision path for the Agent, an exploration path like the following can occur.
hash mismatch
→ re-check
→ re-read the file
→ diff
→ check Git
→ compare the full text
→ ask for human confirmation
→ re-run
This can consume tokens, time, and human attention. The reason a fundamentally small representational difference like CRLF/LF can grow into a relatively large cost in AI-assisted development is that this "absence of meaning" leaves room for the number of confirmation paths to increase.
What's described here isn't a fixed property of AI Agents in general. Nor did I quantitatively measure how much exploration actually occurred, or how often, in this particular case.
4. The initial hypothesis: the LF rule had been lost
When I first noticed the problem, the hypothesis I formed was this:
I thought I had already fixed an LF-unification rule at some earlier point, and wondered whether it had been lost during updates to the Agent operating contract or templates.
Checking the current Agent operating contract, I did find a rule for:
- Windows local repository / local hash checks are performed in PowerShell
— that is, a rule about where to run checks.
But I found no wording addressing:
- canonical line ending = LF
- raw hash mismatch ≠ content mismatch
- handling of a CRLF/LF-only difference
- normalized comparison
- the diagnostic order for a hash mismatch
— anything concerned with giving meaning to the difference.
At this point, the hypothesis that "the LF policy itself had been lost somewhere in the past" looked like the natural explanation.
5. Checking the Git history
To test the hypothesis, I did a read-only investigation of the Git history for the documentation operations base I use to manage rules and templates for AI-assisted development.
Here is what I found.
Current state
.gitattributes had:
* text=auto eol=lftext eol=lffor various file extensions
— a policy for keeping the whole repository on LF, and it was still there.
The tracked files I checked with git ls-files --eol also showed:
i/lf w/lf
— that state.
The Agent operating contract and templates I checked, on the other hand, had no rule for interpreting CRLF/LF differences or hash mismatches.
What the history showed
Looking back through the Git history, an LF-fixing rule had been introduced into .gitattributes in July 2026, and tracked text files had been normalized to LF.
No later history of that line-ending rule itself being changed or removed turned up.
6. The hypothesis was wrong
In other words, the initial hypothesis — that the LF-unification rule had vanished after being introduced — was wrong.
LF normalization via .gitattributes had existed continuously from the point it was introduced to the present, and within the range of history I checked, it had not been changed or removed.
This was an important piece of counter-evidence. If it had simply been a matter of "the rule was lost, so fix it," the response would have been as simple as restoring .gitattributes.
But in reality, what needed fixing was somewhere else.
7. The real problem: insufficient separation between normalization and semantics
After the investigation, I redefined the problem as follows.
Mechanical normalization on the Repository side — the role .gitattributes handles, aligning file representation to LF — was working correctly.
On the other hand, semantics on the Agent side — the role the Agent operating contract should handle, deciding how to interpret an observed difference — was never defined.
As a diagram, the state looked like this.
Repository
LF canonicalization (.gitattributes)
→ working correctly
Agent
raw hash mismatch interpretation
→ undefined
The normalization rule hadn't "disappeared." The conclusion after investigating was that an agreement needed at a different layer from normalization simply hadn't existed in the first place.
8. Canonicalization / Observation / Interpretation
To organize this problem, I split what was being dealt with into three layers.
Layer 1: Canonicalization
The layer that defines what counts as the canonical form. The main target here was making LF the canonical line ending. .gitattributes was responsible for this role.
Layer 2: Observation
The layer that measures what's actually different. This covers things like the raw SHA, EOL differences, comparison after LF normalization, and diffs.
Layer 3: Interpretation
The layer that defines how the Agent gives meaning to an observed difference. Classifications like IDENTICAL / REPRESENTATION_ONLY / CONTENT_DIFFERENCE / UNKNOWN are conceivable here.
What was missing this time was this Layer 3.
What matters is separating the observed fact from its interpretation.
Observed:
raw SHA256 differs.
Inference:
content may have changed.
Additional observation:
LF-normalized content is identical.
Conclusion:
representation-only difference.
That the raw hash differs is an observed fact. But "the content changed" is, at that point, still only an inference.
If an additional observation shows that the LF-normalized content matches, that moves things toward a different conclusion: it was only a representational difference.
This separation makes it easier to keep the Agent from guessing more than necessary, and to constrain its decision path when something unusual is observed.
9. Excessive guardrails can be a problem too
While considering countermeasures, a more thorough form of verification also came up as an option.
For example, checking the raw hash, normalized hash, BOM, encoding, EOL, diff, and — if needed — the Git history, every time, for every file.
I didn't adopt this approach, though. The cost of running the guardrail itself risked exceeding the cost it was meant to reduce.
Imposing heavy verification on every file operation could prevent the divergence of exploration caused by a hash mismatch. But in exchange, verification cost would be incurred constantly, even during normal work where nothing was wrong.
What I adopted instead was a Fast Path / Exception Path approach.
Normal operation
→ no extra checking
When a raw hash mismatch occurs
→ minimal diagnosis
When normalized content mismatches
→ deeper investigation
The goal isn't to increase the total amount of checking. It's to make the branching deterministic for when something unusual actually happens.
10. The fix: a minimal contract
With that in mind, I kept the semantics added to the Agent operating contract to a minimum. Here is the content.
Repository-managed text files use LF as canonical line endings.
A raw hash mismatch alone does not establish a content mismatch.
For text files, first compare LF-normalized content.
If normalized content matches, treat the difference as representation-only
and continue.
Perform deeper investigation only when normalized content differs
or the cause remains unknown.
What this contract means is the following.
- The canonical line ending for Git-managed text files is LF.
- A raw hash mismatch alone is not treated as confirming that the content changed.
- For text files, LF-normalized content is compared first.
- If the normalized content matches, the difference is treated as representation-only.
- A CRLF/LF-only difference is not, by itself, a reason to require regeneration, stopping work, or human approval.
- Further investigation happens only when the normalized content differs or the cause remains unknown.
- Routine work units are not required to include EOL or hash checks by default.
This contract doesn't increase what has to be checked every time. It's a contract that constrains how an observed anomaly is interpreted and how far the follow-up investigation goes.
11. Applying it to the documentation base
I applied this policy to the documentation operations base I normally use.
The changes were limited to:
- the current Agent operating contract
- the template for new environments
- the changelog
— these three items. I did not change the existing .gitattributes, and I did not re-normalize tracked files.
For verification, I confirmed being on the main branch with a clean baseline, that local and remote references matched, that git diff --check and the cached check both passed, that the staged set matched the intended change targets, the content of the Agent contract text, Markdown safety, the full diff, that local and remote references matched again after pushing, and that the worktree was clean.
One automated comparison path wasn't run, due to environment constraints, but the identity of the contract text itself was already confirmed through a full-text diff. For that reason, I don't treat the unrun automated comparison as an unverified item blocking completion of the work.
12. Current status and unverified items
Here is what has been confirmed so far.
- LF canonicalization existed before this issue.
- The line-ending rule in
.gitattributeshad not been changed or removed since. - This problem was not the disappearance of the LF policy.
- There was no contract on the Agent side for how to interpret a hash mismatch.
- A minimal contract was applied to the Agent operating contract and the template.
.gitattributeswas not changed.- Renormalization was not performed.
- Constant hash/EOL validation was not introduced either.
On the other hand, some things remain unverified.
How much rereading, re-investigation, human confirmation, work stoppage, token consumption, and working time actually decrease when a hash mismatch occurs naturally after this contract is in place — that hasn't been observed yet.
So a quantitative claim like "this contract reduced token consumption by X%" cannot be made at this point.
What can be said is only that the design and implementation aimed at reducing wasted exploration branches has been completed.
13. Lessons from operating an AI Agent
Two main lessons came out of this round of work.
The first is that normalizing the state of the Repository alone doesn't determine how the Agent handles the difference. .gitattributes controls file representation, but it doesn't control how a raw hash difference is interpreted. Canonicalization and Interpretation are separate problems.
The second is not to convert an initial hypothesis directly into a countermeasure. This time, the hypothesis that "the LF rule might have been lost" looked natural. But checking the Git history showed it was wrong.
Had I moved straight to a fix without verifying the hypothesis, I might have pointlessly re-set-up a .gitattributes that was already fine, while never touching the Agent-side decision rule that was actually missing.
From this case, I came away with the idea that rather than making the Agent guess more cleverly, it can be more effective to reduce the need to guess in the first place.
14. Summary
The difference between CRLF and LF isn't a new problem in itself. But once an AI Agent is folded into the development process, that small representational difference can give rise to a different kind of problem.
Without a contract for what a raw hash mismatch — an observed fact — actually means, there's more room for the Agent to decide that additional confirmation is needed.
In this case, LF normalization on the Repository side was working correctly from the start. The initial hypothesis — that the LF rule had been lost — was disproved by the Git history investigation.
What was missing wasn't normalization itself — it was a contract for how the Agent interprets an observed difference.
So, instead of adding more verification during normal operation,
raw hash mismatch
→ LF-normalized comparison
→ if representation-only, continue
→ if content mismatch or UNKNOWN, investigate further
— I added this minimal exception handling to the Agent operating contract.
The quantitative effect of this countermeasure is still unverified.
That said, this investigation at least made one operational point clear: mechanical normalization and the Agent's interpretation of a difference need to be designed separately.
This article is a personal development record. It does not claim to be a general standard or the only correct approach. Real names and infrastructure details for the underlying project (repository names, commit hashes, internal directory structure) are deliberately omitted. See the site-wide disclaimer for more.