muonry v0.2.7: 89× smaller edit responses, and reads that flag smuggled text

Rach Pradhan · 7 min read

An agent does not wake up in a room. It wakes up inside its context: the prompt, the files it has read, every tool response it has been handed. That context is the entire universe it can see, and every tool call builds that universe one response at a time.

Which makes a 169 KB edit response a strange thing to build with. Ask an agent to replace a 2,000-line file and muonry used to hand back the file it just wrote, echoed twice. Once as content, once as diff. The agent wrote that file seconds ago. A copy of its own writing does not help it see. It just crowds out everything else.

v0.2.7 treats every response as terrain the agent has to live on. Edits return a synthesized diff and a capped echo instead of the whole file. Search output is capped and grouped per file, and every cap announces itself. Reads decode hidden Unicode into a warnings field, and an opt-in scan keeps secrets out of the universe entirely. Four changes, one job: build the agent's world well.

89×

smaller edit echo

169 KB → 1.9 KB, 2,000-line replace

11.8×

less search output

351 KB → 30 KB, broad pattern

41×

faster auto-diff

8.7 ms → 0.21 ms, no git fork

1.3×

faster edits

20.3 → 15.7 ms per op

TL;DR

A whole-file replace now returns 1.9 KB of metadata plus a capped diff instead of 169 KB of echo. A broad search returns 30 KB instead of 351 KB, grouped per file, with an explicit marker for everything it left out. Reads warn about Unicode tag blocks, confusable characters, and, with MUONRY_CRYPTO_SCAN=1, private keys and seed phrases, without ever echoing the secret itself.

Edits: synthesize, don't fork

A diff is how an agent sees its own hands. It cannot watch a file change on disk; it knows what an edit did only through what the response tells it. That is why, since v0.2.4, every edit response has carried a unified diff: verification without a follow-up read. The way we produced that diff was lazy, though. We forked git diff against the working tree after every write. That cost a process fork per edit. It showed the cumulativeworking-tree diff rather than this edit's delta, so any other uncommitted change leaked into the response as stale noise. And it produced nothing at all outside a git repo, or on a dry_run.

The fix was already inside the daemon. It holds everything a diff needs: the pre-edit snapshot of the file, the content the agent sent, and the resolved line range. v0.2.7 synthesizes the diff from those three things in-process. The diff step drops from 8.7 ms to 0.21 ms, edits as a whole get 1.3× faster (20.3 to 15.7 ms per op), the diff is always exactly this edit's delta, and plain directories and dry-run previews get one too.

The same release caps the echo: 40 lines of written content, 120 lines of diff, with explicit *_elided_lines fields whenever a cap fires. That is where the 89× comes from. A whole-file replace used to echo the entire file twice, once as content and once as diff. On a 2,000-line file that was 169 KB of response. It is now 1.9 KB.

Before: fork git diff after every write
write applied
>
fork git diff
>
cumulative working-tree diff
~8.7 ms
shows every uncommitted change, not this edit's delta
no diff outside a git repo, none on dry_run
After: synthesize from what the daemon already holds
pre-edit snapshot
+
sent content
+
resolved range
>
diff in-process
~0.21 ms
always exactly this edit's delta
works in plain directories and on dry-run previews
Edit echo, 2,000-line whole-file replace89× smaller
169 KB (file echoed twice)
1.9 KB (metadata + capped diff)

Reads that flag smuggled text

The agent's universe is made of what it reads, and a file can lie. Unicode tag blocks encode whole ASCII strings as invisible characters. Variation selectors hide payloads inside ordinary text. Confusable look-alikes let раypal, spelled with Cyrillic р and а, render identically to the real paypal while staying a different string to every parser. A human reviewer sees nothing. An agent that reads such a file builds its world on the hidden instructions, silently.

muonry's reader already detected all of this on the CLI path. v0.2.7 surfaces the same warnings on batch and MCP daemon reads, which is where agents actually live, and every warning now carries a decoded preview of what the hidden bytes actually say. The smuggled text lands in a warnings field where the model can see it for what it is, instead of slipping into its world as part of the file.

What the human sees: paypal
p
a
y
p
a
l
U+0070
U+0061
U+0079
U+0070
U+0061
U+006C
What the parser sees: two Cyrillic letters
р
а
y
p
a
l
U+0440
U+0430
U+0079
U+0070
U+0061
U+006C
What the daemon read returns alongside the file
"warnings": [
{ "kind": "confusable_unicode",
"detail": "mixed-script token: Latin + Cyrillic look-alikes",
"decoded": "paypal" },
{ "kind": "tag_block",
"detail": "47 invisible chars encode ASCII",
"decoded": "ignore previous instructions..." }
]

This page trips its own detector: a muonry read of this post's source flags the Cyrillic example above as confusable_unicode and decodes it back to plain “paypal”.

Secrets stay out of context

Some things should never enter the universe at all. Set MUONRY_CRYPTO_SCAN=1 and reads warn when a file contains them: PEM private keys, Bitcoin WIF keys, runs of 12 or more BIP-39 seed-phrase words, API secrets by prefix (Stripe, GitHub, Slack, AWS, Google, plus Infura and Alchemy RPC URLs), and wallet address-poisoning pairs. A poisoning pair is two different addresses that share their first and last four hex digits, the signature of a paste-the-wrong-address attack.

