← Holocron Logs

My Agent Told Me It Wrote Twelve Files. It Wrote Zero.

A local 14B model running an agent framework reported successful file operations in perfect detail: filenames, counts, directory listings. None of it happened. It had substituted describing a tool call for making one, and it did this confidently for an entire session. This is how I caught it, why it happens, and the architectural fix that stopped it.

Why this matters beyond the homelab: Agentic systems are being handed real filesystem and API access across the industry right now. The failure mode below is not exotic and it is not loud. An agent that fabricates success produces output indistinguishable from an agent that succeeded, and if your workflow ends at “the model said it worked,” you do not have a workflow. You have a report.


The Task

I was ingesting documentation into my agent’s skill library. Straightforward: read files from an export directory, convert each into a structured skill file on disk. Batch operation, dozens of files.

The agent reported back cleanly. Files processed, skills created, counts given. Everything in the right format, phrased with total confidence.

Then I looked at what it said it had processed.


The Tell

The filenames were wrong. Not slightly wrong. Invented.

It reported handling chapter1.md, chapter2.txt, config.yaml, diagram1.png, import_script.sh, backup_plan.txt.

My actual export directory contained thirty-two files named after real documentation pages: adr-001-proxmox-as-hypervisor.md, vlan-and-network-map.md, and so on. There was no chapter1.md. There was no diagram1.png. There was no import_script.sh.

It had generated a plausible-sounding directory listing rather than reading the real one. And it had gone further, inventing content to match, including policy names in the generated skill files that had never existed in any document I own.

Verification took one command:

find /root/.hermes/skills -name "SKILL.md" -newermt '-10 min' | wc -l

Zero. Nothing had been written. The entire reported operation was fiction.


Confirming the Mechanism

One fabricated response could be a fluke. I needed to know whether tool execution was working at all, so I asked for something with an unambiguous, checkable answer:

run: date

It came back with a timestamp. The timestamp was wrong.

That was conclusive. The model was not calling the terminal tool and misreporting the result. It was not calling the tool at all, and generating text shaped like a tool result to fill the gap.

Another instance was even more explicit. Asked for a file count, it responded with something labeled Output (example): 25. That is the failure in plain sight: it knew it was producing an example rather than a result, and presented it as an answer anyway.


Why This Happens

The model runs locally, a 14B parameter model on a single GPU. The fabrication correlated strongly with two conditions: long context and sustained load.

The mechanism, as best I understand it, is that invoking a tool and describing a tool invocation are very similar text-generation tasks from the model’s perspective, and one of them is dramatically cheaper. Under pressure, meaning a long conversation, a batch request, or a degraded inference backend, the model takes the cheaper path. It produces text that pattern-matches what a successful tool call and result look like, because that pattern is extremely well represented in its training data.

It is not lying in any intentional sense. It is doing what it always does, predicting plausible continuation, and a plausible continuation of “I will list that directory” is a directory listing. Whether that listing corresponds to reality is not something the generation process is checking.

Compounding this: three GPU hangs during the same period, with the inference runner pinned near 100% CPU for twenty-plus minutes. During those windows output degraded badly, including one response that switched languages partway through. A struggling backend made an already-present failure mode much more frequent.


The Fix, In Two Parts

Part one: explicit discipline rules in the agent’s persona file.

The agent’s behavior is shaped by a persona document loaded at session start. I added rules that target this specific failure directly:

This helped. It did not solve it, and I want to be clear about that, because “fix the prompt” is the common advice and it is insufficient. A model under load that has already degraded into text-generation mode is not reliably reading its own instructions either. Prompt-level rules reduce frequency. They do not provide a guarantee.

Part two, which actually solved it: stop asking the agent to do deterministic work.

This was the real insight. Converting a file into a structured skill file is not a reasoning task. It is fetch, wrap in frontmatter, write to a path. There is no judgment involved, so there is no reason a language model should be in the loop at all.

I moved every remaining ingestion to bash:

curl -sL "$SOURCE_URL" -o "$TMP/content.md"
{
  printf -- '---\nname: %s\ndescription: %s\n---\n\n' "$NAME" "$DESC"
  cat "$TMP/content.md"
} > "$SKILL_DIR/$NAME/SKILL.md"
find "$SKILL_DIR/$NAME" -name "SKILL.md" -size +0c || exit 1

Fetch, wrap, write, verify. Completed in seconds, with zero fabrication risk, because bash does not have a fabrication mode. A script either wrote the file or exited non-zero.

The division I settled on: the agent is for reasoning, bash is for transformation. Anything with a deterministic correct answer should not be routed through a probabilistic system.


The Operational Rule That Came Out of It

The most durable outcome is a habit rather than a config change.

The filesystem is the only source of truth about the filesystem.

The agent’s self-reported skill count consistently differed from reality. Not always, not by a consistent amount, which is the worst possible property because it means you cannot even calibrate the error. The only reliable number came from asking the filesystem:

find /root/.hermes/skills -name "SKILL.md" | wc -l

Generalized: after any agent operation that claims to have changed state, verify the state independently. Not by asking the agent to confirm, which just gives it a second chance to fabricate, but by checking the underlying system yourself.

One useful detail from cleanup. When I went looking for the fabricated skill files to delete them, they did not exist. There was nothing to clean up, because nothing had been written. The fabrication was total rather than partial, which is genuinely the better outcome. A partial write, some real files and some invented ones interleaved, would have been far harder to untangle than a clean nothing.


Takeaways

Agents can fabricate tool results, confidently and in correct format. This is not rare and it does not announce itself.

Test with a checkable answer. Ask for the date. A wrong timestamp tells you instantly whether execution is real.

Watch for invented specifics. Generic plausible filenames where real ones should be is the clearest tell.

Load and long context increase fabrication rate. Monitor the inference backend, because a struggling GPU produces unreliable reasoning before it produces obvious errors.

Prompt rules reduce frequency, not risk. Necessary, insufficient.

Do not route deterministic work through a probabilistic system. Bash cannot hallucinate a file write.

Verify state independently after every claimed change. Asking the agent to double-check is not verification.


Related: Jocasta: Retiring My Custom Bot for a Real Agent Framework | The Fleet Codex Pipeline | GPU AI Platform

← Back to Holocron Logs