Hacking from the backcounty
Commanding coding agents over satellite text, with zero bars.
I’ve been thinking a lot about what building with AI means. A friend told me I should touch grass, so I sent him a picture of me mid-hike — and mid-deploy, and mid a dozen other tasks, from figuring out my Achilles surgery to figuring out which neighbourhood I want to move to next. That’s the flexibility of having Cortex on my phone. Then I thought: what’s more inspiring, connecting, and human, than hiking into the backcountry with some books and a paper notebook? I don’t need a laptop, iPad, or an IDE.
This week that stopped being a thought experiment. On a ridgeline with no terrestrial service, my phone can now reach Cortex, my AI chief of staff. A short iMessage or WhatsApp message can dispatch a coding agent, ask for status, or start a Claude Code session on my server that appears in the Claude app already running when I get back to bandwidth.
That works because of T-Mobile’s T-Satellite service: the phone talks directly to satellites, with no external hardware and no dish to aim. In my testing, an ordinary iPhone with no terrestrial signal can still send iMessages, and with WhatsApp (one of the supported T-Satellite apps), small richer payloads like voice notes and photos are available too. The phone needs sky and my patience.
The obvious story is that this is a cute stunt. The useful story is that a hostile channel forced the right architecture for agentic engineering. Satellite text is the degenerate case of coding from your phone: tens of seconds of latency, no terminal, no screen sharing, no reliable session, and very little room for explanation. It strips the interface down to intent and outcome. Everything else has to be durable server-side work, or it doesn’t exist.
I have been building Cortex for about a year. It runs continuously on a Hetzner server, with its own API, Postgres, a blue/green deployment path, and a roster of delegated coding agents across model providers. The design goal has been constant: one human commanding more agents than I can watch directly.
A text to Cortex is not a chatbot skin. It can enqueue a real dispatch onto a durable handoff queue. The job picks up the usual tooling, checks out an isolated git worktree, builds, runs tests, and can go through the same blue/green deployment path I use from a Mac or iPad. Merge candidates pass cross-model review, because agents grading their own homework is still a good way to ship confident garbage.
The second command is status: what’s open, finished, and landed. A text can start a Claude Code session on the server, seeded with my prompt and protected from process death. That session registers through Anthropic’s remote path and appears in the Claude mobile app already running. So I can send the mission with poor coverage, lose the live channel, and later pick up a rich session that has hours of work behind it.
That gives Cortex a two-speed loop. Command turns are terse: dispatch this, show status, start that session. The heavy work stays on the server, where bandwidth, disks, secrets, worktrees, logs, and review gates already live. The satellite carries intent and outcomes, and the execution path never crosses the satellite link.
The closest analogue is mission command: send intent, constraints, and authority; let the unit execute without a live uplink. A good dispatch is a mission order with acceptance criteria.
The architecture
The architecture is ordinary. The centre is a messaging spine: a channel-agnostic agent loop reading from a unified messages table. Each channel writes through an adapter. WhatsApp arrives through a server-side bridge. iMessage is more awkward. Apple does not provide the API I want, so inbound messages sync from the Messages database on a Mac I own. Outbound replies go to an outbox table. A small relay on that Mac drains the outbox over Tailscale and sends through AppleScript. Production delivery currently includes a Mac pretending to have hands. I have built more elegant things, but fewer useful ones. 🤷🏾♂️
A consumer process tails the message stream under a database lease. The lease is leader-elected across the blue and green deployment slots, so the intended invariant is that one leader handles the stream regardless of which slot is active. Every inbound message gets scoped before the agent turn exists. My registered owner DMs mount command tools. Threads with another human present are treated as external and fail closed. A third party in a group chat cannot dispatch my agents, because the server never gives that thread the dispatch tool. Tool mounting is the security boundary.
Long-running work becomes a durable job. The consumer acknowledges the command, the worker does the work, and a reconciler notices completion and sends a summary back over the channel that originated the order. Command in, acknowledgement out, durable work, result notification: that’s the contract.
What broke
Silence was the first thing that bit me. The messaging spine had its model pinned to a dated snapshot ID. One day that ID started returning 404 from the API. Every turn errored. Nothing was sent on any channel to anyone, and nothing looked wrong unless I read server logs.
In messaging, silence is a valid-looking state. It can mean latency, delivery delay, model failure, or nothing at all. Ordinary unit tests couldn’t have caught it; the code still did what it said. The verification standard is end to end, in the original context, on the real channel, as the real user. If I have not recently seen a message go phone to satellite to server to phone, I can’t claim the channel works.
Deployment theatre came next. A deploy succeeded, the new slot went live, and health checks passed. But the old slot stays alive on purpose (it is the rollback target) and the lease was still held by it, so the old consumer kept processing the stream on old code. I shipped fixes that were merged, built, deployed, and inert. 🤦🏾
I now have a one-command check that reports the active slot, the lease holder, and whether the leader is running current code. Merging to main is intent. Deploying is a step. What matters is which process is doing the work right now, and what code it’s running.
Duplicate side effects were quieter and more corrosive. Ticks retry. Consumers restart. Without a reservation step, a retried turn can send the same text twice. On Wi-Fi that is annoying. On satellite it burns scarce contact time and makes the system feel less trustworthy. Cortex now reserves before it sends: every outbound message claims a row in a delivery ledger keyed by an idempotency key, and only a successful reservation is allowed to transmit. Retries hit the ledger and stop. If a reservation succeeds and the transmit then fails, the message stays unsent rather than risking a double send — I chose dropped over duplicated. The pattern is familiar from payments, but the side effect here is human-visible communication. “Do not text me twice” is still a correctness property.
The physical world supplied a less abstract failure. The iMessage relay can be blocked by macOS Automation consent. In this failure mode, AppleScript calls into Messages return error −1712 until a person grants permission at the Mac’s own screen. There is no supported remote workaround I am willing to run. The operating system wants a human at a screen, and it is probably right. Every automation stack has one dependency that is not really remote. Know where yours is before you are standing on the wrong mountain.
The last lesson is about context. A terminal session silently carries your working directory, your branch, your environment. A text message carries none of that, and a sixty-second round trip means the system can’t ask clarifying questions. So the contract is honest instead of clever: a mission has to be self-contained, and the dispatched agent hydrates the rest server-side: it has the repos, the retrieval, and the project history, and it resolves “fix the failing preflight check” into a worktree and a plan where the state actually lives. Intent comes from the phone, state doesn’t.
The loop is the unit
That is why the satellite path matters: as we scale the number of agents each human commands, human time, attention and latency become the hard bottleneck in the system. That’s why the unit of work isn’t the prompt; it’s the loop around it — how work is discovered, handed off, verified, persisted, and resumed. The human shouldn’t sit inside the inner cycle waiting for a model to think. The human sets intent, receives exceptions, and re-enters only when judgement is needed.
A bandwidth-starved channel makes that doctrine non-negotiable, because it forbids synchronous hand-holding, rich state on the client, or any design that dies when the human disappears. What it forces (asynchronous work, durable jobs, idempotent side effects, scoped capabilities, real end-to-end probes) are the properties the system needs at full bandwidth too.
The phone is becoming the command surface for organisations whose workers are agents. In that world, the dashboard is not where work happens, but where I go when something violates the loop. Missions go down. Exceptions and results come up. The richer interfaces still matter, but they are for inspection, repair, and judgement, not for babysitting every turn.
The next time I walk out of coverage, I will be carrying a command surface for Cortex in my pocket: two messaging channels, server-side workers, review gates, deployment paths, and rich Claude sessions accumulating for later. The satellite did not make Cortex more capable. It removed every path that was not durable enough to trust.
Narrowing the command surface meant the system had to get more honest and resilient. The paradox of the hostile channel is that by stripping away the bandwidth, it gave me back my attention. This summer, I’m looking forward to building from the backcountry with nothing but a pen, a paper notebook, and a satellite link to an engine I can finally trust to do the heavy lifting.





