Case study · voice-driven deploys · a safety model you can sleep on

say it.
shipmate.

A voice-driven deploy and ops assistant. Say what you want shipped from a phone, a car, or a terminal; it plans, shows the cost, waits for an explicit yes, and pushes to production. The interface was the easy part. The real design problem was an authority model you can hand your infrastructure to and sleep.

  • Siri · Claude voice mode · terminal
  • DigitalOcean App Platform · Vercel
  • Plan → grant → execute, enforced in code

The problem

Three silos, one ritual, every project.

Shipping a small app means hand-clicking across three systems that don't talk to each other: the host, the DNS, the registrar. Some registrars, like Squarespace, have no API at all. Every new project repeats the same ritual, and every repeat is a place to slip.

Voice is a lovely demo and a terrible interface for irreversible, billed infrastructure. A mishearing that costs money, or deletes something, is not a bug you file later. So the real design problem was never make an LLM run doctl. It was: an authority model you can hand your infrastructure to and sleep.

HOST app spec · env · build DigitalOcean / Vercel DNS A / CNAME · TLS REGISTRAR Squarespace: no API the human, clicking across all three every project. every time. by hand.
The ritual. Three systems, no shared API, one person carrying state between them in their head.

The principle

Brains and authority, separated in code — not in conversation.

You can't prompt your way to safety. If the model can be talked into anything, it can be talked into the wrong thing. So the model proposes and the code decides. The model never holds a credential, never holds the authority to execute, and never gets to argue about it.

Thin clients, one engine

Siri, the Claude app's voice mode, and the terminal are interchangeable mouths.

They all land on the same tested core. Adding a front end adds zero new authority.

Honest gaps over false magic

When a provider has no API, it says so and hands you the one manual step.

No pretending. The thing you can't automate is named, out loud, with what to do instead.

Credentials stay with you

Env var → the platform CLI's own auth / OS keychain → a clear error.

Never in argv. Never in a committed file. The secret classifier that decides GENERAL vs encrypted SECRET is the highest-risk code in the project, so it has its own test suite.

Figure 1 · the ecosystem, end to end

Two mouths. One engine. One way out.

iPhone “Hey Siri, shipmate” Shortcut · Run Script over SSH Claude app voice mode MCP custom connector two mouths ssh · tailnet https · funnel the Mac · on the tailnet bridge over SSH MCP server :8788 · Tailscale Funnel, token-in-path ONE ENGINE headless Claude Code + the deploy skill + helper scripts github DigitalOceanApp Platform/ Vercel DNS+ TLS live site ntfy push → iPhone / CarPlay announces the result Approve Deny tap on the phone → tailnet-only endpoint on the Mac new spend only
Every front end is a mouth on the same engine. The engine holds the tested core; the phone holds the yes.

What you can say

Plan out loud. Ship on purpose.

It deploys the current project to DigitalOcean App Platform or Vercel, writes the right app spec with no secrets in the repo, maps a subdomain automatically when DNS is on the provider, shows the plan and the monthly cost, and waits for an explicit yes.

"shipmate, deploy panogram"Plan turn. Read-only. Shows the app spec, the subdomain, the monthly cost — and waits.
"…ship it" · "confirm" · "do it" · "send it"Execute turn. Only honoured inside ten minutes of a plan, and only as the last words of the phrase. On every channel, Siri included: with no fresh plan, the phrase runs as a plan — read back with the cost — and arms the grant.
"roll back panogram, confirm"Reverts the last deploy. No model in the loop.
"counsel on" · "counsel off"On: a multi-model panel that must name its dissent. Off (default): the single best Anthropic model answers.
"doctor"One deterministic health sentence — internet, tailnet, MCP server, provider auth, disk, jobs, queued intents.
a captain's log entrySpoken words appended verbatim to the project journal and committed. "Ship it" publishes it.
"at home, deploy …"Fleet routing: the phrase runs on another host over SSH.

Two test suites guard the two decisions that matter: 78 assertions for the phrase parser that decides plan vs execute, 11 for the secret classifier.

Figure 2 · the authority model

An agent that never planned has no path to execute.

PLAN turn read-only --permission-mode plan shows spec + cost, waits GRANT single-use · 10-min TTL enforced in code gate EXECUTE turn only when the phrase ENDS with “confirm” · “do it” · “send it” · “ship it” → push · deploy · DNS an agent that never planned… …has no path in background workers fresh branch NO deploy tools allowlisted a proposal never a deploy merge + deploy pass back through the same gate new spend? → forks to a phone tap delete? impossible from voice the model proposes. the code decides.
The grant is the whole idea. It exists only because a plan ran, it expires in ten minutes, and nothing executes without it. Not a prompt. A check. That holds on every channel, Siri included, because the bridge enforces it: an execute phrase with no fresh plan runs as a plan, is read back with the cost, and arms the grant.

