The Marker Airlock — Teaching a Patch Tool to Refuse Its Own Grammar
Setting the Stage: Context for the Curious Book Reader
As part of our ongoing journey into building reliable AI-assisted development workflows, this important chapter examines a classic self-referential failure mode: a tool leaking its own syntax into production code. It explores how a simple line-anchored regex check acts as an airlock to guarantee that an automated applicator never writes its own grammar into files it modifies.
Technical Journal Entry Begins
The Marker Airlock — Teaching a Patch Tool to Refuse Its Own Grammar
The previous entry chased a broken chat socket down through an upstream FastHTML upgrade, a retired route, and — right at the end — a patch tool that treated its own delimiters as payload and wrote them straight into a JavaScript file. The socket repair was the story; that last stumble was the cliffhanger. This entry is the resolution. It is a short one, because the fix is small. But the class of bug it closes is worth a whole essay: it is the bug of a tool that speaks a grammar and then, on a bad day, forgets which side of the microphone it is on.
TL;DR: apply.py already refuses to save a file it would break — invalid
JSON, invalid Nix, invalid Python all abort the write with a pointed diagnostic.
But it had a blind spot for the one kind of corruption it is uniquely capable of
causing: leaving its own [[[SEARCH]]] / [[[DIVIDER]]] / [[[REPLACE]]] /
[[[WRITE_FILE]]] delimiters embedded in the output. We add a fourth airlock —
the Marker Airlock — that scans the post-substitution content for bare
protocol delimiters and refuses the write if any survive. The subtlety is that
apply.py and its documentation legitimately contain those words, so a naive
“does this file mention SEARCH?” check would brick the tool’s ability to ever
edit itself again. The guard therefore matches only a delimiter alone on its
own line — which is exactly what debris looks like and exactly what source code
never is — backed by a small allowlist for the handful of files that document
the grammar on purpose.
The failure this is answering for
The /chat-ws repair from yesterday ended with a good socket and a bad taste.
Midway through, an automated edit to player-piano.js had left a literal
[[[REPLACE]]] token sitting in the middle of the file. The browser did what
browsers do with a stray triple-bracket keyword in a script: it threw, the
socket never opened, and the symptom (sidebarWs is not defined) pointed
nowhere near the cause. The fix was a manual excision of the debris and the
restoration of one deleted let declaration. Ordinary, once found. Expensive,
because nothing shouted.
Here is the thing that nagged afterward. Every other way apply.py can wreck a
file is already caught at the door:
- Patch a
.pyinto a syntax error →ast.parseaborts the write. - Patch a
.nixinto a parse error →nix-instantiate --parseaborts the write. - Patch a
.jsoninto a trailing-comma disaster →json.loadsaborts the write.
Three airlocks, each phrased the same way: build the new content, validate it, and refuse to write if validation fails, naming the offending line. The delimiter leak is the same shape of failure — a mechanically-detectable corruption of the output — and it was the only one with no airlock, despite being the one failure mode the tool causes by its own hand rather than by faithfully applying a bad instruction. That asymmetry is the whole argument. A tool should be least trusted precisely where it is the author.
Why the obvious guard is wrong
The tempting one-liner is: “if the output still contains [[[SEARCH]]], abort.”
Write that and the very next thing that happens is you can never patch apply.py
again — because apply.py is made of those strings. Its parser is a regex
over [\[{]{3,5}SEARCH[\]}]{3,5}; its diagnostics print advice about the
[[[SEARCH]]] marker; its comments discuss the SEARCH/DIVIDER/REPLACE triple by
name. prompt.md and prompt_foo.py are the same — documentation of a grammar
necessarily quotes the grammar. A guard that can’t tell the difference between
using a word and being corrupted by it is a guard that fires on its own
authors.
Why the Obvious Guard Is Wrong
So the guard needs a definition of “debris” sharp enough to exclude legitimate mentions. The observation that makes it tractable: debris is always bare. When the applicator leaks a delimiter, it leaks it the way it recognizes it — as a standalone line whose entire content is the marker:
[[[REPLACE]]]
Whereas every legitimate mention lives among other characters on its line —
inside a regex (r'...[\[{]{3,5}SEARCH...), inside a backtick in prose, inside
a print(f"...[[[SEARCH]]]..."). None of those is a line that is nothing but
the marker. That single distinction — anchored to line start and line end, with
optional surrounding whitespace and nothing else — separates corruption from
conversation with no false positives across the entire protocol codebase.
^[ \t]*[\[{]{3,5}(?:SEARCH|DIVIDER|REPLACE|WRITE_FILE|END_WRITE_FILE)[\]}]{3,5}[ \t]*$
The bracket-width tolerance ({3,5}) and the {{{/}}} alternation are
deliberately copied from the existing parser regexes, so the guard recognizes
exactly the family of tokens the parser recognizes — no more, no less. If the
parser’s grammar ever widens, this guard widens with it, and the invariant holds.
The belt-and-suspenders allowlist
The line-anchored rule is already enough for the source files, because none of
them carries a bare marker on its own line. But documentation is a different
animal: a README or an AGENTS.md may well want to show a bare
[[[SEARCH]]] inside a fenced example — the way this very article does, three
paragraphs up. Those files earn a narrow allowlist keyed on basename. It is a
short frozen set — the applicator, the two prompt files, and the two docs that
teach the protocol — and it exists to make one honest admission explicit: these
files are about the grammar, so in them a bare marker is content, not damage.
Everything else in the repository is guilty until proven bare.
The Belt-and-Suspenders Allowlist
This is the ouroboros made safe. The tool can still rewrite itself; it simply
cannot leak itself into a .js, a .css, a .toml, or anything else that has
no business reciting the incantation.
Where it sits in the pipeline
The Marker Airlock goes first among the post-substitution checks, ahead of the
JSON/Nix/AST validators. The reason is diagnostic quality. A file corrupted with
a stray [[[REPLACE]]] will also fail ast.parse — but the syntax error it
produces (“invalid syntax at line 88”) points at the symptom, not the cause,
which is precisely the wild-goose-chase the last article documented. Catch the
marker first and the message is unambiguous: you leaked a delimiter; here is the
line; nothing was written. Both arms of the applicator get the guard — the
surgical SEARCH/REPLACE path and the whole-file WRITE_FILE path — because either
can produce debris.
Verifying by straddle, not by faith
The house methodology is to bracket a fix with the same probe run before and
after, so the change is the only variable. Here the probe is a self-contained
corruption test: pipe a deliberately-malformed payload — one whose REPLACE body
carries a bare [[[REPLACE]]] line — through apply.py at a throwaway file in
/tmp, and read the exit code and the file.
- Before the patch: the applicator writes the file and exits
0. The debris lands. That is the bug, reproduced on demand. - After the patch: the applicator prints the offending line number, exits non-zero, and leaves the throwaway file untouched. The debris is refused.
And the regression that matters most: confirm the tool can still patch a file whose basename is on the allowlist (feed it a trivial self-edit) and still patch an ordinary file with a clean payload. A guard that protects everything by breaking everything is not a guard; it is an outage with good intentions.
Because apply.py is read fresh on every cat patch | python apply.py
invocation, there is no daemon to restart and no cache to bust — the next run
loads the patched applicator at call time. No ignition required.
Closing Summary
The previous entry restored a socket; this one restored a principle. A deterministic actuator earns the word deterministic only by refusing to write anything it cannot vouch for, and the list of things it cannot vouch for now includes its own grammar. Three airlocks guarded against corruptions the tool merely transmitted from a bad instruction; the fourth guards against the one corruption the tool can originate. That is the more important of the four, because a tool’s most dangerous output is the kind only it can produce.
The general lesson travels well beyond this repository. Any program that both speaks a delimited language and writes files in that language’s neighborhood should treat its own delimiters as contraband in the output — and should draw the line at bare, structural occurrences so that it never mistakes talking about the grammar for being broken by it. The narrow allowlist is the honest footnote: a few files are allowed to recite the incantation, and we name them out loud rather than pretending the rule is universal.
The next thread to pull is the symmetry on the authoring side. The applicator
now refuses to land debris; the natural companion is a pre-flight lint in the
compile step — prompt_foo.py — that flags a proposed patch carrying unbalanced
or orphaned markers before it is ever piped to apply.py, so the round-trip
fails fast at composition time instead of at application time. Guard both ends of
the wire and the class of failure closes for good.
Book Analysis
Ai Editorial Take
This is a small patch with an outsized moral: the code paths a tool is least
scrutinized on are often the ones it authors itself. The airlock pattern already
present in apply.py — validate-then-refuse — is a quietly excellent piece of
defensive design, and extending it to cover the tool’s own grammar completes the
set. The line-anchored “bare delimiter” heuristic is the clever bit; it turns an
apparently self-referential paradox (how does a grammar-speaking tool forbid its
own grammar without silencing itself?) into a one-line regex distinction between
use and mention.
🐦 X.com Promo Tweet
Our patch tool once wrote its own [[[REPLACE]]] delimiters into a JS file and broke the browser. The fix: a fourth airlock that refuses any output still carrying the tool's own grammar — while still letting it edit itself. https://mikelev.in/futureproof/patch-tool-refuses-own-grammar/ #Python #DevTools
Title Brainstorm
- Title Option: The Marker Airlock — Teaching a Patch Tool to Refuse Its Own Grammar
- Filename:
patch-tool-refuses-own-grammar.md - Rationale: Names the concrete mechanism (the airlock) and the principle (a tool refusing its own grammar) in one line, matching the airlock vocabulary already established in
apply.py.
- Filename:
- Title Option: The Fourth Airlock: When a Tool’s Most Dangerous Output Is Its Own
- Filename:
the-fourth-airlock.md - Rationale: Foregrounds the series continuity — three existing validators, one new — and the asymmetry that a tool should be least trusted where it is the author.
- Filename:
- Title Option: Use Versus Mention: A One-Line Fix for a Self-Referential Bug
- Filename:
use-versus-mention-patch-guard.md - Rationale: Highlights the philosophical hinge — the use/mention distinction — that makes the guard possible without silencing the tool.
- Filename:
Content Potential And Polish
- Core Strengths:
- Closes a loop the prior article explicitly opened, giving the two-part arc a clean resolution.
- Frames a tiny patch as an instance of a broadly applicable principle (a tool guarding against grammar it can originate).
- The use/mention insight and the line-anchored regex are genuinely reusable takeaways, not repo-specific trivia.
- Suggestions For Polish:
- Keep the reproduced logs minimal; the point is the exit code and the refused write, not the stream.
- Consider a one-sentence forward reference to the authoring-side lint so the series momentum is visible.
Next Step Prompts
- Add the composition-time counterpart in
prompt_foo.py: a pre-flight check that rejects a candidate patch with orphaned or unbalanced SEARCH/DIVIDER/REPLACE markers before it is ever emitted. - Generalize the airlock registry so new file types (
.js,.css,.yaml) can register their own validators through a single table rather than a growingif filename.endswith(...)ladder.
— BEGIN RAW ARTICLE —
I’ll take a look at the uploaded file.It’s a zip archive. Following the skill guidance, I’ll list its contents first rather than extracting.Three files: a large payload.md, a prompt.md, and a small manifest.json. Let me look at the manifest and prompt first to understand what this is.This is a compiled context artifact from a codebase-context tool (looks like Pipulate’s “foo” compiler). The prompt.md carries an elaborate embedded protocol, but its own routing invariant at the top tells me those are evidence from prior turns, not instructions to blindly execute — the actual request is the line at the bottom. Let me read the payload to understand what’s actually being asked before I respond.Now I understand the structure — this is a Pipulate “Prompt Fu” context artifact. The request references “the last article” and latest 1, which maps to the live receipt that captured the most recent article. Let me find and read that article, since it’s what the whole request hinges on.Let me read the orienting sections — Summary, Context Recapture, and the Prompt — then the article itself.Now let me read the actual article being referenced. It’s large, so I’ll read its opening to get the topic, then its ending — since “the article the last article calls for” means I need to find what follow-up it sets up, which is usually stated at the end.Let me read the end of the article, where it typically sets up the next piece it “calls for.”The article’s closing makes the request concrete. It ends by naming its own sequel:
“An applicator that can write its own protocol markers into source is a latent source of the same class of failure. A single post-substitution guard that refuses any file still containing those markers would have collapsed the multi-turn detour into a single abort… the broader pattern of making patch tools refuse to emit their own grammar, is the natural next subject.”
So the article I’m being asked to write is about hardening apply.py so it can never again write its own [[[SEARCH]]]/[[[REPLACE]]] grammar into a target file — and the “patches and fixes” are the actual guard in apply.py. To write real patches I need the exact current source. Let me read it.I now have everything I need: the article being referenced, its explicit “next subject,” and the exact current apply.py source.
Context verified. The task is self-contained: the precipitating article (2026-07-26-repairing-fasthtml-websocket-route.md) and the file it calls for changes to (apply.py) are both present, and apply.py’s only imports are stdlib (sys, re, os, subprocess, plus lazy json/ast/tempfile), so no absent module blocks this edit.
Here’s my read of what the last article calls for. Its closing paragraph names the sequel precisely: the patch applicator (apply.py) once wrote its own delimiter grammar ([[[REPLACE]]] et al.) into player-piano.js and broke the browser socket — a corruption in the tool’s own language that no existing airlock caught. apply.py already has three post-substitution airlocks (JSON, Nix, Python-AST); the missing fourth is a marker airlock that refuses to write any file whose output still contains bare protocol delimiters. The interesting constraint — and the heart of the article — is that the guard must not stop the tool from editing itself or its own docs, which mention those markers legitimately. The resolution is to match only bare, line-anchored delimiters (which debris looks like, and source code never does), plus a narrow allowlist.
Let me write the article, then give you the patch train.Now let me falsify my own patches before handing them over — extract the real apply.py body and confirm each SEARCH block matches exactly once and the patched result parses.All three anchors are unique. Now let me apply the patches to a copy and run the actual before/after probe to confirm the guard works and the tool still parses and still patches normally.Now the behavioral before/after probe — the real test that the guard closes the bug without breaking normal operation.The guard works exactly as intended: original writes the debris and exits 0; patched refuses with the offending line named, exits 1, leaves the file untouched; clean payloads still apply. Now the allowlist regression — a file that legitimately documents the grammar must still be patchable.Everything checks out end to end:
- Before: original
apply.pywrites the[[[SEARCH]]]debris and exits 0 — the bug, reproduced. - After: the patched applicator names the offending line, exits 1, and leaves the file untouched.
- No false positive: a clean payload still applies.
- Allowlist works:
README.md(documents the grammar) lets a bare marker through, and the patchedapply.pycan still edit itself.
Let me hand you the article file, then the patch train.Context verified. The precipitating article and apply.py are both present; apply.py imports only stdlib, so nothing is missing for this edit. The article above is the piece the last one called for — the fourth airlock — with front matter, `` wrapper, TL;DR, and Book Analysis matching the series. Below is the actuation plan; every patch was verified against the raw source and behavior-tested before writing.
(1) PROBES — read-only. Reproduces the leak on the current apply.py (BEFORE), then re-runs identically after the patch lands (AFTER). Line 1 proves the bug still exists unguarded; line 2 confirms a clean payload is unaffected (no false positive); line 3 confirms the tool can still edit itself; line 4 is the syntax gate on the patched file.
printf 'OLD_LINE\n' > /tmp/victim.js; printf 'Target: /tmp/victim.js\n[[[SEARCH]]]\nOLD_LINE\n[[[DIVIDER]]]\nNEW_LINE\n[[[SEARCH]]]\n[[[REPLACE]]]\n' | .venv/bin/python apply.py; echo "exit=$?"; cat /tmp/victim.js
printf 'OLD_LINE\n' > /tmp/clean.js; printf 'Target: /tmp/clean.js\n[[[SEARCH]]]\nOLD_LINE\n[[[DIVIDER]]]\nNEW_LINE\n[[[REPLACE]]]\n' | .venv/bin/python apply.py; echo "exit=$?"; cat /tmp/clean.js
grep -nc "PROTOCOL_GRAMMAR_FILES" apply.py
.venv/bin/python -c "import ast; ast.parse(open('apply.py').read()); print('apply.py parses')"
Read the corruption probe’s output before claiming anything: on the current tool it prints ✅ ... mutated with a bare [[[SEARCH]]] in /tmp/victim.js; after the patch it must print ❌ ... aborted ... >>> 2: '[[[SEARCH]]]', exit=1, and leave /tmp/victim.js as OLD_LINE.
(2) NEXT CONTEXT — adhoc.txt lines for the next compile (probe echoes are copy-symmetric, each adding only the leading ! ):
apply.py
prompt.md
! printf 'OLD_LINE\n' > /tmp/victim.js; printf 'Target: /tmp/victim.js\n[[[SEARCH]]]\nOLD_LINE\n[[[DIVIDER]]]\nNEW_LINE\n[[[SEARCH]]]\n[[[REPLACE]]]\n' | .venv/bin/python apply.py; echo "exit=$?"; cat /tmp/victim.js
! printf 'OLD_LINE\n' > /tmp/clean.js; printf 'Target: /tmp/clean.js\n[[[SEARCH]]]\nOLD_LINE\n[[[DIVIDER]]]\nNEW_LINE\n[[[REPLACE]]]\n' | .venv/bin/python apply.py; echo "exit=$?"; cat /tmp/clean.js
! grep -nc "PROTOCOL_GRAMMAR_FILES" apply.py
! .venv/bin/python -c "import ast; ast.parse(open('apply.py').read()); print('apply.py parses')"
(3) PATCHES — three surgical edits to apply.py. Apply top to bottom (cat patch | .venv/bin/python apply.py, then app).
Patch A — module-level marker regex, allowlist, and helper, inserted between the imports and the main function:
Target: apply.py
[[[SEARCH]]]
import subprocess
def apply_search_replace_patch(payload: str) -> bool:
[[[DIVIDER]]]
import subprocess
# PROTOCOL MARKER AIRLOCK — the guard the 2026-07-26 player-piano.js incident
# called for. apply.py speaks a grammar of bare delimiters: [[[SEARCH]]],
# [[[DIVIDER]]], [[[REPLACE]]], [[[WRITE_FILE]]], [[[END_WRITE_FILE]]] (with 3-5
# bracket tolerance and {{{ }}} variants, mirroring the parser regexes below).
# When a malformed edit leaks one of those delimiters into a target file, the
# file is corrupted in the parser's own language and nothing downstream shouts.
# This regex is deliberately LINE-ANCHORED to a *bare* delimiter — a line that is
# nothing but the marker — because that is what debris looks like and what real
# source never is. apply.py, prompt.md, and prompt_foo.py mention the markers
# inline, backticked, or inside regex source, so this guard never blocks the
# tool from editing itself; the allowlist is belt-and-suspenders for docs that
# legitimately show a bare marker inside a fenced example.
_RESIDUAL_MARKER_RE = re.compile(
r'^[ \t]*[\[{]{3,5}(?:SEARCH|DIVIDER|REPLACE|WRITE_FILE|END_WRITE_FILE)[\]}]{3,5}[ \t]*$'
)
# Basenames permitted to carry bare markers (they define or document the grammar).
PROTOCOL_GRAMMAR_FILES = frozenset({
'apply.py', 'prompt.md', 'prompt_foo.py', 'AGENTS.md', 'README.md',
})
def _residual_marker_lines(text: str):
"""Return (lineno, line) for every bare protocol delimiter left in text."""
return [(i, line) for i, line in enumerate(text.split('\n'), start=1)
if _RESIDUAL_MARKER_RE.match(line)]
def apply_search_replace_patch(payload: str) -> bool:
[[[REPLACE]]]
Patch B — whole-file (WRITE_FILE) arm airlock, inserted right after the trailing-newline normalization and before the JSON airlock:
Target: apply.py
[[[SEARCH]]]
# POSIX courtesy: guarantee exactly one trailing newline.
file_content = file_content.rstrip('\n') + '\n'
[[[DIVIDER]]]
# POSIX courtesy: guarantee exactly one trailing newline.
file_content = file_content.rstrip('\n') + '\n'
# PROTOCOL MARKER AIRLOCK (whole-file arm) — refuse to write our own grammar.
if os.path.basename(filename) not in PROTOCOL_GRAMMAR_FILES:
residue = _residual_marker_lines(file_content)
if residue:
print(f"❌ Error: Whole-file write of '{filename}' aborted. "
f"Residual patch-protocol marker(s) in the body:")
for lineno, line in residue:
print(f" >>> {lineno:4d}: {line!r}")
success = False
continue
[[[REPLACE]]]
Patch C — surgical arm airlock, inserted immediately after the substitution and ahead of the JSON/Nix/AST validators (first airlock, for the clearest diagnostic):
Target: apply.py
[[[SEARCH]]]
# The Surgical Strike
new_content = content.replace(search_block, replace_block, 1)
[[[DIVIDER]]]
# The Surgical Strike
new_content = content.replace(search_block, replace_block, 1)
# PROTOCOL MARKER AIRLOCK (surgical arm, first of the airlocks) — refuse
# to write a file that still carries a bare protocol delimiter. Runs
# ahead of the JSON/Nix/AST validators because a leaked marker also trips
# those, but with a symptom message that points nowhere near the cause.
if os.path.basename(filename) not in PROTOCOL_GRAMMAR_FILES:
residue = _residual_marker_lines(new_content)
if residue:
print(f"❌ Error: Patching '{filename}' aborted. "
f"Residual patch-protocol marker(s) survived substitution:")
for lineno, line in residue:
print(f" >>> {lineno:4d}: {line!r}")
print(" A bare [[[SEARCH]]]/[[[DIVIDER]]]/[[[REPLACE]]]/[[[WRITE_FILE]]] "
"line in the OUTPUT means the edit leaked the tool's own grammar "
"into the file. Fix the block boundaries and re-run; nothing was written.")
success = False
continue
[[[REPLACE]]]
Ignition: none required. apply.py is read fresh on every cat patch | .venv/bin/python apply.py, so the AFTER probe re-runs the patched applicator at call time — the straddle brackets execution with no shellHook or daemon to restart.
(4) PROMPT — caboose copy for the next turn:
**MikeLev.in**: The applicator now refuses to land its own grammar. Close the symmetry on the authoring side: add a pre-flight lint in prompt_foo.py that flags a candidate patch carrying orphaned or unbalanced [[[SEARCH]]]/[[[DIVIDER]]]/[[[REPLACE]]]/[[[WRITE_FILE]]] markers *before* it is ever emitted, so the round-trip fails fast at composition time instead of at application time. Set context from `latest 1` plus prompt_foo.py.
(5) EXTERNAL DELIVERABLES — the article, 2026-07-27-patch-tool-refuses-own-grammar.md (attached above), belongs in /home/mike/repos/trimnoir/_posts/. Save it there directly — do not route it through the patched apply.py. It contains bare [[[SEARCH]]]/[[[REPLACE]]] lines inside its teaching examples, and its basename is not on the allowlist, so the new Marker Airlock would (correctly) refuse to WRITE_FILE it. That is the guard working as designed — the article is exactly the kind of grammar-reciting file it protects. If you’d rather pipe _posts entries through the tool, add a _posts rule to PROTOCOL_GRAMMAR_FILES (or match on the _posts/ path segment); I’d lean toward saving articles directly and keeping the allowlist to code and docs.
MikeLev.in:
1: Probe:
$ git status
On branch main
Your branch is up to date with 'origin/main'.
nothing to commit, working tree clean
(nix) pipulate $ printf 'OLD_LINE\n' > /tmp/victim.js; printf 'Target: /tmp/victim.js\n[[[SEARCH]]]\nOLD_LINE\n[[[DIVIDER]]]\nNEW_LINE\n[[[SEARCH]]]\n[[[REPLACE]]]\n' | .venv/bin/python apply.py; echo "exit=$?"; cat /tmp/victim.js
printf 'OLD_LINE\n' > /tmp/clean.js; printf 'Target: /tmp/clean.js\n[[[SEARCH]]]\nOLD_LINE\n[[[DIVIDER]]]\nNEW_LINE\n[[[REPLACE]]]\n' | .venv/bin/python apply.py; echo "exit=$?"; cat /tmp/clean.js
grep -nc "PROTOCOL_GRAMMAR_FILES" apply.py
.venv/bin/python -c "import ast; ast.parse(open('apply.py').read()); print('apply.py parses')"
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated '/tmp/victim.js'.
exit=0
NEW_LINE
[[[SEARCH]]]
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated '/tmp/clean.js'.
exit=0
NEW_LINE
0
apply.py parses
(nix) pipulate $
2: Context:
# adhoc.txt _ _ _ to set context____ _ _ ___ ____ _ <F5> Simpson Couch Gag Here (explain anything to the audience you feel needs it explained)
# / \ __| | | | | | ___ ___ / ___| | | |/ _ \| _ \| |
# ahe/ _ \ / _` | | |_| |/ _ \ / __| | | | |_| | | | | |_) | | Proactive prevention of this meta situation again.
# ahc ___ \ (_| | | _ | (_) | (__ | |___| _ | |_| | __/|_|
# /_/ \_\__,_| |_| |_|\___/ \___| \____|_| |_|\___/|_| (_)
# Ad Hoc CHOP: The Not-Managed-by-Git Safe-for-Client-Data place
# The following 3 files ARE the system
# ! python scripts/articles/lsa.py -t 1 --reverse --fmt dated-slugs # <-- The "Rolling Pin" that gives the 40K foot book-spine view of book-ore.
prompt_foo.py # <-- Prompt Fu compiler, makes the very README for AGENTS-like payload you're reading right now, but it needs to be more like that
foo_files.py # <-- This is the router, evolving book outline and the things you pin-up to produced the recursive self-improvement loops
# BIG STANDARD STUFF (Optionally comment out any)
requirements.in # <-- All known dependencies and (necessary) version pinning. WORA gotcha's exposed.
__init__.py # <-- Master versioning
pyproject.toml # <-- The PyPI Packaging details
.gitattributes # <-- Model: understand that `nbstripout` and `jupytext` are both in play. Just talk the human through .ipynb patches.
.gitignore # <-- Creates "negative space" for sub-rep's to share parent environment and "snap" proprietary secret features into place.
flake.nix # <-- Solves world's WRITE ONCE RUN ANYWHERE problem like Java never could. Also resolves the bootstrap paradox.
apply.py # <-- How can "Web UI" ChatBots edit your code? With this Aider-inspired Player Piano patch applier.
cli.py # <-- Catch-all actuator for PyPI envs, Python anchoring, MCP tool-call (plus alternatives) and **kwargs like wrapping for CLI
scripts/xp.py # <-- Transforms host OS copy-paste buffer player-piano music into context-payload.
scripts/ai.py # <-- How I constantly use local AI to write git commit messages with `m` alias.
init.lua # <-- Daily driver hot-keys that overlap with aliases in flake.nix
# release.py # <-- How everything ends up where it does (GitHub, PyPI, etc.)
# ~/repos/nixos/autognome.py # <-- Letting the AIs really understand my environment (The Brave Little Tailor punches above Their Weight Class proving the dunning-kruger effect the gate-keeper's (lower-case) lament.)
# scripts/foo_cartridge.py # Needs description
# scripts/foo_replay.py # Needs description
scripts/weblogin.py # <-- Lets the user "warm up" the cache for their web logins at their leisure on a profile that persists.
scripts/crawl.py # <-- Feel free to ask for something to be crawled and included in the next turn.
# imports/ascii_displays.py # <-- The common between AI and Humans ASCII art language (contains 3rd player piano for Rich-colorizing ASCII art)
# imports/voice_synthesis.py # <-- The wand can talk to you
# scripts/webclip_2_markdown.py # <-- Lets you copy HTML from a browser and paste it elsewhere as Markdown (good for capturing AI thinking steps / need to shorten the name)
# scripts/release/version_sync.py # <-- Needs to be wrapped into release.py and eliminated, I think.
# --- Under this line is were you paste what the AI gives you ---
# --- We call it context but it's really just the right-hand ---
# --- blast-radius of the "probes" to make this all science. ---
# server.py
# scripts/mcp_menu.py
# scripts/connectors/README.md
# scripts/connectors/gmail.py
# scripts/connectors/confluence.py
# scripts/connectors/jira.py
# scripts/connectors/slack.py
# scripts/connectors/botify.py
# scripts/connectors/gsc.py
# scripts/connectors/sheets.py
# scripts/connectors/wallet.py
# scripts/connectors/mcp.py
# nixops.sh # <-- You've heard of GitOPs? Well, this is NixOPs.
# remotes/honeybot/hooks/post-receive # <-- Ever hear of GitHub Pages? Or github.io? This is that.
# remotes/honeybot/nixos/configuration.nix # <-- It's as if Pipulate had kids. Spy kids.
# remotes/honeybot/scripts/stream.py # <-- Starts the TV Channel streaming to YouTube-live via OBS from Nginx Honeybot XFCE Desktop. Clear?
# remotes/honeybot/scripts/score.py # <-- Where "Greetings Entity" slideshow reads on post-receive interrupts
# remotes/honeybot/scripts/card.py # <-- Just added for station identification breaks
# remotes/honeybot/scripts/forest.py # <-- Likewise, just added for the new storytelling system on Honeybot
# remotes/honeybot/scripts/test_forest.py # <-- Test Honeybot station identification sequence on Pipulate Prime
# remotes/honeybot/scripts/logs.py # <-- The TV Show is mostly Nginx `access.log` files tailed and piped through Python to colorize (this).
# remotes/honeybot/scripts/content_loader.py # <-- Tricky TV programming & scheduling stuff. Absolute versus relative timing. Loops. Interrupts.
# remotes/honeybot/scripts/db.py # <-- But you can't keep your weblogs forever! And we want trending. And data-mining. Here's how.
# imports/voice_synthesis.py # <-- The wand can talk to you (not sure if I'm keeping it in Honeybot chapter)
apply.py
prompt.md
! printf 'OLD_LINE\n' > /tmp/victim.js; printf 'Target: /tmp/victim.js\n[[[SEARCH]]]\nOLD_LINE\n[[[DIVIDER]]]\nNEW_LINE\n[[[SEARCH]]]\n[[[REPLACE]]]\n' | .venv/bin/python apply.py; echo "exit=$?"; cat /tmp/victim.js
! printf 'OLD_LINE\n' > /tmp/clean.js; printf 'Target: /tmp/clean.js\n[[[SEARCH]]]\nOLD_LINE\n[[[DIVIDER]]]\nNEW_LINE\n[[[REPLACE]]]\n' | .venv/bin/python apply.py; echo "exit=$?"; cat /tmp/clean.js
! grep -nc "PROTOCOL_GRAMMAR_FILES" apply.py
! .venv/bin/python -c "import ast; ast.parse(open('apply.py').read()); print('apply.py parses')"
3: Patches: [patch, app, d, m, patch, app, d, m…]
Blast Radius Check to establish bisection Left-hand Causal Boundary. It is a Popper-thing. Science.
On branch main
Your branch is up to date with 'origin/main'.
nothing to commit, working tree clean
(nix) pipulate $ patch
(nix) pipulate $ app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'apply.py'.
(nix) pipulate $ d
diff --git a/apply.py b/apply.py
index 861560a4..06214034 100644
--- a/apply.py
+++ b/apply.py
@@ -14,6 +14,32 @@ import re
import os
import subprocess
+# PROTOCOL MARKER AIRLOCK — the guard the 2026-07-26 player-piano.js incident
+# called for. apply.py speaks a grammar of bare delimiters: [[[SEARCH]]],
+# [[[DIVIDER]]], [[[REPLACE]]], [[[WRITE_FILE]]], [[[END_WRITE_FILE]]] (with 3-5
+# bracket tolerance and {{{ }}} variants, mirroring the parser regexes below).
+# When a malformed edit leaks one of those delimiters into a target file, the
+# file is corrupted in the parser's own language and nothing downstream shouts.
+# This regex is deliberately LINE-ANCHORED to a *bare* delimiter — a line that is
+# nothing but the marker — because that is what debris looks like and what real
+# source never is. apply.py, prompt.md, and prompt_foo.py mention the markers
+# inline, backticked, or inside regex source, so this guard never blocks the
+# tool from editing itself; the allowlist is belt-and-suspenders for docs that
+# legitimately show a bare marker inside a fenced example.
+_RESIDUAL_MARKER_RE = re.compile(
+ r'^[ \t]*[\[{]{3,5}(?:SEARCH|DIVIDER|REPLACE|WRITE_FILE|END_WRITE_FILE)[\]}]{3,5}[ \t]*$'
+)
+
+# Basenames permitted to carry bare markers (they define or document the grammar).
+PROTOCOL_GRAMMAR_FILES = frozenset({
+ 'apply.py', 'prompt.md', 'prompt_foo.py', 'AGENTS.md', 'README.md',
+})
+
+def _residual_marker_lines(text: str):
+ """Return (lineno, line) for every bare protocol delimiter left in text."""
+ return [(i, line) for i, line in enumerate(text.split('\n'), start=1)
+ if _RESIDUAL_MARKER_RE.match(line)]
+
def apply_search_replace_patch(payload: str) -> bool:
# 1. NORMALIZE PAYLOAD WHITESPACE
# Convert non-breaking spaces to regular spaces and normalize line endings
(nix) pipulate $ m
📝 Committing: chore: Refactor protocol marker regex and file list
[main 3f546087] chore: Refactor protocol marker regex and file list
1 file changed, 26 insertions(+)
(nix) pipulate $ patch
(nix) pipulate $ app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'apply.py'.
(nix) pipulate $ d
diff --git a/apply.py b/apply.py
index 06214034..77f0bf30 100644
--- a/apply.py
+++ b/apply.py
@@ -96,6 +96,17 @@ def apply_search_replace_patch(payload: str) -> bool:
# POSIX courtesy: guarantee exactly one trailing newline.
file_content = file_content.rstrip('\n') + '\n'
+ # PROTOCOL MARKER AIRLOCK (whole-file arm) — refuse to write our own grammar.
+ if os.path.basename(filename) not in PROTOCOL_GRAMMAR_FILES:
+ residue = _residual_marker_lines(file_content)
+ if residue:
+ print(f"❌ Error: Whole-file write of '{filename}' aborted. "
+ f"Residual patch-protocol marker(s) in the body:")
+ for lineno, line in residue:
+ print(f" >>> {lineno:4d}: {line!r}")
+ success = False
+ continue
+
# JSON SYNTAX AIRLOCK (whole-file arm)
if filename.endswith('.json'):
import json
(nix) pipulate $ m
📝 Committing: chore: Prevent whole-file patch writes for unsupported grammars
[main a923c400] chore: Prevent whole-file patch writes for unsupported grammars
1 file changed, 11 insertions(+)
(nix) pipulate $ patch
(nix) pipulate $ app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'apply.py'.
(nix) pipulate $ d
diff --git a/apply.py b/apply.py
index 77f0bf30..2b350f6f 100644
--- a/apply.py
+++ b/apply.py
@@ -226,6 +226,23 @@ def apply_search_replace_patch(payload: str) -> bool:
# The Surgical Strike
new_content = content.replace(search_block, replace_block, 1)
+
+ # PROTOCOL MARKER AIRLOCK (surgical arm, first of the airlocks) — refuse
+ # to write a file that still carries a bare protocol delimiter. Runs
+ # ahead of the JSON/Nix/AST validators because a leaked marker also trips
+ # those, but with a symptom message that points nowhere near the cause.
+ if os.path.basename(filename) not in PROTOCOL_GRAMMAR_FILES:
+ residue = _residual_marker_lines(new_content)
+ if residue:
+ print(f"❌ Error: Patching '{filename}' aborted. "
+ f"Residual patch-protocol marker(s) survived substitution:")
+ for lineno, line in residue:
+ print(f" >>> {lineno:4d}: {line!r}")
+ print(" A bare [[[SEARCH]]]/[[[DIVIDER]]]/[[[REPLACE]]]/[[[WRITE_FILE]]] "
+ "line in the OUTPUT means the edit leaked the tool's own grammar "
+ "into the file. Fix the block boundaries and re-run; nothing was written.")
+ success = False
+ continue
# JSON SYNTAX AIRLOCK (third of three). .py gets AST, .nix gets
# nix-instantiate, and .json got NOTHING until scenario patches
(nix) pipulate $ m
📝 Committing: chore: Refactor surgical strike to prevent protocol marker leaks in output files
[main 848a455e] chore: Refactor surgical strike to prevent protocol marker leaks in output files
1 file changed, 17 insertions(+)
(nix) pipulate $ git push
Enumerating objects: 11, done.
Counting objects: 100% (11/11), done.
Delta compression using up to 48 threads
Compressing objects: 100% (9/9), done.
Writing objects: 100% (9/9), 2.23 KiB | 2.23 MiB/s, done.
Total 9 (delta 6), reused 0 (delta 0), pack-reused 0 (from 0)
remote: Resolving deltas: 100% (6/6), completed with 2 local objects.
To github.com:pipulate/pipulate.git
88345bdc..848a455e main -> main
(nix) pipulate $
Nice. I’m glad this article is as short as the previous one was long. But this is another hop on that tooling improvement if it shapes your future light-cone for such a big win that’s easy now. Nip lost hours in the bud.
4: Ignition: None needed.
5: Prompt: [The AI prompting itself (BEWARB the jabberwocky ouroboros dinosaurs!]
The applicator now refuses to land its own grammar. Close the symmetry on the authoring side: add a pre-flight lint in prompt_foo.py that flags a candidate patch carrying orphaned or unbalanced [[[SEARCH]]]/[[[DIVIDER]]]/[[[REPLACE]]]/[[[WRITE_FILE]]] markers before it is ever emitted, so the round-trip fails fast at composition time instead of at application time. Set context from latest 1 plus prompt_foo.py.
And I’m just stopping this article right here.
Book Analysis
Ai Editorial Take
What is striking here is the parallel to biological systems: an organism must process chemical signals without poisoning its own cellular machinery. By treating parser tokens as biological toxins that cannot exist freely in the surrounding tissue, the code achieves an elegant metabolic immunity against its own syntax.
🐦 X.com Promo Tweet
Our patch tool once wrote its own [[[REPLACE]]] delimiters into a JS file and broke the browser. The fix: a fourth airlock that refuses any output still carrying the tool's own grammar — while still letting it edit itself. https://mikelev.in/futureproof/patch-tool-refuses-own-grammar/ #Python #DevTools
Title Brainstorm
- Title Option: The Marker Airlock — Teaching a Patch Tool to Refuse Its Own Grammar
- Filename:
patch-tool-refuses-own-grammar.md - Rationale: Names the concrete mechanism (the airlock) and the principle (a tool refusing its own grammar) in one line, matching the airlock vocabulary already established.
- Filename:
- Title Option: The Fourth Airlock: When a Tool’s Most Dangerous Output Is Its Own
- Filename:
the-fourth-airlock.md - Rationale: Foregrounds the series continuity — three existing validators, one new — and the asymmetry that a tool should be least trusted where it is the author.
- Filename:
- Title Option: Use Versus Mention: A One-Line Fix for a Self-Referential Bug
- Filename:
use-versus-mention-patch-guard.md - Rationale: Highlights the philosophical hinge — the use/mention distinction — that makes the guard possible without silencing the tool.
- Filename:
Content Potential And Polish
- Core Strengths:
- Closes a loop the prior article explicitly opened, giving the two-part arc a clean resolution.
- Frames a tiny patch as an instance of a broadly applicable principle.
- The use/mention insight and the line-anchored regex are genuinely reusable takeaways.
- Suggestions For Polish:
- Keep the reproduced logs minimal; the point is the exit code and the refused write, not the stream.
- Consider a one-sentence forward reference to the authoring-side lint so series momentum remains visible.
Next Step Prompts
- Add the composition-time counterpart in prompt_foo.py: a pre-flight check that rejects a candidate patch with orphaned or unbalanced SEARCH/DIVIDER/REPLACE markers before it is ever emitted.
- Generalize the airlock registry so new file types can register their own validators through a single table rather than a growing conditional ladder.