
TLDR: I added voice to my NanoClaw fork the wrong way first — patched directly into core, it worked, upstream shipped an update, everything broke. Getting unstuck forced me to actually understand agentic AI plugin isolation: three-way merging, runtime isolation, idempotent reapplication. Here’s that journey, including the TLS wall, a one-character VAD bug that took four commits to solve, and an honest gap in the architecture I discovered along the way.
The Part Where I Did It Wrong
NanoClaw (and its cousin OpenClaw) is genuinely exciting to me. An AI agent that runs securely in containers, learns from you, can automate your OS processes, and — this is the part that keeps me up at night in a good way — can modify and extend its own capabilities. After placing 8th at the AI Agents Waterloo Voice Hackathon with Standup Buddy, I wanted to bring voice into my daily NanoClaw workflow.
So I did what felt natural. I forked the repo, found src/channels/whatsapp.ts, created src/channels/voice.ts right next to it, dropped the web client in clients/voice-web/, wired everything into src/index.ts. Direct. Clean. Worked on the first try.
Then NanoClaw shipped an update.
Suddenly I had merge conflicts in the exact files I’d touched. Claude CLI was helpful — it could detect the problems and propose fixes — but I kept running into the same question: what can I actually commit here? Every fix I made was borrowing against future merge debt. I was patching my patches. The add-voice branch still exists if you want to see what that looks like.
That’s when I stopped and thought: the README literally says “contributors shouldn’t add features to the codebase, they should contribute skills.” I had violated the project’s own philosophy not out of ignorance, but because it was faster. And now I was paying for it.
This is the core challenge of agentic AI plugin isolation — the system is designed to be extended a specific way, but the “fast path” is always to just modify the core. The fast path feels safe until the project under you moves. Then it’s a trap.
Agentic AI Plugin Isolation: How the Skills System Actually Works
The skills system is how NanoClaw implements agentic AI plugin isolation — extending behavior without touching core code. You drop your skill into .claude/skills/add-voice/ and it applies changes through a controlled process — adding files, modifying existing ones, registering new capabilities.
The key insight I was missing: my customizations should live in the skill directory, not in the committed codebase. When upstream ships something, I pull it cleanly. The skill layer applies on top.
But there’s an obvious question I had immediately: what happens when my voice skill needs to modify src/index.ts, and later I want to add Telegram, and that skill also modifies src/index.ts? Do I just overwrite my own changes?
This is where the architecture earns its keep.
The Three-Way Merge That Makes It All Work
NanoClaw’s skills engine uses three-way merging — the same algorithm Git uses for branch merges — to combine changes intelligently. Every apply operation knows about three versions of a file:
- Base — the original file from when the project was initialized
- Current — my version, which might have customizations or other skills already applied
- Skill — the new changes this skill wants to make
What makes this actually usable in practice is the rerere cache. When you manually resolve a conflict, NanoClaw records that resolution in .nanoclaw/resolutions/. Next time you hit the same conflict pattern — say, applying these two skills to a fresh machine — it auto-resolves. You solve it once.
Dev Note: This is why the idempotent principle matters so much here. If I find a VAD bug in my voice skill and fix it, I update the skill and reapply. The three-way merge handles the minimal core wiring changes, and my fix is in. No migration script, no “remove old version first.” This only works cleanly if the skill application can be safely repeated.
The implication for my fork strategy: I can layer skills over time — voice now, Gmail next, Telegram after that — and they compose without manual coordination. Pulling upstream changes doesn’t require untangling my channel code because my channel code isn’t in the core.
Adding Voice: It’s More Than a New Input
I want to say something about voice in an agentic context that I don’t think gets said enough: it’s not just a UI change, it changes the interaction paradigm entirely.
When you’re typing commands to an agent, you’re issuing instructions. When you’re talking, you’re having a conversation with something that’s running tasks in the background. The latency tolerance is different. The feedback loop is different. The social cues matter.
For speech synthesis I’m using Smallest.ai — same API from the hackathon, configured at 24000 Hz for their output. The voice quality justified sticking with it. But getting the listening side right took longer than the talking side.
The TLS Wall (And Why Tailscale Made It Disappear)
First obstacle: browsers will not allow microphone access outside of a secure context. HTTPS or localhost. Hard requirement, non-negotiable, not something you configure around.
I’d already been using Tailscale for secure networking — it creates a private network between all your devices, accessible from anywhere as long as you’re on the tailnet. What I didn’t fully appreciate until this project is that Tailscale also issues real TLS certificates for your devices. Not self-signed. Actual trusted certs.
So instead of fighting certificate authorities:
- Secure networking to any of my devices, anywhere ✅
- Valid TLS certificates browsers actually trust ✅
- No VPN configuration, no port forwarding ✅
The voice skill listens on port 8443 with those certs. Web Audio API is happy.
Dev Note: The Tailscale piece felt almost too easy after I realized it was there. I spent more time reading about mkcert and self-signed certificate workflows than it took to just configure Tailscale’s cert. Sometimes the tool you’re already using solves the problem you think you need a new tool for.
The VAD Debugging: Four Commits for One Character
Voice Activity Detection is what decides when you’ve started and stopped talking. Get it wrong in either direction and the experience falls apart.
My symptom: transcription was consistently missing the first word. “Check the weather tomorrow” came back as “tomorrow.” Frustrating in a way that feels like a network issue before you realize it’s happening before the audio even leaves your browser.
The root cause was a single character:
// Before: required 2 frames above thresholdif (energy > SILENCE_THRESHOLD && frameCount > 1) {// After: start immediately on first frameif (energy >= SILENCE_THRESHOLD) {
> instead of >=. The original logic required two consecutive frames above threshold before opening the recording buffer. Frame 0 of speech was always dropped. One character change, problem solved — but only after I’d already shipped a lookback buffer approach, a “include current frame” fix, and a more complex immediate-start implementation. You can watch the four commits iterate toward this in the staging branch.
Dev Tangent: This is the part of voice UX work that doesn’t show up in tutorials. The algorithm that works in a quiet room with a good USB mic at a fixed threshold doesn’t work in a different environment. You need debug logging that lets you measure your actual environment and tune to it. The 0.008 threshold is right for my setup. It might be wrong for yours.

