Skip to content
open tomato
GitHub
  1. Dev diary
  2. Enough to Prove a Point
  3. Part 5

The checker was looking elsewhere: 85 of 85 files reported missing, and every one was there

A verification tool reported 85 of 85 files missing. All 85 existed.

ShareLinkedInBlueskyMastodonXHacker NewsEmail.md

TL;DR:

  • A verification tool reported 85 of 85 files missing. All 85 existed.
  • The tool was right about everything it could see. It was checking a directory one path segment away from the one the service writes to.
  • Three versions of the same failure turned up in two days, from three unrelated causes.

I worked through this with Claude, in the Claude desktop app's Claude Code tab. There is no transcript left of this one, so Claude's lines here are reconstructed from the note we wrote at the time, not quoted from a log.

This closes the series that began with part 1. That piece is worth reading first, though this one stands on its own.

Two words this piece uses. The pipeline is a recruiter project of mine, built on n8n, a workflow-automation tool whose steps are called "nodes". It later became the project now called bart.

The failure class

A checker that reports absence is making two claims at once:

  1. The thing is not there.
  2. I looked in the right place.

It only ever shows you evidence for the first. From the output alone, a broken system and a misaimed checker are identical.

That is why this deserves its own name in a series about stopping too early. A tool that says "nothing found" sounds like an answer, and it is often a question.

Instance A: the doubled path

We introduced a shared storage location. The service writes there, and a verification tool reads there. One setting drove both paths.

Then the storage was restructured. The mount point became a shared bucket, so the service moved into a named sub-directory.

The next run of the tool reported 85 of 85 records missing. Its printed directory ended in the sub-directory name twice.

The setting named the parent location, and the code appended the sub-directory itself. I had set that variable to the sub-directory, because its name reads naturally as "the path where my files are". The documentation said "mount point". The name won.

Me: I think we have to do some clean or something.

Claude: 85 of 85 isn't a data distribution. Real corruption is partial. A total is a location bug until proven otherwise.

That is the tell worth keeping: the number, not the error.

The fix is forgiving rather than loud, which is a deliberate departure from how we usually do things here. Both spellings now resolve to the same directory, and the tool prints which one it saw.

The match has to be exact on the last part of the path. If the setting ends in the sub-directory's name, the code doesn't append it again. If it ends in something that merely contains that name, like sub-workspace, it does. There was nothing for me to decide between the two readings, so there was nothing worth stopping for.

Instance B: the mount that was never there

Separately, the service's container had no mount for that location at all.

This does not produce an error. The path inside the container is simply an ordinary directory in the container's own writable layer. We created it by hand, wrote a marker file, and everything worked.

For one day. The next time the container was recreated, that layer went, and every file with it.

That is what makes it dangerous. A missing mount is indistinguishable from a working one until the container is replaced, and then the failure is retroactive: the data was never anywhere durable.

My own report had the clue in it:

We did create it and at some point it worked, but there's nothing in the config for it. I can't figure out how it ran the second time after failing.

Worked once, then stopped, with no config change, is the signature of ephemeral storage. A wrong path fails every single time.

The fix was the mount. It also needed a second setting, because without it the workflow tool's file steps look somewhere else entirely and refuse to read anything. That refusal reads like a missing mount too, so both had to be set at once.

The setting was given no default on purpose. A missing value now stops the service at start, instead of quietly recreating the throwaway directory.

Instance C: the sandbox that answered like a filesystem

The third one wasn't a bug at all.

Claude, running on a machine with a network share mounted, could check an individual file through that share. Listing any directory on it returned a permission error. On that machine, and on that day, the operating system's privacy layer refused to list a directory on a network volume while allowing a check on a single file through the same share.

The consequence for verification is precise: a check that lists returns "empty", and a check that asks about one file returns "present", for the same directory, in the same minute. An agent using the first would confidently report an empty vault.

There was a second restriction in play too, from a different party. Shell access to the host running the service was blocked by a deny rule in the agent's own permission configuration, which I had set. I had also written allow rules for those hosts, and believed they applied. Deny wins. My mental model and the effective policy disagreed silently.

Neither restriction is wrong. The mitigation is a protocol, and it is the one I now use whenever an agent is walled off from the thing it needs to check:

if you're limited or restricted, provide me an output I can execute… and I'll provide its output back to you.

What it cost

  • One round of investigation into 85 files that were never missing, plus my reasonable conclusion that something needed purging.
  • Real data loss, bounded but unknown: everything written into the container's throwaway layer between the day it appeared to work and the next recreation. The database still believes those files exist, because the records were stamped as written.
  • A compounding effect, which is the part I'd underline. While the checker was pointed at the wrong directory, its output could not be used to evaluate whether the mount fix had worked. A broken checker doesn't just fail to help. It hides the thing it was built to watch.

What changed

Safeguard What it prevents What it costs
Path resolution that accepts both spellings, prints the resolved directory, and notes when it saw the already-scoped one Instance A, for either reading of an ambiguous setting Trivial. Four tests, including a control proving a merely similar name is still appended to.
Retry with backoff on the storage-liveness probe, with the absence path unchanged A slow or sleeping disk read as an absent mount About 20 seconds, worst case, on a once-daily job. Only the patience changes.
No default for the mount-location variable The throwaway-directory bug recurring silently The service refuses to start without it. A loud failure at start beats a silent one at write.
A fallback protocol for sandboxed checks: emit a runnable command, get the output back Instance C, and every future restriction One round trip per check. Removes the temptation to substitute a reachable proxy for the real system.

The close

Four habits came out of this, and they are cheap.

  1. Treat a total as a location bug. 85 of 85, 100%, "none found": real failures are usually partial. Prove the path before you investigate the data.
  2. Print the resolved location, always. Both A and C were diagnosed the moment the directory appeared next to the count. One line of output turns an unfalsifiable claim into a falsifiable one.
  3. A setting that can be read two ways will be set both ways. Accept both and report which you saw. Documentation loses to a plausible variable name every time.
  4. Ask what a missing configuration produces, not what it prevents. An absent mount, an absent volume or an unset base path often yields a working-looking local substitute rather than an error.

And the one that belongs to this whole series: an intended restriction and a bug look the same to the party being restricted. An agent that cannot list a directory sees the same empty result whether the directory is empty or the listing is denied. "Nothing found" from a restricted surface means unknown, not absent.

At the time of writing, some of the planned safeguards are still in development. In bart today: there is no attempt counter to retire a failing item, no halt on authorization or quota errors, no restart: no on two services and no container labels, the panic script stops containers by project rather than by label, and the intake path still makes one model call per document.

One correction to this piece's own account, from the same check: the two-spelling path resolution described above is recorded in the note written at the time, and nothing matching it was found in bart on 22 September. The fix did not travel with the project when it was forked. A second look on 22 October found no storage-location setting in bart under any name, at any point in its history, so there is nothing there for the fix to resolve.

Text licensed under CC BY 4.0: re-post or adapt it freely, with credit and a link back to this entry.