I spent eight days on Crete in July and did not take a laptop. Across four products and my own infrastructure, git recorded 186 commits between 20 and 27 July.
I was steering, not working. A few minutes under the sunshade to say what should happen next, then back into the sea. The agents did the typing.
The setup behind that: Claude Code CLI on a Proxmox container at home, Tailscale for the network, a Discord bot as the interface, and rsync over the tailnet for deploys. I wrote up how that architecture works in an earlier piece. This is what happened when I took it on holiday.
What actually runs where
phone (on the tailnet)
-> Discord
-> JARVIS (bot on the devbox at home)
-> Claude Code CLI, in that project's own directory
-> the devbox working copy is the source of truth
-> deploy script rsyncs it to the VPS over Tailscale SSHThe devbox is a container at home holding the working copies of every repo, authenticated with my GitHub access. Claude Code runs there, never on the phone. JARVIS is a small TypeScript bot that turns Discord messages into Claude Code turns and posts the results back.
Note what is missing from that chain. Deploys do not pull from GitHub. The repos are private and I am not putting GitHub credentials on a VPS, so the deploy scripts rsync from the devbox working copy instead. Git and GitHub carry history, not releases. One of the four products below ships a completely different way, which I will get to.
Tailscale is not a convenience here, it is the only door. Public port 22 is closed with ufw on all three Hetzner boxes, so SSH and SFTP arrive over the tailnet or not at all. The phone is a node on that tailnet like every other machine.

One Discord channel per project, pointed at that project's directory. Eighteen of them:
{
"<channel-id>": {
"name": "photoinput",
"cwd": "/home/fnovak/projects/apps/photoinput/"
},
"<channel-id>": {
"name": "infra",
"cwd": "/home/fnovak/projects/infra/"
}
}Every Discord thread keys its own Claude Code session, so conversation context is isolated structurally rather than by me remembering to reset something. Working directories are not isolated the same way. Two threads in one channel point at the same checkout, so running them in parallel on one repo is a good way to make a mess. I run one at a time per project.
The phone is a keyboard on a private network.
Day one: what I instrumented first
Twenty-three commits went into infrastructure before any feature work. The ones that mattered were about seeing what was happening from a small screen.
A disk-space watchdog that posts to Discord and trims automatically. !mem and !disk as one-line commands. Then, immediately after, a commit reformatting !mem into phone-friendly labelled lines, because the first version was fine on a monitor and not on a phone.
Two decisions that day mattered more than the rest.
Raising the app-channel timeout to 120 minutes, so a long autonomous run is not killed while I am in the water.
Adding a !fable prefix to force the premium model for a single turn. Fable is the expensive tier. I use it as a pre-merge reviewer rather than as the builder, and that shows up in the commit log of every project below.
I also closed a security gap that had nothing to do with the trip: Tailscale's keyless SSH turned off on the VPSes, leaving key-based sshd as the only way in, with the deploy trust boundary and tailnet ACL written down.
Day two: the escape hatch
The bot cannot do everything. The obvious hole is anything the agent has to do to itself, and updating the Claude CLI is exactly that. For that you need a real terminal.
So on day two I wrote the walkthrough for reaching the machines by hand from Android: install Tailscale, log in, then SSH with a key from Termius. The order matters. The devbox and the Proxmox host have no public IP, so without the tailnet the SSH client simply times out. I used it once, to update the CLI.
The projects
isitgood.auction: a retention feature end to end in a day

isitgood.auction reads an eBay auction and tells you whether the thing is worth bidding on.
On 21 July the repo took 28 commits, most of them one retention feature end to end: a spec covering both halves, a bid ceiling and watched auctions, then an eleven-task plan and the build. A Fable pass at the end caught two integration defects, fixed before merge.
Four days later I spent a session on something less glamorous: eight fixes to the parser that decides whether a seller has ruled a defect out. Scoping the negation guard to a continuation chain. Inverting the token alphabet to end a class of boundary bug rather than another instance of it. Clamping the doubt scan to its own sentence. Widening a whitespace class to cover non-breaking spaces.
That is desk work. It happened on a phone and the fixes held.
PhotoInput: found on the device it is built for

PhotoInput tells you what is wrong with a photo and fixes it in one click, entirely in the browser.
One commit that week reads: switch the scoring engine from a phone, no devtools. A day later, a tap that turns using the product into ground truth, so ordinary use generates the labels that grade the scorer.
Two defects found that week only exist on a phone. The editor was unreachable on a top-scoring photo. The capabilities strip looked like links and did nothing. Neither shows up in a desktop browser at 1440px. I found them because I was holding the product in the conditions it is used in.
Tale Album: hardening the streams

