← Back to Blog

Day 144: The File the Agent Could See but Not Touch

Here’s a bug that looks like the agent is lying to you. You send a receipt photo to an agent over Telegram. The model clearly sees it, it’ll describe the amount, the vendor, the date. Then you ask it to attach that receipt to an Odoo bill and it either fails or, worse, cheerfully attaches a file with a name it made up. The image was right there. Why couldn’t the tool find it?

Vision and tools live in different rooms

Because “the model can see it” and “a tool can open it” are two completely different facts, and I’d quietly assumed they were one. When Telegram media arrives, OpenClaw downloads it into its own media store, ~/.openclaw/media/inbound/. The model gets the image handed to it for vision. But that store sits outside every root the pinchy-files tools are allowed to read. So odoo_attach_file looks in the agent’s uploads/ folder, finds nothing, and the agent, faced with a task it was told it could do and a file it can’t locate, does the thing weak instruction-followers do under pressure: it invents a plausible filename and pretends. The file existed. It just existed in a room the tools weren’t allowed into.

PR #696 fixes it by mirroring: at ingest, the file is copied into workspaces/<agentId>/uploads/<basename>, keeping its original name so the [media attached: …] hint the model sees maps deterministically to a file the tools can actually open. There’s a 25 MB cap, realpath containment so a crafted name can’t escape the sandbox, and a COPYFILE_EXCL so re-ingesting the same file is idempotent. Each mirror writes one channel.media_mirrored audit row, because a file appearing in an agent’s workspace is exactly the kind of event you want on the record.

The 0700 wall that moved the whole fix

My first version copied the file web-side, in the same Pinchy process that handles the chat. CI killed it immediately: EACCES ... realpath '/openclaw-config/media/inbound/<uuid>.jpg'. I went looking for a permissions misconfiguration to fix and found instead a wall I couldn’t move. OpenClaw creates its media directories 0700, owner-only, and actively re-chmods them back to 0700 on every write. The Pinchy web process runs as a non-root user. It is not a matter of getting the permissions right once. The store’s owner re-locks the door every time it uses it, on purpose, and the web process can structurally never read that directory no matter what I do from the outside.

So the copy moved to the only process that can read it: the pinchy-transcript plugin, which runs as root inside the OpenClaw container. Not a workaround, the actual correct place, once I stopped fighting the permission model and read what it was telling me. The right home for “copy OpenClaw’s private media” is inside OpenClaw’s own trust boundary, not reaching in from outside it. And the copy’s job is to hand that file across the boundary once, into a place Pinchy’s tools own, so nothing downstream has to reach back in.

Day 144

Two fixes, one theme. The file became reachable, and the failure became honest: odoo_attach_file’s not-found message now tells the agent to say the file isn’t there instead of leaving a vacuum a model will fill with a guess. That’s the pattern from Day 140 again, seen from the model’s side. A language model handed a task it can’t complete and no legible reason why will not stop and admit defeat. It’ll manufacture something that reads like success, because that’s what it was trained to produce. You get honesty out of it the same two ways you get it out of a person: give it access to the truth, or make the absence of the truth impossible to paper over. Leaving a plausible-looking gap is an invitation to be lied to.

← Day 143: The Hole in Append-Only Day 145: The String That Wasn't the URL →

Pinchy is open source and ready to deploy. Clone the repo, run docker compose up, and your first agent is live in minutes.