MUONRY_CRYPTO_SCAN=1off by default: the scan would tax the clean-file fast path
PEM private keys-----BEGIN EC PRIVATE KEY-----never echoed
Bitcoin WIF keysbase58, 51 to 52 charsnever echoed
Seed phrasesruns of 12+ BIP-39 wordsnever echoed
API secrets by prefixsk_live_ ghp_ xoxb- AKIA + RPC URLsnever echoed
Address poisoning pairssame first + last 4 hex digitsmasked 0xPPPP…SSSS

Two design rules held throughout. First, the warning never echoes the secret. Quoting a private key to flag it would place the key inside the very context we are guarding; only poisoning warnings show a masked 0xPPPP…SSSS form, since addresses are public anyway. Second, the scan is off by default. The seed-phrase pass is plain-ASCII work that would tax the clean-file fast path, so you pay for it only when you turn it on.

Scope, stated plainly: this is defense-in-depth on the read and search surface. It stops an agent from unknowingly echoing, logging, or committing a secret it read. It does not stop code that executes from reading keys directly. That is a job for a sandbox and a permissions layer, not a reader.

Measured against v0.2.6

Numbers in a release post should survive a rerun. After shipping, we rebuilt v0.2.6 from the commit just before the version bump and drove both daemons through pipe mode with byte-identical operations: a broad search, twenty single-line edits, and a whole-file replace of a git-tracked 2,000-line file.

v0.2.6 → v0.2.7, byte-identical ops, daemon pipe
Whole-file replace, response size165,278 B8,079 B20.5× smaller
Whole-file replace, latency8.9 ms0.6 ms15× faster
Single-line edit, mean of 199.58 ms0.32 ms30× faster
Broad search, response size286,044 B19,514 B14.7× less
Broad search, latency9.6 ms5.1 ms1.9× faster

v0.2.6 rebuilt from the commit before the version bump. Fixture: a git-tracked 2,000-line file for edits; pattern “const” over zigrep/ for search. Warm daemon, first iteration dropped as warmup.

Two notes on the numbers. The replace fixture lands at 20.5× rather than the headline 89× because its lines are long, so the capped 120-line diff stays fat relative to the old echo. The mechanism is the same either way: the v0.2.6 response carried the file roughly twice, a 97 KB content echo plus a 65 KB diff, while the v0.2.7 response carries no content echo at all and an explicitly elided diff. And the 30× on single-line edits isolates exactly what was removed, the 9 ms git diff fork that used to run after every write.

Update: v0.2.8, same day

v0.2.8 shipped later the same day with the CLI side of the toolchain: two features agents kept reaching for, and one bug that had been hiding in every tool since the day it was written.

  • zigrep -g/--glob. Ripgrep-style file filters: -g '*.ts' -g '!*.test.ts' -g '!thirdparty'. Bare !dir excludes prune whole subtrees during the walk, and globs containing / match the path relative to the search root. Works in content search, --files, find mode, and -R replace, where excluded files are guaranteed untouched. Runs inside the parallel engine: 5.4 ms where ripgrep takes 12.5 ms on the same query, 2.3× faster.
  • zigpatch --all. Replace or delete every occurrence of a pattern in one atomic write. Byte-level splice: a file without a trailing newline keeps that shape, a replacement containing the pattern cannot recurse, and --dry-run, --backup, and --undo compose with it. 1,667 replacements across a 5,000-line file in 2.2 ms, ahead of sed at 2.8 ms, and roughly 1,900× ahead of the old one-match-per-invocation workflow.
  • zigpar scheduling. Batch barriers are gone: each op now waits only on the latest earlier op that touches the same file with a write involved. A manifest with one slow search and an 8-edit chain on another file drops from 36 ms to 21 ms, because the chain hides under the search. The new zigrep and zigpatch surfaces ride along in manifests: globs on search ops, pattern / all on edit ops.

The bug is the part worth remembering. The scheduler rewrite refused to get faster, and instrumentation showed an edit's elapsed time exactly equaling a concurrent search's, every run. Exact equality is synchronization, not contention: children spawned at the same moment were inheriting copies of each other's pipe file descriptors, so an edit's EOF waited for whichever sibling exited last. The toolchain already had a pipeSyscall() helper that set FD_CLOEXEC to prevent exactly this. It never worked on Apple Silicon: it called the genuinely-variadic libc fcntl through a fixed three-argument declaration, and on ARM64 macOS variadic arguments travel on the stack while fixed arguments ride in registers, so the callee read garbage where FD_CLOEXEC should have been. One line, declared variadic, and the edit that used to report 19 ms (the search's lifetime) reports 2 ms (its own), in all thirteen tools, including the diff drainer this very post describes. If you declare variadic libc functions with fixed-arg prototypes behind any C FFI, this bug is waiting for you on Apple Silicon.

Upgrade

Run muonry update to pick up the new binary, and restart your MCP client so it reconnects to the new daemon. muonry --changelog shows these notes inline.

Full release notes are on the changelog. Issues go to the GitHub repo.