---
title: "Bootstrapping a New Agent Station, and Giving It a Phone"
canonical: https://dxdev.com/blog/bootstrapping-an-agent-station-and-giving-it-a-phone/
datePublished: 2026-04-30
---
Halfway through a normal Thursday I decided my desktop should stop being "a place I run Claude" and become a real agent station: its own repo, its own identity, its own tooling, and a way to dispatch work to it from my phone. By that evening it had all of those, and I could SSH into it from an iPhone over Tailscale and kick off a job while standing in line at a store.

The reason I bother to write this up is not the SSH trick. It is the thing I learned the hard way earlier that same day: if you run agents across more than one machine and you do not give each machine an explicit identity, the agents start guessing about what they are allowed to touch. And a guessing agent with shell access and a git remote is exactly the thing you do not want.

## The close that started it

That morning I closed a production ticket from the wrong machine. The code lived in one of my long-lived git clones; the session I closed it from was rooted somewhere else entirely. I drove the whole thing from the outside over the JIRA REST API and the git CLI: merge to the release branch, push, clean up the feature branch, fire the JIRA transition, verify in a browser. It worked. It was also my first cross-station deploy, and it shook loose seven small frictions in a single sitting.

A few of those frictions were pure environment mismatch. Windows has no `/tmp`, so writing a scratch JSON payload to `/tmp/whatever.json` just failed and I had to use `$TEMP`. Inline Python inside a bash heredoc died on the quoting once you nest an f-string inside a heredoc inside a Windows shell. `git branch -d` refused to delete the local feature branch because it carried the merge commit the remote did not have yet, so the fix was to delete the remote branch first, then the local one.

None of those are interesting on their own. Every one of them was a default waiting to be encoded. The session had no idea which machine it was on, what that machine could reach, or which conventions applied here versus there. It was acting like a generic shell, because nothing told it otherwise.

So I decided the desktop needed to be a named thing with a written-down identity.

## What a station actually is

I cloned a fresh repo for it and seeded four things: a README, a `CLAUDE.md`, a stations doc, and a scripts folder. The `CLAUDE.md` is the part that matters. It is a per-machine identity doc, and it answers three questions before any agent does any work:

- **What is this station?** Mine declares itself an operational orchestrator. Not a knowledge store, not a code host. Its job is to drive other things.
- **What can it reach?** Local repos on this disk. Git push to my own GitHub org over SSH. An SSH hop to my always-on Linux box. Local shell, local Python, local Node.
- **What can it NOT reach?** This is the load-bearing half. No Google Drive. No email. No production servers, no hosted services, no paid APIs without explicit credential setup.

That "cannot reach" list is the whole point. An agent that knows its own boundaries does not invent a path to a production database because it sounds plausible. It does not assume it can read your email because email exists in the world. The identity doc converts "the model will probably behave" into "the model has been told, in writing, what authority it has here."

I also wrote a separate stations doc that holds the access matrix across every machine I run agents on: which station owns operations, which holds knowledge, which sits always-on and reachable. When I promoted the desktop to operational primacy, I updated that doc in the same beat so the topology was canonical and not folklore. The next agent that boots on any of these machines reads one file and knows the map. It does not reconstruct it from clues.

## The skill family and the tooling

A station with an identity but no hands is just a config file. So the same day I built out a small skill family for the common loop: opening a ticket, syncing state, picking what is next, logging what happened, closing out. Behind the skills sit plain Python scripts: a session tracker, a work-log writer, and a board generator that renders a live dev board so I can see every clone's state at a glance instead of walking ten of them by hand.

The split is deliberate. A skill is a markdown procedure, the human-readable "how we do this here." It dispatches to executable Python that does the actual work. That separation is what lets the seven frictions from the morning become permanent fixes instead of notes I promise myself I will remember. The merge-order lesson, for example, does not live in my head anymore. It is the only entry point the git wrapper exposes, so you physically cannot do it backwards.

There is a second, sharper reason the tooling has to be raw Python and not my usual token-saving CLI wrapper. I run a wrapper that compresses verbose command output to save AI tokens, and it saves a lot of them. But it is structural: it recognizes a git log or an API response and hands back a tidy summary instead of the bytes. That is fine for a human reading along. It is a correctness bug for an agent that needs to verify a merge SHA or read back an exact API field, because the agent is parsing, not reading. So the station's scripts are banned from that wrapper. They use raw subprocess and raw HTTP. On the paths that must be authoritative, lossy-but-pretty is worse than nothing.

## Giving it a phone

The last piece was reach. I installed OpenSSH Server on the Windows desktop, locked it to key-only auth, pointed the shell at Git Bash, and exposed it over Tailscale so nothing is sitting open on the public internet. Then I put Termius on my iPhone and added a couple of short aliases so the desktop reaches feature-parity with my always-on Linux box for dispatching work.

I jogged to a store mid-day. While I was out, a teammate approved the ticket I had been waiting on. In the old world that approval sits until I am back at the keyboard. In the new world I could have opened Termius, SSHed into the desktop over Tailscale, and dispatched the next step from the sidewalk. "Send work to my desktop" became a thing I can do from my phone, with my keys, over a private network, against a station that knows exactly what it is allowed to do.

That last clause is why the identity doc has to come first and the phone second. A phone that can SSH into a machine whose agent has no sense of its own boundaries is not a convenience. It is a way to fire an unbounded agent from anywhere. The order matters: name the station, write down what it can and cannot touch, and only then hand it a phone.

## Related

- [When Working Notes Become Operational Infrastructure](notes-become-infrastructure-when-ai-helps-everywhere): when personal tooling crosses the line from notes to infrastructure
- [SSH-From-Phone Over Tailscale: The Failure Modes Nobody Warns You About](ssh-from-phone-over-tailscale-three-quiet-failure-modes): the quiet failure modes in the same phone-over-Tailscale setup
- [Write the Handoff Before You Close the Session, or Pay to Rebuild State Next Time](session-handoff-doc-as-first-class-deliverable): making context portable across machines and sessions
- [When AI Makes Analysis Cheap, Your Decision UI Becomes the Bottleneck](jira-cockpit-ai-makes-analysis-cheap-decision-ui-is-bottleneck): the UI surface that matters once agents can reason for you
- [I Was About to Write a daily_digest.py. The Platform Already Shipped It.](dont-build-infrastructure-the-platform-already-ships): building station tooling only where the platform genuinely falls short
- [My AI agent's memory was per-folder, so my four clones never learned from each other](git-tracked-agent-memory-not-per-folder): the cross-clone isolation problem that station identity is meant to solve