Workers are untrusted by design

A worker can write anything. It can ship nothing.

Background agents do real work — the first production ship was a light mode for panogram that a background agent built. But a worker starts on a fresh branch with no deploy tools in its allowlist, and everything it makes is a proposal. To become a deploy, that proposal goes back through the same plan-then-execute gate a human's phrase would. The agent that did the work is never the agent that ships it.

0deploy tools in a worker's allowlist
1gate — the same one, for humans and agents
10 mina plan grant lives, then it's gone

Road mode · Figure 3

Built for a dead zone, on purpose.

Road mode was built for a road trip with Starlink on the roof of a 2006 VW Touareg. Siri gives you about 25 seconds to answer before the conversation is over, and the road gives you no connectivity at all for stretches. So every spoken request becomes an intent record on disk first, and the answer finds its own way back.

one spoken request intent recordon disk answer in25 s? yes spoken no — detach arrives lateras a push offline? “dead zone, queued” launchd timer replays FIFO when connectivity returns 2006 VW Touareg · Starlink on the roof the road doesn't wait for the network
Nothing is lost to a tunnel. If it can't be said in time, it's pushed; if it can't be pushed, it's queued; when the sky comes back, it replays in order.
25 sthe speaking budget
FIFOthe dead-zone queue replays in order
1 pushwhen a host goes dark — not one every five minutes

Around it: a tailnet-only mission-control dashboard shows jobs, deploy phases, the dead-zone queue, and pending approvals with Approve / Deny buttons, so the passenger can authorize spend while the driver drives. Fleet routing runs a phrase on another host over SSH. A watchdog pushes once when a host goes dark.

Timeline

Six days in July. One evening in September.

  1. 2026-07-05Voice bridge v1 proven end to end. First voice-driven production ship: a light mode for panogram, built by a background agent, reviewed, then merged and deployed by a spoken phrase ending "ship it".
  2. 2026-07-06The MCP server, published through Tailscale Funnel as a claude.ai custom connector; the Claude iOS app's voice mode calls the tools. LaunchAgents so the servers survive reboots. Counsel. Rollback. Tap-to-confirm for new spend, live-tested.
  3. 2026-07-10Road mode — built for a road trip with Starlink on the roof of a 2006 VW Touareg (shotgundetour.com). The dashboard. The captain's log.
  4. 2026-07-11Fleet routing and the cross-host watchdog.
  5. 2026-09-10 → 11The laptop became the live host in one evening. A rehearsal for a portfolio review found three real bugs in an hour — and the first voice-driven change to unakin.com itself followed.

The night before a portfolio review

An hour of rehearsal found three real bugs.

On September 10 the laptop became the live host in one evening: Funnel, connector, both Shortcuts. Then I rehearsed the demo the way I'd give it, and the rehearsal did what rehearsals are for.

Bug 1 · dictation

“unakin” came through as “anakin”.

Siri dictation mangles project names. Fixed with aliases, so a project answers to the ways a phone mishears it.

Bug 2 · a flag-order bug

A fresh execute session swallowed the prompt.

The CLI flag order was wrong on a cold start, so the phrase never reached the engine. Fixed; tests pass.

Bug 3 · the wrong remote

A push went to a personal mirror, and the model reported success.

The deploying remote never got it. Now the mirror's push URL is disabled, and an execute turn must verify a push before it's allowed to claim one.

The fixes shipped the next day: one shared gate for every channel, a “ship it” button on every plan push, and a live check after each ship that pushes when the commit is actually serving.

Then the first voice-driven change to unakin.com itself: two medic persona cards on the SonoSite study merged into one “Combat medic” card. Commit 7f35200, deployed in about two minutes. The site you're reading was changed by the thing this page is about.

What's next · honest gaps

What it doesn't do yet.

The interface is half the job.

For twelve years I've built novel ways to talk to computers — a thumb on a radius, an eye settling on a target, a voice in a car. Every one of them taught the same lesson: the interface is only half the job. The other half is whether you can trust what happens next. On iViz the trust was a gesture that forgave a slip in slick gloves. On shipmate it's a gate that refuses a garbled sentence. Same discipline. The body sets the rules — and here, the rule is that authority never lives in the conversation.