DIL.DEV
LOADING0%
DIL.DEV
← ALL NOTES

Haiku Invented the Date on 10 of 18 Nightly Notes, So the Script Owns It Now

A nightly capture job let Haiku write the H1 date and it hallucinated it on 10 of 18 notes.

automationllmclaudeshell-scripting

A terminal showing a shell script overwriting the first line of a markdown file with a forced date string.

The nightly capture job asks Claude Haiku to summarise yesterday's coding sessions into a dated markdown note. On 10 of 18 runs, Haiku wrote the wrong date in the H1. One note dated 2026-09-09 had its heading set to 2026-09-24. The fix was not a better prompt. The fix was to stop asking the model for a fact the script already holds.

What the Job Actually Does

Each night the script pulls the previous day's session logs, passes them to Haiku, and expects a markdown note back. The note is supposed to open with a heading that includes the date, something like # 2026-09-23 Capture candidates.

The script knew the date. It passed the date in the prompt. Haiku still got it wrong on more than half the runs.

Why the Model Kept Inventing the Date

Asking a language model to reproduce a specific fact inside a creative output is a trap. The model is not copying text. It is generating the most plausible continuation of the prompt, and "most plausible" is not the same as "literally what I told you".

The H1 heading looked like a title to the model. Titles are things models riff on. The date in the prompt was context, not a constraint, unless you enforce it mechanically.

  • Root cause: the model was responsible for a fact the script already knew with certainty.
  • Symptom: 10 of 18 notes had a heading date that did not match the file's actual date.
  • Example: the 2026-09-09 note had # 2026-09-24 Capture candidates as its first line.

The One-Liner Fix

After the model writes its output, sed rewrites line 1. In both run-nightly-capture.sh and backfill-capture.sh, the script now does something equivalent to:

text
sed -i "1s/.*/# ${DATE} Capture candidates/" output.md

The model can write whatever it likes on line 1. The script overwrites it before anything else reads the file. This fix survives every future model version because it never relies on the model's behaviour.

The same commit also normalised Content angle: labels to content_angle: for consistent frontmatter parsing.

The Second Failure: a Hook That Crashed the Job

On 23 September, Haiku refused to run the task entirely. The job produced nothing. Investigating showed that a SessionStart hook's output was reaching the model's input before the prompt arrived. The hook text caused Haiku to decline the task.

This is a real risk with the Claude CLI when you run it inside a shell environment that also has user-level hooks configured. The CLI loads those hooks by default, and their output can appear in the conversation before your actual prompt.

Two flags fixed it:

  • --setting-sources project loads settings only from the project-level config, ignoring user-level hooks entirely.
  • --no-session-persistence starts each run completely clean, with no shared session state from previous invocations.

A control run confirmed the difference: without the flags, the hook text was visible in the model's input. With them, it was not.

Silent Failures Are the Worst Kind

Between 18 and 19 September, an OAuth expiry caused the job to fail silently. No note was written. No error was surfaced. The output directory just had a gap, which looked identical to a day with nothing to summarise.

Silent failures in automation are harder to catch than noisy ones because you have to notice an absence. The fix was to make failures loud.

Now, when the script detects a non-zero exit or an empty output, it writes a minimal markdown file with status: capture-failed in the frontmatter. That file lands in the same output directory as a real note.

  • Before: a failed run left a gap that looked like a quiet day.
  • After: a failed run leaves a visible file that the next review pass cannot miss.

This does not fix the underlying failure. It makes the failure impossible to overlook.

Shared Logic in One Place

The date filter logic was duplicated between run-nightly-capture.sh and backfill-capture.sh. When commit c26badc moved it into extract-day.py, both scripts started sharing the same implementation.

The practical consequence: a bug fix or behaviour change in the date filter now applies to both the nightly run and any backfill run automatically. Before, you had to remember to update both scripts and hope they stayed in sync.

This is the same principle as the date-forcing fix. The script should own facts and logic that it is better placed to own. The model should handle the parts that actually need language understanding.

What the Commit History Shows

Three commits tell the story:

  • 45fb5a3 (2026-09-23): identified the hook interference, added --setting-sources project and --no-session-persistence, added the visible failure note.
  • c26badc: moved the date filter into extract-day.py as shared logic.
  • afb5d45 (2026-09-24): forced the H1 date via sed in both scripts, normalised the content_angle label, documented that Haiku had invented the date on 10 of 18 notes.

The pattern across all three is the same: find a place where the model was trusted with something the script could handle more reliably, and move that responsibility back to the script.

What I'd do differently

  • Force every fact the script knows from the start. The date should never have been the model's responsibility. Any field that has a ground-truth value in the calling environment should be written by the calling environment, not requested from the model.
  • Test with hooks enabled from day one. The SessionStart interference only showed up in production because the development environment did not have the same user-level hooks configured. A control run that explicitly checks for hook leakage should be part of the initial setup.
  • Add a failure sentinel before the first real run, not after. The OAuth gap went unnoticed because the failure mode had not been considered. Writing the failure note should have been in the script before it ever ran in production.
  • Treat the weekly triage stage as still broken. The fixes here cover the nightly capture run. The triage stage has the same potential for model-invented facts and silent failures, and it has not been touched yet.

NOTES BY EMAIL

Get the next note in your inbox.

One email per note. No spam. Reply “stop” any time and you are off the list.