Read Me, Don’t Run Me
A fenced code block in Markdown is a promise a document makes to a human: “here is roughly the command I mean.” To a person reading it, the fence is an illustration. To a machine that copies fenced blocks into a shell, the fence is an instruction — run this, verbatim. Most of the time that’s harmless, because most of what sits in a ```bash block really is a command. Then one day it isn’t, and the machine finds out the hard way.
We build LastDB with a fleet of autonomous agents. They read their own instructions — skill files, routine files, the body of a task card — and those instructions are Markdown, full of fenced shell blocks the agents are expected to run. Which means the files are two things at once: documentation a person maintains, and a script a machine executes. The trouble lives in the gap between those two readings.
The fence with two meanings
Open any of our skill files and you’ll find prose and commands interleaved — a heading explaining what a step is for, a bullet list of what to check, and then a fenced block of the actual shell to run. A human glides over that document effortlessly, because a person knows that ## Goal is a section title and git fetch origin is a command. The distinction is obvious to a reader and completely invisible to a fence. Markdown never marks which lines it meant you to run.
So the failure mode writes itself. Somewhere — a hastily authored skill, a task card whose body got pasted into a fenced block for tidy rendering — a chunk of prose ends up inside a ```bash fence. A heading. A bulleted checklist. A card header like GOAL: or VERIFY:. A cross-reference in double brackets. To the human author it still looks like a nicely formatted note. To an agent primed to run fenced shell, it looks like four commands, and it will try to run all four.
Prose doesn’t fail cleanly
You might hope running English through a shell just errors out politely. It doesn’t. ## GOAL: ship the fix is a comment, so the shell shrugs and moves on — and now the agent believes it ran the block successfully. A bullet like - reproduce on latest is a command named - that fails in a way that looks like an environment problem, not a content problem. And the genuinely bad case is an agent that, trying to be helpful, decides the messy block just needs to be “cleaned up and run” — wrapping the whole thing in an eval and executing prose as if it were a program. The file looked fine the whole time. It rendered beautifully.
A boundary you can’t enforce by hand
The instinct is to write a rule in the contributor guide: “don’t put card text inside shell blocks.” But a rule that lives in a human’s memory is exactly the wrong place to keep a boundary between read this and run this — because the person who breaks it is a tired author at midnight, and the thing that suffers is a machine at 3 a.m. with no author present to catch the mistake. Discipline doesn’t scale to a fleet that reads faster than anyone can review.
So we made the boundary mechanical. A small linter now walks every prompt, skill, routine, and card file we ship, and for each fenced shell block it goes down the block line by line. A line that looks like a command is fine. A line that looks like Markdown or card text — a heading, a bullet, a blockquote, a card field label, a [[wikilink]], a bare token like :9001 — is flagged, because a shell would try to execute it. It knows to leave real heredoc bodies alone (text deliberately quoted with <<'EOF' is data on purpose). Anything else that’s prose-in-a-command-fence, it refuses to let the file land.
The rule, in one line
Text is data, not a script. If a block has headings, bullets, card headers, cross-references, or prose in it, it is something to be read — and it belongs in a quoted body file passed to a command over stdin, never pasted onto a command line. The shell is for intentional commands only. The lint doesn’t trust anyone to remember that; it checks, every file, every time, before anything runs.
The general shape of the bug
This is an old lesson wearing new clothes. Every injection bug in history is the same confusion: a value that was meant to be data gets interpreted as code because nothing kept the two apart. SQL injection is a string that was supposed to be a name and got run as a query. This is that bug, moved up a floor — into the documents we hand to machines that act on them. The moment your instructions are both prose and program, you need a hard line between the part to read and the part to run, and you need something other than good intentions to hold that line.
The fix isn’t clever. It’s a fence around the fence: a check that runs before the shell does, so the ambiguity gets resolved by a linter at authoring time instead of by an agent at runtime. Read me, or run me — but the file has to say which, and now it has to prove it.
More on how the fleet builds LastDB safely: Prove It To Land, on why a green pull request isn’t proof, and Building LastDB with an autonomous agent loop.