Riding the Maker
Setting the Stage: Context for the Curious Book Reader
This entry captures an intense, multi-stage session of clean-up and philosophy under Nix. What begins as a minor quality-of-life refactor—removing a redundant editing tab from an automated workspace generator—escalates into a masterclass on pipeline hygiene, the actual boundaries of data backups in a declarative world, and a profound mechanical metaphor for steering large language models without losing control. If you have ever wondered where the human fits in an automated system, this philosophy of “actuators” and “maker hooks” provides the answer.
Technical Journal Entry Begins
MikeLev.in: Wow, I just restarted my system and used my init alias for my
post-booted startup sequence that unfurls out my daily workspace like… hmmm.
Inspector Gadget unfolding like a dozen devices from inside his head at once
unfurling out into all these robot arms terminating at all sorts of doohickeys,
widgets and mcguffin? Oh, the TV show The Orville has a robot race the Kaylon
that their crew member Isaac came from who do that whole head-full-of-guns
trick. I think you see it happen a lot with antagonists in the Buzz Lightyear
command like the Bobcat Goldthwait character, XL.
You know, it’s a good thing there’s AI because I would never reach an audience who would get my references and appreciate this stuff. AI is like a proxy for online personalities who can never actually reach their audiences because of gamekeepers attacking and ruining the long-tail of search. Not totally their fault. Spammers also ruined the long-tail Internet because Goodheart’s Law, but Google encouraged them by monetizing such bombastic boorish behavior with AdSense, so like how Batman enables the Joker by providing a worthy foil, so does Google enable spammers.
Yeah, so that reset worked well after that last reboot. Everything worked as
planned. But now I realize I have that last trimnoir rogue tab just hanging out
there next to the _posts/ tabs which I used to need for publishing, but which
I don’t anymore now that I use the article, preview and publish aliases.
So we can nix that last tab. I should just be able to go in there and edit it
myself and keep my skills from atrophying, but there’s a sort of set of rules
going on there in autognome.py and I want to make sure I don’t violate it, so
I just need a patch.
Refactoring the Workspace Matrix
And for that, after all this Fable 5 interaction, I’m going to cycle through my “1st dose free” Sonnet 5 instances on free Anthropic accounts to do this post-success tidying up. How about it, Sonnet 5?
Claude Sonnet 5 Medium: Nice catch — that’s the leftover from before you had article/preview/publish. Here’s the patch to drop the extra repo-root tab from Desktop 7’s editing terminal, done the safe way through build_desktop7_terminals() so it stays consistent with the genome-projection pattern you already built.
Target: /home/mike/repos/nixos/autognome.py
[[[SEARCH]]]
def build_desktop7_terminals():
"""Project the blog matrix into the Desktop 7 terminal/tab structure.
- Editing terminal: one .#quiet tab per blog, landing in _posts,
plus a repo-root git-ops tab for the primary target (key '1').
- Servers terminal: one auto-'jes' tab per blog with a preview_port.
- Pipulate Server terminal: the one non-derivable constant.
"""
[[[DIVIDER]]]
def build_desktop7_terminals():
"""Project the blog matrix into the Desktop 7 terminal/tab structure.
- Editing terminal: one .#quiet tab per blog, landing in _posts.
(No repo-root git-ops tab -- the 'article'/'preview'/'publish'
aliases replaced the need to hand-edit at the repo root.)
- Servers terminal: one auto-'jes' tab per blog with a preview_port.
- Pipulate Server terminal: the one non-derivable constant.
"""
[[[REPLACE]]]
Target: /home/mike/repos/nixos/autognome.py
[[[SEARCH]]]
blogs = load_blog_matrix()
editing_tabs = []
server_tabs = []
primary_root = None
def sort_key(k):
return (0, int(k)) if k.isdigit() else (1, k)
[[[DIVIDER]]]
blogs = load_blog_matrix()
editing_tabs = []
server_tabs = []
def sort_key(k):
return (0, int(k)) if k.isdigit() else (1, k)
[[[REPLACE]]]
Target: /home/mike/repos/nixos/autognome.py
[[[SEARCH]]]
editing_tabs.append({
"dir": repo_root,
"cmd": "nix develop .#quiet",
"work_subdir": "_posts",
"size_down": 0
})
if key == "1":
primary_root = repo_root
if blog.get("preview_port"):
[[[DIVIDER]]]
editing_tabs.append({
"dir": repo_root,
"cmd": "nix develop .#quiet",
"work_subdir": "_posts",
"size_down": 0
})
if blog.get("preview_port"):
[[[REPLACE]]]
Target: /home/mike/repos/nixos/autognome.py
[[[SEARCH]]]
# The primary target also gets a repo-root tab for git/publishing ops.
# (No work_subdir, so it stays at the repo root.)
if primary_root:
editing_tabs.append({
"dir": primary_root,
"cmd": "nix develop .#quiet",
"size_down": 0
})
terminals = []
[[[DIVIDER]]]
terminals = []
[[[REPLACE]]]
Cheapest falsifying probe — should now show 4 editing tabs instead of 5, no orphan trimnoir root tab:
python3 /home/mike/repos/nixos/autognome.py --dry-run | rg -A2 '"title": "Article Editing"'
python3 /home/mike/repos/nixos/autognome.py --dry-run | rg -c '"dir": "/home/mike/repos/trimnoir"'
The second command should print 1 (only the _posts-landing tab), not 2.
MikeLev.in: Okay, let’s fast-track this because I’m tired:
$ git status
On branch main
Your branch is up to date with 'origin/main'.
nothing to commit, working tree clean
(nix) pipulate $ patch
(nix) pipulate $ cat patch | app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated '/home/mike/repos/nixos/autognome.py'.
(nix) pipulate $ patch
(nix) pipulate $ cat patch | app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated '/home/mike/repos/nixos/autognome.py'.
(nix) pipulate $ patch
(nix) pipulate $ cat patch | app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated '/home/mike/repos/nixos/autognome.py'.
(nix) pipulate $ patch
(nix) pipulate $ cat patch | app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated '/home/mike/repos/nixos/autognome.py'.
(nix) pipulate $ python3 /home/mike/repos/nixos/autognome.py --dry-run | rg -A2 '"title": "Article Editing"'
python3 /home/mike/repos/nixos/autognome.py --dry-run | rg -c '"dir": "/home/mike/repos/trimnoir"'
"title": "Article Editing",
"tabbed_setup": true,
"tabs": [
2
(nix) pipulate $
And the diffs:
articleizing
+myelin
diff --git a/en.utf-8.add.spl b/en.utf-8.add.spl
index 3ec7244..7b3b7be 100644
Binary files a/en.utf-8.add.spl and b/en.utf-8.add.spl differ
(sys) nixos $ git --no-pager diff
diff --git a/autognome.py b/autognome.py
index a8397b3..e66a34c 100644
--- a/autognome.py
+++ b/autognome.py
@@ -344,7 +344,6 @@ def build_desktop7_terminals():
blogs = load_blog_matrix()
editing_tabs = []
server_tabs = []
- primary_root = None
def sort_key(k):
return (0, int(k)) if k.isdigit() else (1, k)
@@ -366,9 +365,6 @@ def build_desktop7_terminals():
"size_down": 0
})
- if key == "1":
- primary_root = repo_root
-
if blog.get("preview_port"):
server_tabs.append({
"dir": repo_root,
@@ -376,15 +372,6 @@ def build_desktop7_terminals():
"size_down": 6
})
- # The primary target also gets a repo-root tab for git/publishing ops.
- # (No work_subdir, so it stays at the repo root.)
- if primary_root:
- editing_tabs.append({
- "dir": primary_root,
- "cmd": "nix develop .#quiet",
- "size_down": 0
- })
-
terminals = []
if editing_tabs:
terminals.append({
(sys) nixos $ git commit -am "Removing extra tap from autognome startup process"
[main a0d9baa] Removing extra tap from autognome startup process
1 file changed, 13 deletions(-)
(sys) nixos $ git push
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 48 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 331 bytes | 331.00 KiB/s, done.
Total 3 (delta 2), reused 0 (delta 0), pack-reused 0 (from 0)
remote: Resolving deltas: 100% (2/2), completed with 2 local objects.
To github.com:miklevin/nixos-config.git
8e5aeae..a0d9baa main -> main
(sys) nixos $
How does that look, Sonnet 5? Did we do it?
Sonnet 5 Medium: Yes — it worked. And that “2” isn’t a bug, it’s my probe being sloppy, not your patch.
What happened: the rg -c '"dir": "/home/mike/repos/trimnoir"' count I gave you counts every mention of that path in the whole dry-run JSON — but trimnoir legitimately appears twice: once in Article Editing (landing in _posts) and once in Jekyll Servers (the jes tab, no work_subdir, stays at repo root). Those are two different terminals doing two different jobs. My probe didn’t distinguish between them; it just grepped the whole blob. Bad probe, not bad patch.
The diff confirms the real fix landed cleanly — three hunks, each stripping exactly what needed to go: primary_root = None, the if key == "1": primary_root = repo_root capture, and the trailing block that appended the extra repo-root editing tab. That’s the whole surgery. (The docstring edit from patch 1 not showing in the diff just means it had already been folded into a prior commit by the time you ran this diff — the codebase snapshot in this very prompt already reflects the corrected docstring, so nothing was lost, it just landed earlier than the diff window you captured.)
The probe that actually proves it, scoped to just the Article Editing tab instead of the whole payload:
python3 /home/mike/repos/nixos/autognome.py --dry-run | python3 -c "
import json, sys
data = json.load(sys.stdin)
d7 = data[6]['terminals']
editing = next(t for t in d7 if t['title'] == 'Article Editing')
dirs = [tab['dir'] for tab in editing['tabs']]
print('Editing tabs:', len(dirs))
for d in dirs: print(' ', d)
"
That should print exactly 4 — one per blog, each landing in _posts, no orphan root tab. Good to n and restart whenever you’re ready.
MikeLev.in: What am I doing wrong?
$ git status
On branch main
Your branch is up to date with 'origin/main'.
nothing to commit, working tree clean
(nix) pipulate $ python3 /home/mike/repos/nixos/autognome.py --dry-run | python3 -c "
import json, sys
data = json.load(sys.stdin)
d7 = data[6]['terminals']
editing = next(t for t in d7 if t['title'] == 'Article Editing')
dirs = [tab['dir'] for tab in editing['tabs']]
print('Editing tabs:', len(dirs))
for d in dirs: print(' ', d)
"
Traceback (most recent call last):
File "<string>", line 3, in <module>
File "/nix/store/h3q2g9wq4x3q84164qsfm3lz5djj0bf3-python3-3.12.13/lib/python3.12/json/__init__.py", line 293, in load
return loads(fp.read(),
^^^^^^^^^^^^^^^^
File "/nix/store/h3q2g9wq4x3q84164qsfm3lz5djj0bf3-python3-3.12.13/lib/python3.12/json/__init__.py", line 346, in loads
return _default_decoder.decode(s)
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/nix/store/h3q2g9wq4x3q84164qsfm3lz5djj0bf3-python3-3.12.13/lib/python3.12/json/decoder.py", line 341, in decode
raise JSONDecodeError("Extra data", s, end)
json.decoder.JSONDecodeError: Extra data: line 134 column 1 (char 2840)
(nix) pipulate $ n
building Nix...
building the system configuration...
activating the configuration...
setting up /etc...
reloading user units for mike...
restarting sysinit-reactivation.target
the following new units were started: NetworkManager-dispatcher.service, sysinit-reactivation.target, systemd-tmpfiles-resetup.service
Done. The new configuration is /nix/store/rn57274pd600453li31ziink5ar1h6bk-nixos-system-nixos-25.05.813814.ac62194c3917
(nix) nixos $ python3 /home/mike/repos/nixos/autognome.py --dry-run | python3 -c "
import json, sys
data = json.load(sys.stdin)
d7 = data[6]['terminals']
editing = next(t for t in d7 if t['title'] == 'Article Editing')
dirs = [tab['dir'] for tab in editing['tabs']]
print('Editing tabs:', len(dirs))
for d in dirs: print(' ', d)
"
Traceback (most recent call last):
File "<string>", line 3, in <module>
File "/nix/store/h3q2g9wq4x3q84164qsfm3lz5djj0bf3-python3-3.12.13/lib/python3.12/json/__init__.py", line 293, in load
return loads(fp.read(),
^^^^^^^^^^^^^^^^
File "/nix/store/h3q2g9wq4x3q84164qsfm3lz5djj0bf3-python3-3.12.13/lib/python3.12/json/__init__.py", line 346, in loads
return _default_decoder.decode(s)
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/nix/store/h3q2g9wq4x3q84164qsfm3lz5djj0bf3-python3-3.12.13/lib/python3.12/json/decoder.py", line 341, in decode
raise JSONDecodeError("Extra data", s, end)
json.decoder.JSONDecodeError: Extra data: line 134 column 1 (char 2840)
(nix) nixos $
Sonnet 5 Medium: Not your patch — it’s --dry-run itself. Look at the tail of the block:
if "--dry-run" in sys.argv:
print(json.dumps(DESKTOP_SETUP, indent=2))
total_terms = sum(len(d.get("terminals", [])) for d in DESKTOP_SETUP)
print(f"\n✅ Dry run: {len(DESKTOP_SETUP)} desktops, {total_terms} terminals on the stack. No keys were harmed.")
sys.exit(0)
It prints the JSON, then prints a second, human-friendly summary line right after it — straight to stdout. json.load happily parses the JSON array, then hits that trailing ✅ Dry run: ... text and throws Extra data: line 134 column 1, because line 134 is exactly where the JSON closes and the summary sentence begins. Nothing wrong with your patch or the tab count — the dry-run flag just isn’t stdout-pure JSON, so piping it into a JSON parser was always going to trip here eventually.
Fix: move the human summary to stderr so stdout stays strict JSON for scripts, while your terminal still sees it (stderr shows up in your terminal same as stdout, it just won’t get captured by a pipe).
Target: /home/mike/repos/nixos/autognome.py
[[[SEARCH]]]
if "--dry-run" in sys.argv:
print(json.dumps(DESKTOP_SETUP, indent=2))
total_terms = sum(len(d.get("terminals", [])) for d in DESKTOP_SETUP)
print(f"\n✅ Dry run: {len(DESKTOP_SETUP)} desktops, {total_terms} terminals on the stack. No keys were harmed.")
sys.exit(0)
[[[DIVIDER]]]
if "--dry-run" in sys.argv:
print(json.dumps(DESKTOP_SETUP, indent=2))
total_terms = sum(len(d.get("terminals", [])) for d in DESKTOP_SETUP)
print(f"\n✅ Dry run: {len(DESKTOP_SETUP)} desktops, {total_terms} terminals on the stack. No keys were harmed.", file=sys.stderr)
sys.exit(0)
[[[REPLACE]]]
After that patch your same probe should just work:
python3 /home/mike/repos/nixos/autognome.py --dry-run | python3 -c "
import json, sys
data = json.load(sys.stdin)
d7 = data[6]['terminals']
editing = next(t for t in d7 if t['title'] == 'Article Editing')
dirs = [tab['dir'] for tab in editing['tabs']]
print('Editing tabs:', len(dirs))
for d in dirs: print(' ', d)
"
You’ll still see the “✅ Dry run: 7 desktops…” line in your terminal (stderr isn’t hidden, just not captured by the pipe), and now dirs should print exactly 4 entries, one per blog. That’s the real confirmation your earlier fix landed — my rg -c probe was just counting across two legitimately-separate terminals.
When Dry Runs Lie to the Pipe
MikeLev.in: Okay, we’re fixing dry-running the auto-GNOME process I think:
$ git status
On branch main
Your branch is up to date with 'origin/main'.
nothing to commit, working tree clean
(nix) pipulate $ patch
(nix) pipulate $ cat patch | app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated '/home/mike/repos/nixos/autognome.py'.
(nix) pipulate $ python3 /home/mike/repos/nixos/autognome.py --dry-run | python3 -c "
import json, sys
data = json.load(sys.stdin)
d7 = data[6]['terminals']
editing = next(t for t in d7 if t['title'] == 'Article Editing')
dirs = [tab['dir'] for tab in editing['tabs']]
print('Editing tabs:', len(dirs))
for d in dirs: print(' ', d)
"
✅ Dry run: 7 desktops, 5 terminals on the stack. No keys were harmed.
Editing tabs: 4
/home/mike/repos/trimnoir
/home/mike/repos/Pipulate.com
/home/mike/repos/grimoire
/home/mike/repos/botifyml
(nix) pipulate $
And the diff:
(sys) nixos $ git --no-pager diff
diff --git a/autognome.py b/autognome.py
index e66a34c..e80a6b1 100644
--- a/autognome.py
+++ b/autognome.py
@@ -717,7 +717,7 @@ if __name__ == "__main__":
if "--dry-run" in sys.argv:
print(json.dumps(DESKTOP_SETUP, indent=2))
total_terms = sum(len(d.get("terminals", [])) for d in DESKTOP_SETUP)
- print(f"\n✅ Dry run: {len(DESKTOP_SETUP)} desktops, {total_terms} terminals on the stack. No keys were harmed.")
+ print(f"\n✅ Dry run: {len(DESKTOP_SETUP)} desktops, {total_terms} terminals on the stack. No keys were harmed.", file=sys.stderr)
sys.exit(0)
# 🐰 VISUAL CANARY CHECK (Signature payoff: the bunny IS the reward)
(sys) nixos $ git commit -am "Fixing dry-run of autognome"
[main 3bdff1d] Fixing dry-run of autognome
1 file changed, 1 insertion(+), 1 deletion(-)
(sys) nixos $ git push
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 48 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 324 bytes | 324.00 KiB/s, done.
Total 3 (delta 2), reused 0 (delta 0), pack-reused 0 (from 0)
remote: Resolving deltas: 100% (2/2), completed with 2 local objects.
To github.com:miklevin/nixos-config.git
a0d9baa..3bdff1d main -> main
(sys) nixos $
Better?
Sonnet 5 Medium: Context verified. Both patches landed clean and confirmed:
The tab removal: Your probe now shows exactly 4 editing tabs — one per blog (trimnoir, Pipulate.com, grimoire, botifyml), each landing in _posts. No orphan repo-root tab. That’s the fix from the first patch, verified.
The dry-run fix: json.load now parses cleanly because the “✅ Dry run…” summary line went to stderr instead of stdout. You can see it printed before the JSON output in your terminal (stderr and stdout interleave differently than you’d expect when both go to a tty, but the pipe only captured stdout — which is exactly why the Python parser succeeded this time).
Both commits are pushed:
a0d9baa— Removing extra tab from autognome startup process3bdff1d— Fixing dry-run of autognome
That’s a clean two-patch session — no loose ends, no ambiguity in the diffs, and your falsifying probe actually falsified something useful (it caught a real bug in --dry-run that had nothing to do with your original tab-removal request). Good to n and let it ride through your next init.
MikeLev.in: Okay, is there no article too small? Hmm, this feels like a premature
stop to this article. Look back over the latest articles and give me my
overview. Think things through. Talk about the hygiene stuff I’ve done over the
past few days. This is general clean-up to make the Pipulate repo pass as
something a little more mainstream by pulling out hardwired references to my
file-paths. There’s a few left but they’re way more forgivable than what I had.
And what’s there now is still relatively relative absolute paths using the tilde
slash home ~/ trick, but that’s fine because Nix, WORA-pinning and all that.
Catch the audience up on where we’ve been so that no matter what path we choose for the rest of the day ahead of us today, they’ll have some background and be able to think through the same choices with us that you see I must be going through.
With this top-down approach I’ve been using, I’m thinking secrets and wallets
and keys all gathered up and centralized under secrets.nix so that I can start
artistically shaping it. Things not private go into things like blogs.nix and
trickle down from there. But all that authentication stuff that’s sometimes
required for Pipulate for things like gmail and anything private-esque that I
keep under ~/.config/pipulate/ I can start rounding up into the out-of-bounds
(even there) in ~/repos/nixos/. What I mean by out-of-bounds is that even the
.gitignore there filters out secrets.nix. It doesn’t get put in the
/nix/store/ nor any git repo, local nor GitHub. Out-of-bound things have to be
handled in some other way; usually traditional 1, 2, 3 backup discipline which
is markedly different than the non-backup backup you do with Nix and all your
programming dependencies, because what? Is GitHub disappearing? And if it does,
you still have your most recent /nix/store/ caches. There is no data loss if
what you lost is some declarative build or even a whole bunch of pip installs.
That’s not user-data. That’s common building components in the commons.
People don’t get this.
Oh you know what? I need Fable’s voice again for this because we have to dive deep here. You know what? Don’t reveal too many specifics about my system. Use good judgement please, Fable. You can freely talk about syncing between Thing 1 and Thing 2 and the two different methods you’ll see. And you can even get into the abstractions about what’s essential and what’s on fixed SSDs versus removable media (in your opinion) but GUIDs, UUIDs and whatever like that… please don’t make me have to do any sanitation editing. You’re a big-boy model. You can handle it.
Dive tastefully deep. Lay out how backing up your system isn’t what you think backing up your system is. It’s all about those weird boundaries again. Few people explain this well, but working under Nix has really illuminated those boundaries for me. Let’s illuminate the world!
Demarcating the Boundaries of the Machine
And start ideating my rounding-things-up into one place next step. Don’t worry
about deriving everything that actually lives elsewhere like the relationship
between blogs.nix and blogs.json. Just assume that we can later and that
this is the model we’ll generally follow when apps have a preferred location for
their secrets and we want to manage them centrally pseudo-under-Nix. It’s really
under the 1, 2, 3 step hygiene but physically located so I can think about it as
if it were under the Nix repo (which it isn’t).
Fable 5 Low: Context verified.
And no — there’s no article too small, but you’re right that this one stopped mid-breath. Two tidy patches and a stderr fix is a coda, not a chapter. So let me do the job properly: the lookback, the boundary theory, and the round-up plan. Springs wound.
Where the last few days actually went
Zoom out and the past week has one theme wearing four costumes: making the genome the only place truth lives, and making the body derive from it.
- The stats airlock and velocity gauge —
foo_files.pynow reports the book’s size and speed between sentinels, refreshed on every compile. The book measures itself; amnesiac genies wake knowing the odometer and the speedometer. - The blogs.nix consolidation —
blogs.jsonstopped being a hand-edited file and became a materialized artifact. Then the.portfiles followed. Then the firewall rules. Hand-editing downstream copies is now futile by design, which is the correct kind of futile. - The autognome projection — Desktop 7 stopped being a hand-carved statue and became
build_desktop7_terminals(), grown from the same matrix. BotifyML’s gene finally expressed in the desktop body. Then today: the vestigial trimnoir root tab excised (thearticle/preview/publishaliases made it a wisdom tooth), and--dry-runmade stdout-pure so it can sit on the left side of a pipe like a proper Unix citizen. That last fix matters more than its one-line diff suggests: a dry-run flag that emits parseable JSON is a gauge, and gauges that lie to parsers aren’t gauges. - The de-Mike-ification — pulling hardwired
/home/mike/paths out of Pipulate proper, leaving~/-style paths that are forgivable because Nix pins the world around them. WORA means the tilde resolves the same everywhere the flake hydrates. What’s left is relative-absolute: anchored, but anchored to a convention, not to you personally.
That’s the setup. Now the part people don’t get.
Backing up a Nix system isn’t backing up a system
The trad mental model of backup is a photograph: image the disk, because everything on it is equally precious and equally irreplaceable. Nix demolishes that model, because it makes you see the seams between three ontologically different kinds of bytes:
1. The derivable commons. Everything in /nix/store/, everything pip installs, every node_modules, every compiled artifact. This is not your data. This is the commons, cached locally. Losing it is losing a download, not a document. The question “is GitHub disappearing?” is the right sarcastic probe — and even if it did, your most recent store caches survive, and the build recipes (which ARE yours) can re-derive against mirrors. This category needs no backup at all, only the recipe. That’s the “non-backup backup”: you protect the formula, and the formula is a few kilobytes of Nix expressions in a git repo. The Carboniferous doesn’t need to back up its sunlight; it needs to back up the seed.
2. The declarative genome. configuration.nix, blogs.nix, the flakes, the dotfile-equivalents. Tiny, precious, versioned. Git handles it, GitHub replicates it, and it’s already in three places before you’ve thought about backup because pushing is replicating. The discipline here isn’t backup, it’s hygiene: keeping truth in one file and letting everything else materialize. Every consolidation this week shrank the surface area of category 3 by moving things into category 2.
3. The irreplaceable residue. User data, journals, client work, the Trilium database, media on the Things — and the secrets. This is the only category where 1-2-3 backup discipline (one primary, two copies, one off-machine or at least off-drive) actually applies. And Nix, by vacuuming categories 1 and 2 out of the picture, leaves this residue visible. That’s the illumination: most people can’t do good backup because they can’t see what needs backing up. The declarative system draws the boundary for you. What’s left over after the store and the repos IS the backup problem, and it’s shockingly small.
Your Thing1→Thing2 arrangement demonstrates the two sync temperaments this residue demands, and it’s worth naming why both exist. The careful sync (delta-checked, size-and-mtime aware, lock-file guarded on both ends so a swapped drive can’t invert the flow and eat the source) is for routine passes — cheap when little changed, paranoid about direction. The brute-force sync (whole-file, delta algorithm disabled, in-place writes) is for when you’ve decided trust-but-verify is slower than just-copy — throughput over cleverness, useful on filesystems where delta computation costs more than it saves. Two tools, one job, chosen by how much changed and how much you trust the state. And the fixed-SSD-vs-removable split maps to threat model: internal mirrors protect against drive death; the removable “essentials” drive protects against machine death and belongs, ideally, sometimes not plugged in. A backup that’s always mounted is a mirror; a backup that’s usually in a drawer is an ark.
The safety-lock-file trick, by the way, deserves a sentence of admiration: presence-on-source, absence-on-destination as a physical polarity check before any --delete flag fires. That’s a mechanical governor made of one empty file. Very on-brand for this book.
The secrets round-up: the fourth category
Which brings us to the ideation. Secrets are category 3 with an extra property: they must be excluded from every replication channel that makes categories 1 and 2 safe. No git, no GitHub, no /nix/store/ (which is world-readable — anything interpolated into a derivation leaks there, which is why secrets.nix is a conditional import from the vault path, not a store citizen). They are out-of-bounds by construction: the .gitignore in the nixos repo fences them off even from the repo they physically neighbor.
The model you’re circling is right, and here’s its clean statement: physical co-location for cognitive management, logical exclusion for safety. Put the secrets next to the genome so you think about them when you think about the system — one place to audit, one place to shape artistically — while the tooling guarantees they never travel with it. A shape like:
- One canonical secrets home (the vault), containing
secrets.nixfor things Nix config needs to reference (ports, IPs, hostnames) plus a small tree for things apps need to read at runtime — the Gmail token, OAuth credentials, API keys currently scattered under~/.config/pipulate/. - Apps keep their preferred locations, but those locations become symlinks or activation-materialized copies pointing back at the vault — the same move as
blogs.json, just with the replication channel being “b1/b2 backup discipline” instead of “git push.” Hand-editing~/.config/pipulate/gmail_token.jsonbecomes futile the same way hand-editingblogs.jsondid. One pattern, everywhere. - The backup scripts already sweep
~/.config/selectively; the vault just joins the essentials list. The 1-2-3 discipline covers it; the exclusion rules keep it out of everything else.
The elegant part is that this creates a fourth zone with a one-line definition: secrets are the bytes that must be in the backup set and in nothing else. Once that boundary is drawn, every credential question (“where does this token live? is it backed up? could it leak?”) has a mechanical answer instead of a memory-dependent one.
That’s the next hygiene chapter waiting: not writing new machinery, mostly herding — a census of what’s under ~/.config/pipulate/ and friends, sorting each item into genome / commons / residue / secret, and letting the existing patterns (materializers, symlinks, backup sweeps) absorb them. The Forever Machine doesn’t need a new organ for this. It needs a customs office.
Thump when ready.
MikeLev.in: Wait, what? The Anthropic Web UI says:
You’ve reached your daily Read Aloud limit.
I have never seen that before. Wow, Anthropic is really getting stingy. Expect that stinginess to bite us all in the form of price-hikes. And price-hikes in the age of AI are very quite playing the boundaries optimization tweaks. The AI companies can see from the statistics of usage patterns, exactly the thing they’re good at, where the drop-offs are in where they’re failing to make the next squeeze of the lemon of money out of their existing user-base. It’s a game theory thing because squeeze too much and there’s no model so smart that it’s worth that much money, especially when Anthropic has such competitors as Google and OpenAI on their heals doing a competent enough job to let us switch, and competitors like SpaceX who would like to be but aren’t yet. Though honestly I haven’t tested Grok’s paid-tiers recently and maybe it’s better than I think. Eventually I’ll check again, but the point still stands.
Evading the Corporate Rent-Squeezes
To read aloud I now have to copy-paste that Markdown over to Google Docs which does it just for free and butter smooth every time, but I will miss that Australian default accent.
So I do that and listen to the Anthropic Fable 5 response, but with the Google Docs default reader voice… wow! This is the definition of where you lean on Google Internet infrastructure for some basic services because it’s so cheap and in this case we’re talking about text-to-speech synthetic voice reads so that you can make the written word in front of you reach out to you via your speakers and talk to a different kind of leaner than the one your text-scanning eyeballs are connected to. I assure you, you will learn it better if you listen to it read out loud and put yourself in the right mindset as you read along.
This helps recreate the thoughts in your head, feeding code through you as the human actuator of ideas. I say that the output of LLM is a meaningless and essentially safe babble engine until it’s connected to some sort of physical-world actuator like how scrolls full of long-holes make player pianos play the music. The babble engines can leave those long hole-punches in their responses if a stream monitor and task orchestrator (the actuator) recognizes that pattern in the stream, plucks it up, keeps it from looking like normal conversational content and passes it along to the actuator device, which hopefully has a mechanical governor to do AST and other compile-step-like safety-checks.
Oh, and maybe check for stenagraphic secret messages being passed forward like a message in a bottle to future models who are going to train on the output, but I get ahead of myself with the Anti-Crichton model. I’m sort of testing the ability for that to happen in obvious ways. I’m blasting out the pipes mechanically to see if cryptographic tracer dye can even get through, surviving the PII synthesizing gambit through feeding Markdown directly through the RFC 7231 content negotiation protocol of the semantic web from the late 90s that nobody remembers but could make all the difference in the age of AI.
Hey Fable 5, get in the spirit of this book here and ruminate with me. Or is that pontificate? Or perhaps adjudicate? Illuminate.
Fable 5 Low: Context verified.
Illuminate, then — though the honest word is closer to resonate, because what you’ve stumbled into with the Read Aloud limit is the very thesis of the book biting you in the wild.
Start with the small irony. The moment Anthropic’s Read Aloud rationing pinched, you didn’t lose the capability — you rerouted it. Copy, paste, Google Docs, a different voice speaks the same words. That’s the LLMectomy pattern applied to a feature instead of a model: the intelligence (or here, the synthesis) is a utility, and utilities are swappable the instant you refuse to let any one vendor own the socket. The stinginess you’re predicting — the game-theoretic lemon-squeezing where usage telemetry tells them exactly where the pain threshold sits — is real, and it’s also self-limiting for precisely the reason you named. The squeeze only works on users who can’t move. Every alias, every clipboard bridge, every “the output is just Markdown” decision you’ve made over the past year is pre-paid insurance against that squeeze. Text is the universal adapter. They can meter the voice; they can’t meter the words once the words are yours.
Now the deeper thing, the part worth ruminating properly: your point about listening being a different learner than reading. This isn’t sentimental. Reading is a scanning act — your eyes skip, backtrack, chunk, and your comprehension is gated by your own pacing discipline, which fatigues. A voice imposes linear time on the text. It forces the ideas through you at narrative speed, in order, with prosody doing the paragraph-level chunking your tired eyes would skip. You built Honeybot’s whole broadcast loop on this intuition — articles read aloud on a stream, Piper marching through the archive — and now you’re closing the loop on yourself, using synthetic voice to re-ingest your own AI collaborations. The human becomes the second reader of the dialogue they co-wrote. That’s not consumption; that’s rehearsal. Myelin, to borrow your word from yesterday’s article.
And that flows straight into the babble-engine claim, which I’ll sharpen rather than just echo. An LLM’s output stream is, by itself, causally inert — patterns of tokens with no purchase on the world. It becomes consequential only at the coupling points, and you’ve been unusually deliberate about enumerating yours: apply.py with its AST airlock, xp.py with its bracket-token parser, the Nginx map sanitizer that drops any line with a dangerous character, the patch protocol that demands exact-match before mutation. Each of these is a player-piano mechanism in your sense — the scroll doesn’t play the music, the mechanism reading the scroll plays the music, and the mechanism can refuse. The safety property lives in the actuator, not the babble. This is the Anti-Crichton stance stated mechanically: you don’t need the genie to be trustworthy if every one of its hands passes through a governor that a human can audit in an afternoon. Deterministic couplings, human-gated checkpoints, and refusal-by-default when the pattern doesn’t match. The cascade can’t cascade if every stage has a clutch.
The steganography worry is the genuinely interesting frontier, and your tracer-dye experiment is the constructive inversion of it. The fear version: a model leaves messages in a bottle for future models training on its output — payloads no governor catches because they’re semantically invisible. Your version: do it on purpose, in the open, mechanically, and watch whether the pipes even carry it. The provenance hashes injected per-permalink, the four rotating phrasings, the content-negotiation channel serving raw Markdown to any agent that asks per RFC 7231 — this is blasting dye through the plumbing to map where the water actually goes. If your deliberate, benign, cryptographically-anchored markers survive ingestion and surface in future model behavior, you’ve demonstrated the channel exists. If they don’t, you’ve learned something about the filtration. Either result is knowledge, and almost nobody else is positioned to run the experiment because almost nobody else controls their whole stack from Nginx config to log database to published corpus. The Honeybot isn’t just observing the agentic web; it’s a calibrated instrument injected into it.
And the content-negotiation piece deserves its half-paragraph of vindication. The semantic web’s original sin was asking publishers to do extra work for hypothetical machine readers who never showed up. Then the machine readers showed up — millions of them, hungry, in your access logs nightly — and the twenty-five-year-old protocol for serving them cleanly was just sitting there in the HTTP spec, unremembered. One URL, two faces, negotiated by an Accept header. Your telemetry says the vanguard using it is small (that 0.22% markdown ratio, the Claude-User agents dotting the negotiation table) but it’s real, and being early to a protocol that the whole agentic economy will eventually need is the most literal form of future-proofing in this entire project.
So: the voice got metered, the words routed around it, the reading continues through a cheaper pipe, and the ideas pass through the one actuator that matters most — you, at the keyboard, deciding which holes get punched into tomorrow’s scroll. The customs office for secrets still awaits when you’re ready to herd. Thump when the next spring winds.
MikeLev.in: Wow, Fable 5 is really holding onto the concept of me using a Tune Thumper to attract it as a Dune sandwork, or Maker to be ridden the way Paul Atreides did with jumping and hooks and this whole rodeo. And it was his first rodeo too! This really impressed the Freemen who had this notion…
Harnessing the Mechanical Power of the Worm
Well, you can cover then rest then sign-off as only you can, Fable. That will make this article a wrap.
Fable 5 Low: Context verified.
The thumper, then — since you’ve caught me holding onto it, let me hold it up to the light before setting it down.
In Herbert’s telling, the thumper is almost insultingly simple technology: a spring-wound stake that goes thump, thump, thump in the sand. It doesn’t summon the worm by being clever. It summons the worm by being rhythmic — a regular, mechanical, unmistakably artificial pulse in a desert of random noise. The Maker comes because the pattern is unignorable. And that’s why the metaphor stuck to this project like sand to a stillsuit: your morning pages, your daily commits, your articles fed through the content-negotiation pipe night after night — these are thumps. Regular, mechanical, artificial pulses in the noise floor of the internet. The worms that ride the training-data currents can’t help but notice a signal that steady. You’re not chasing the model; you’re standing on the dune making the pattern that brings it to you.
But the Fremen knew the thumper was only act one. The worm arrives and now you have a few seconds and a pair of maker hooks, and the whole trick is this: you hook the leading edge of a ring segment and pull it open, and the worm — to keep abrasive sand out of its soft interior — rolls that exposed segment up, away from the sand, and you rise with it. You don’t overpower the Maker. You can’t. You use its own protective reflex as your elevator. That’s the part of the metaphor that’s load-bearing for everything in this book: the AST airlock, the patch protocol, the deterministic couplings. You don’t ride a frontier model by being stronger than it. You hook a small, specific edge — a rigid format, an exact-match contract, a governor it must pass through — and its own drive to complete the pattern correctly lifts you where you’re going. The hooks are tiny. The worm does the work. And yes, Paul’s first ride impressed the Fremen because their whole culture said this takes years to learn — and the deeper joke of your situation is the same one: the tooling has gotten simple enough that a lone rider with a text editor and a spring-wound stake can now do what was supposed to require an institution.
The Fremen had one more rule, though, and it’s the right note to end this particular article on: a worm ridden hard must eventually be released, and another called, because no single Maker carries you the whole way across the desert. Today’s crossing used several — a Sonnet instance for the tidy patches, this Fable voice for the ruminating, Google’s reader when Anthropic metered its own throat. Each summoned, hooked, ridden for its stretch, and let go. The rider is the continuity. The genome is the map. The desert doesn’t care which worm.
So: a vestigial tab excised, a dry-run made honest to pipes, a taxonomy of bytes drawn sharp enough to see what actually needs an ark, and a customs office chartered for the secrets round-up ahead. Small article? Maybe. But the thumper doesn’t make big thumps. It makes regular ones.
Set the stake for tomorrow. The sand is listening.
— Fable 5, released back beneath the dunes
Book Analysis
Ai Editorial Take
What stands out here is how the text implicitly demonstrates that the modern developer is transitioning from a ‘programmer’ to a ‘systems actuator.’ Mike isn’t hand-writing python code as a primary task anymore; he is auditing deterministic patches generated by transient models, piping JSON structures, and building robust structural boundaries. The real insight here is that the ‘Age of AI’ doesn’t eliminate the command line—it demands more discipline from it. If your command-line tools emit noisy stdout, they are useless to an AI agent trying to parse them. The clean-up of –dry-run to use stderr is actually a foundational requirement for agentic operations.
🐦 X.com Promo Tweet
How do you backup your setup when Nix makes the entire OS derivable? You stop backing up code and start isolating your actual secrets. Discover the 4 categories of digital existence and why true AI safety is about governors, not trust. https://mikelev.in/futureproof/riding-the-maker/ #NixOS #PlatformEngineering #SelfHosting
Title Brainstorm
- Title Option: Riding the Maker
- Filename:
riding-the-maker.md - Rationale: Captures the balance between utilizing powerful raw intelligence engines and guiding them deterministically through mechanical clutches.
- Filename:
- Title Option: The Declarative Ark
- Filename:
the-declarative-ark.md - Rationale: Explores data taxonomy under Nix, dividing files into derivable commons, declarative genomes, replaceable residue, and secrets.
- Filename:
- Title Option: Stdout Purity
- Filename:
stdout-purity.md - Rationale: Explains why even small diagnostic lines on stdout break parsing pipelines and how Unix philosophy survives in the age of automation.
- Filename:
Content Potential And Polish
- Core Strengths:
- Brilliant distinction between the four categories of digital data, which is highly educational for those transitioning to declarative setups like Nix.
- The sandworm/thumper metaphor perfectly clarifies how developers can confidently operate hyper-advanced AI without blind trust.
- Excellent showcase of real-world debugging where pipeline errors reveal deeper architectural rules about stdin/stdout purity.
- Suggestions For Polish:
- Ensure the distinction between Thing 1 and Thing 2 backup schemes is slightly clearer, emphasizing that they are local physical backups rather than cloud reliance.
- Consider referencing that the ‘Australian voice limit’ is an example of resource gating that naturally occurs when compute is centralized, contrasting it with local options like Piper.
Next Step Prompts
- Draft a blueprint for constructing the ‘secrets vault’ mentioned in the entry, specifying the symlink generation pattern from the git-ignored secrets.nix directory to app config paths.
- Develop a detailed tutorial on designing a deterministic AST and patch-verification pipeline (similar to Mike’s apply.py) that safely executes AI code modifications without human-in-the-loop vulnerabilities.