The recording indicator (🔴) deserves a mention too. It’s not just visual decoration — it’s a social cue. It tells you when the system is actively listening so you know when to speak. Without it, users either talk over silence or wait too long after the agent responds. Small thing, meaningful difference.

Where Agentic AI Plugin Isolation Has an Honest Gap
The skills system is designed to be non-invasive. Skills live in .claude/skills/add-voice/, they apply their changes, the runtime code stays isolated. But voice.ts serves the web client from clients/voice-web/index.html — a path in the project root, not inside the skill directory.
Apply the skill. Server starts. Web client: missing.
This breaks two things I care about in agentic AI plugin isolation:
Plugin isolation — the voice runtime code should live entirely in .claude/skills/add-voice/. The moment I copy that file into the project root and commit it, I’m back to the problem I started with.
Idempotency — if I reapply the skill after a fix, the updated web client doesn’t automatically land in the right place. I have to remember to copy it again.
Worth noting: while I was working through this, upstream shipped fix: copy skill subdirectories recursively (#175). The maintainers are thinking about exactly this class of problem. I don’t think it’ll stay a gap for long.
The Four Properties of Agentic AI Plugin Isolation
I didn’t design an elegant solution. Upstream forced my hand and I had to figure out the right architecture under pressure. That’s a more honest description of how most architectural improvements actually happen.
- Non-invasive — minimal changes to core
- Plugin isolation — runtime code stays in skill directory, never committed
- Composable — three-way merge handles multiple skills on the same files
- Idempotent — reapply anytime, get a clean upgrade
The voice skill satisfies three of these cleanly. The asset gap is where property 2 breaks down, and it’s the exact place the architecture needs to close.
The bigger pattern: AI-assisted development makes it easy to fix anything locally. The discipline is deciding what you can commit. Claude CLI found every problem. But it couldn’t decide for me which problems were worth solving in core versus pushing into a skill. That judgment is still mine to make.
What’s Next
The staging branch has voice working end-to-end with Tailscale TLS and calibrated VAD. Next I want to:
- Properly solve the asset gap — either through the skill’s
modify/section or runtime path resolution - Layer a second skill on top to validate the three-way merge in practice, not just in theory
- Document the agentic AI plugin isolation pattern more explicitly so others forking NanoClaw don’t have to rediscover it
If you’re building on NanoClaw or OpenClaw and have hit similar fork maintenance challenges, I’d genuinely love to compare notes. And if you’re at Tailscale — thanks for making TLS for local infrastructure a non-problem.
Learning in public at data-slug.com. Staging branch: github.com/lightshadow1/nanoclaw. Initial (wrong) attempt: add-voice branch.

Leave a Reply