What works with what
Three things have to line up before a single command runs: a transport to carry it, a station to run it, and a controller to publish it from. This page is all three, and which combinations actually work.
The tables are generated from one source, so they cannot disagree with each other. Where a row says something is unproven, that is the honest state rather than modesty.
Every transport is one of two shapes
This is the distinction the whole design rests on, and it decides more about an estate's answer than any other fact on this page.
| pigeonhole | A dead letter drop. You cannot reach the far side, the far side cannot reach you, and both can reach one agreed place. Both sides dial out; neither ever accepts a connection |
| intercom | You can reach the station's endpoint directly, so there is no drop in the middle. Unusual, because the whole tool exists for when you cannot |
Everything shipped is a pigeonhole. git, the relay, a file share, Azure Blob, an object store and a bundle are all the same shape: something is left somewhere, and collected later by the other side. The words come from the station itself - pigeonhole.sh and intercom.sh have carried them since before this page existed.
Why the shape matters more than the speed
The obvious difference is latency, and it is the less important one.
A pigeonhole keeps the gates where they belong. The step is already on the far side, the station reads a request that names it, and the station decides whether to run it. # heliograph-mode: read-only is a property of a file the operator can read before anything happens.
On an intercom the script travels with the request. There is no git on the far side to have planted it, so the caller ships the thing to be run - and at that moment the mode header stops being a control and becomes a claim the caller makes about its own file. What is left is the function key and an IP allowlist. That is a real trade and the intercom page makes it in full; it is not a worse transport, it is a different security model.
A pigeonhole also needs nothing to be reachable, ever. No inbound port, no endpoint, no tunnel. That single property is what makes this permissible in estates where a reverse connection would be a breach, and it is why a raw TCP transport was dropped rather than built.
Pick what you have
Pick what you already have. Anything that cannot work with it dims, and says why.
TransportThe channel
StationThe far side
ControllerYour side
Every transport
| Transport | Shape | Your side | The far side | What it is |
|---|---|---|---|---|
| git | pigeonhole | proven | proven | A private repository is the channel in both directions. The only transport that can bring the station a newer copy of itself. |
| relay | pigeonhole | proven | works | Both sides dial out over ordinary HTTPS. Needs no git host, no storage account and no VNet - and needs heliograph-seal on the far side, which no other transport does. |
| file share | pigeonhole | proven | proven | The cheapest there is, where both machines already mount the same directory. The mount is the credential. |
| Azure Blob | pigeonhole | with a condition | works | Reached through drop.sh in the station payload rather than through the heliograph binary. It is what the Azure Function App host uses. |
| object store | pigeonhole | works | none yet | S3-compatible: AWS, R2, MinIO, B2, Spaces, Ceph. The CLI drives it and no station can read it, so the combination cannot work yet. |
| bundle | pigeonhole | works | none yet | The only thing that makes air-gapped literally true. A person carries the file. No station can read one yet. |
| intercom | intercom | works | with a condition | The one case where you CAN reach the station. The script travels with the request, so heliograph-mode stops being a control and becomes a claim the caller makes about its own file. |
Every station
| Station | Status | Flavour | Transports | What it is |
|---|---|---|---|---|
| An operator's terminal | proven | bash, PowerShell | git; relay; file share; Azure Blob; intercom (the station must be reachable from your side) | Still the best host when there is a willing person. No infrastructure request, and start.sh prints its own preflight to somebody who can read it. |
| Docker | proven | bash | git; relay; file share; Azure Blob | CI builds the image and runs a loop in it. The image plants the payload itself, so a transport with nothing to clone still has one. |
| Kubernetes | proven | bash | git; relay; file share; Azure Blob | The same image, one replica. CI applies the manifest to a real cluster. |
| systemd, launchd, setsid | proven | bash | git; relay; file share; Azure Blob | Survives a logout. A detached process inherits nothing from the shell that installed it, so the variables go in .station-env. |
| Windows scheduled task | proven | PowerShell | git; relay (the PowerShell relay transport is not written yet); file share; Azure Blob (through the bash station) | CI registers the task and reads it back. .station-env is the only way to give it a token - there is no EnvironmentFile on Windows. |
| GitHub Actions, Azure Pipelines | written, never run | bash | git (the push is the trigger, which is the whole point) | Often the one machine in an estate that can already reach both sides. Git-only by design: the push is the trigger, and that is what keeps the latency down. |
| Azure ACI, Web App, Container Apps Job | proven | bash | git; relay (the two key files have to be put on the host); file share (the share has to be mounted); Azure Blob | Deployed live, then torn down. The templates take a transport through two maps rather than a parameter per transport. |
| Azure VM | proven | bash | git; relay (the key files, and a git host reachable once at first boot); file share (the share has to be mounted); Azure Blob | Deployed live on a Standard_D2s_v3. The exception to everything else here: a bare VM has no image, so git clone is how the toolkit arrives whatever transport carries the logs. |
| Azure Function App | written, never run | bash | git (there is no git in the image); Azure Blob (through pigeonhole.sh); intercom (the one host with a reachable endpoint) | A timer, not a loop. Validated and never deployed. It is the host the intercom was written for, because a Function App has a public endpoint while sitting inside the VNet. |
| ECS Fargate, Cloud Run, anything else | none yet | bash | git (against the contract, never run); relay (against the contract, never run); file share (against the contract, never run); Azure Blob (against the contract, never run) | Recipes against the host contract, not templates. Issue #5 settled that deliberately: a template that has never started a station spends the credibility of the ones that have. |
Every controller
| Controller | Status | Transports | What it is |
|---|---|---|---|
| Linux, macOS or Windows | proven | git; relay; file share; object store; bundle; intercom (through intercom.sh) | One static binary, amd64 or arm64, no runtime. Only the git transport shells out to anything - the rest are the binary alone. |
| An AI agent, over MCP | proven | git; relay; file share; object store; bundle | heliograph mcp serves the same commands as typed tools over stdio. The gates do not move: a tool call publishes a request, and the station still decides whether to run it. |
| A machine with git and no binary | never checked | git (write the request by hand, push, read the log back) | Already possible and documented nowhere. A request is a key: value text file, so a laptop that permits git and refuses new binaries can write it, push it, and read the log back out of the repository. |
| Claude Code on the web | never checked | git (only where the session proxy allows the host); relay (the relay's own domain is unlikely to be on the allowlist) | A cloud sandbox is a real shell, so the binary simply runs. The session proxy is what decides: private hosts and self-hosted infrastructure are refused by the allowlist, so a GitHub-hosted transport repo is the combination with a chance. |
| Android, through Termux | never checked | git (never checked); relay (never checked); file share (never checked); object store (never checked) | The arm64 Linux binary is already built and git is a Termux package, so this is a check rather than a port. Nobody has run it. |
Two things the matrix cannot show you
A transport needs both halves. The object store and the bundle are implemented on your side and have no station side at all, so the combination cannot work however the rows line up. That is why the transport table has a column for each side rather than one status.
Proven is not the same as written. A station that has never run is a template that looks authoritative, and shipping twenty of those would spend the credibility of the ones that work. The host contract is published so you can judge an unlisted host yourself.
What might be added next, what is deliberately refused, and why, is on the roadmap.