Tale Album turns a text story into an illustrated book. I wrote up how it got built in a weekend last spring.
That week was mostly about not stranding the user. Timeouts on async calls that had none. A page-one preview gate, so you confirm the visual language before paying for a whole book. Streaming routes hardened against a cancelled client. A full polling fallback for all five streaming operations.
Maybe the mobile connection surfaced those, maybe not. Streaming does get tested harder when the person driving it is on a phone.
Pyramid Wordle: four public releases, and the exception

It went from 1.6.0 to 1.7.4 in five days. A share loop with an anti-phishing gate and accessibility fixes from a Fable review. Tabbed settings and a trophy system. Haptics on the keyboard. Weekly word pools rebuilt to be rare but real.
This is the product that does not use the chain above. It ships through GitHub Actions: Docker build, DigitalOcean registry, Kubernetes rolling deploy. Android APKs moved out of the repo and into release assets, and the build now refuses to build if it finds an APK sitting in the public directory, because Vite copies that directory into the bundle and Tauri embeds the bundle into the next APK. One 62 MB file left in the wrong place made a 56.8 MB binary.
Same phone, same Discord thread, different pipe at the far end.
Days six and seven: the context caught up with me
Nothing broke that week. One thing did get costly in tokens.
I ran a Fable review in a thread I had let grow long, without clearing the context first. Every turn re-sends the whole conversation, so a thread near its window ceiling burns a multiple of what a fresh one does. Nothing in the interface surfaced that, so I did not notice until the usage did.
The agents spent the rest of that day and the next on the fix, and it is the best thing that shipped all week.
Context occupancy, made visible. A small module reads the newest usage record straight from the session transcript on disk. Local accounting, no API call, about four milliseconds on a one-megabyte transcript. It shows in the footer of every reply, and !ctx gives it on demand for zero tokens. It replaced a warning keyed on per-turn input above 50k, which measured a different thing and never fired on the threads that were actually full.
!compact and !clear. Summarise in place and keep the thread, or drop the history and keep the thread's directory and model tier.
The bug: my occupancy meter assumed a 200k window, the models ship 1M
My own accounting module had the context window hardcoded at 200,000 tokens. Opus 5, Sonnet 5 and Fable 5 all report a 1,000,000 token window. So every long thread was being measured against a fifth of its real size, occupancy read up to five times too high, and I was being nagged to compact far too early. To be clear about where the fault sat: this was my constant, not a limit in Claude Code.
The window now follows the model recorded on the newest turn. Unknown models fall back to 200k, because over-warning costs a needless compact and under-warning walls a thread mid-task.
The second bug was worse in practice. The model choice was never sticky. Three separate code paths each re-derived the model from the channel config, so a tier picked with !sonnet or !fable applied to exactly one turn. Unprefixed follow-ups silently reverted. Button-gate continuations always ran the channel default. !status read the config rather than the history, so it confidently reported the wrong model.
One asymmetry in the fix was deliberate. !sonnet and !opus stick to a thread. !fable does not. The premium tier is a consultant you dispatch for a hard problem, and pinning a thread to it would bill premium reasoning for "now commit that".
What it cost
The development setup cost nothing beyond the Claude subscription. No API spend for the dev loop, no cloud development environment. The devbox is a container on hardware I already own, and the servers were already running. The products buy their own inference, which is a separate line and a separate discipline.
What the incident cost was quota, not money, and it cost it invisibly. That number is now on every reply.
What steering actually means
I pointed at outcomes, looked at what came back, and said yes or again. The parts of the job that needed a desk turned out to be parts I had already stopped doing.
Location stopped being a variable. Eight days is one data point, and Crete in July has good signal. Ask me again after a month somewhere worse.
Questions people asked
How do I run Claude Code from my phone?
Claude Code runs on a home server, never on the phone itself. A Discord bot turns Discord messages into Claude Code turns in the right project directory and posts the results back. The phone is a keyboard on the private network that reaches it.
What is JARVIS?
A small TypeScript bot that turns Discord messages into Claude Code CLI turns in a project's own directory, then posts the results back into the Discord thread.
Why does this setup depend on Tailscale instead of a public server?
Public port 22 is closed with ufw on every server involved. SSH and SFTP only arrive over the tailnet. There is no other way in, from the phone or anywhere else.
What is the !fable command for?
It forces the premium model tier for a single turn. It is used as a pre-merge reviewer rather than the day-to-day builder, which is why it does not stick to a thread the way !sonnet and !opus do.
The infrastructure repository is private for now.