A run that fires at 3 a.m. writes into your notes while nobody watches. Three things make it something you can trust and undo. A run that cannot produce all three is not finished, whatever it did.
One line per run in the vault's log note (or a JSONL file the run owns), written at the end, in a fixed shape:
2026-10-20T03:00Z ingest raw/ -> 4 new, 7 updated, 19 links, 0 contradictions, 2 gaps
Without it, the morning question "what happened overnight" has no answer except reading diffs.
Commit the vault before the run and once after it, with the run in the message (ingest raw/ 2026-10-20: 4 new, 7 updated, the same ingest prefix the obsidian-ingest skill uses, so the watchdog below finds both scheduled and hand runs). Stage only the files the run created or changed (git add <paths>), never git add -A: otherwise reverting the run also reverts whatever else was lying uncommitted in the vault. One commit per run is what makes a bad night revertible in one command:
git revert <commit of the run>
git revert keeps history and removes the notes the run created. git checkout <rev> -- folder/ does not: every note the run created stays, and with it stays in the index and in search. git reset --hard <before> does remove the run's committed notes, but rewrites history and leaves any file the run wrote without committing.
The run ends by telling you something, even "nothing new in raw/". A schedule that produces nothing visible is one you stop reading, and then a failure lives for weeks.
The dangerous failure is not a crash, it is a run that stopped firing, or fires and does nothing. Its own status will often say "success". So watch the output, not the job: check the age of the newest log line or of the newest commit by the run, from somewhere other than the job itself.
# exits 1 when the nightly ingest has not committed in the last 30 hours
VAULT="${VAULT:-$HOME/brain}" # set VAULT to your vault folder
RUN="${RUN:-ingest}" # the commit-message prefix your run uses
[ -d "$VAULT/.git" ] || { echo "not a git vault: $VAULT"; exit 2; }
# match the prefix as plain text, not a regex: RUN=".*" must not count every commit
last=$(git -C "$VAULT" log --format='%ct %s' | awk -v p="$RUN " 'index(substr($0, index($0, " ") + 1), p) == 1 { print $1; exit }')
[ $(( $(date +%s) - ${last:-0} )) -lt 108000 ]Run that check from a different scheduler than the job it watches (a phone shortcut, another machine, a CI cron): a watchdog living inside the thing it guards goes down with it.
- Do the same run by hand, with a dry-run first (list what would change, write nothing), and read the diff.
- Notes with
maintained_by: humanin frontmatter get no writes at all from an unattended run, not even appended links: the run lists what it would have linked in its report, and the owner decides. Appending links is allowed only with the owner in the session. - Give the run write access to the vault folder only, not your home directory.
Related: the receipt shape a run reports in is in skills/obsidian-ingest/SKILL.md; merges go through the dry-run in skills/dedup/SKILL.md.