[← Blog]

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.

ONE CODE FENCE ```bash   … ``` A HUMAN reads it — documentation AN AGENT runs it — verbatim THE FENCE NEVER SAYS WHICH ONE IT MEANT
Fig. 1 — the same fence is documentation to a human and an instruction to a machine; it never says which

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.

```bash git fetch origin main COMMAND — OK ## GOAL: ship the fix HEADING — DATA - reproduce on latest BULLET — DATA [[card-1298]] · :9001 PROSE — DATA ``` THREE OF THESE FOUR LINES ARE NOT COMMANDS
Fig. 2 — one real command, three lines of prose a shell would still try to execute

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.

PROMPT · SKILL & CARD FILES THE LINT walks every shell fence line by line PASS merges · may run REJECT move to <<'EOF' pass via stdin THE CHECK RUNS BEFORE THE SHELL DOES
Fig. 3 — every instruction file passes the lint before an agent can run it

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.

[← Blog]