Homing Pigeon
Works with Muse, Grok, ChatGPT, Claude, Gemini, Copilot, etc.
Your cloud SI is smart, but it can't touch your computer. Homing Pigeon fixes that: tell your SI to send a signed job, it executes on your machine (Linux/macOS/FreeBSD/Windows), and the results come back so your SI can iterate — run the tests, read the failure, fix, repeat. From your phone, from anywhere.
Think "GitHub self-hosted runner, but the control plane is email." Python 3.9+, standard library only, no dependencies. (Email is just the first transport — the protocol doesn't care about the pipe. See TRANSPORTS.md.)
⚠️ USE AT YOUR OWN RISK. This tool hands full administrative control of the machine to whoever holds the shared secret. It reads email and executes what it finds — blindly, as a privileged user. Anyone who can produce a correctly-signed email owns the box: install software, read files, add users, pivot to your network. Only run this with a dedicated throwaway account on a machine you can afford to lose, and read PROTOCOL.md before you deploy it.
What this is for
Homing Pigeon is a personal remote-development agent. The idea is simple: the most powerful computer you have access to shouldn't stop being useful the moment you walk away from your desk — and neither should your SI assistant. Send a job from your phone, let the big machine run the tests, crunch the data, or grind through the refactor, and read the results when they land in your inbox.
It belongs to the same family as GitHub self-hosted runners, VS Code Remote tunnels, and Tailscale — legitimate tools for administering your own hardware remotely. And like all of those, its power is the point, not an accident: whoever holds the shared secret has full control of the machine. That's exactly why this project states its threat model plainly instead of burying it. Keep the secret like a password, use a throwaway account, and only ever install the agent on machines you own and operate.
SI (cloud) Your machine
──────────── ──────────────────
send_job.py ──sign──▶ job email ──▶ runner.py
│ │ │ poll IMAP
│ │ │ verify HMAC
│ │ ▼
│ │ run script
│ │ │
│ result email ◀──┘ sign
▼ │
verify result ◀──────────┘
Large inputs don't go through email: your SI uploads them to file hosting and the job email carries download URLs (with optional SHA-384).
Why email? Because it's the only transport with all five properties on day one: reachable from anywhere, outbound-only polling (no inbound ports — NAT-proof), authenticated sender, at-least-once delivery, and zero infrastructure to operate. And because the protocol doesn't care about the pipe: Cloudflare Queues, NATS, MQTT, Redis Streams and friends all satisfy the same contract. Email is just Transport #1 — see TRANSPORTS.md.
The Oracle: answers, not output
v1 sends full job output back to the SI. The v2 proposal (spec'd in PROTOCOL.md, not yet implemented) flips this: your machine becomes the Oracle — a black box the SI can question but never inspect.
The SI asks "did the tests pass?" or "does the output contain secrets?" and gets back HMAC-signed attestations: proofs about the output that reveal nothing else. It's the zero-knowledge shape applied at the trust boundary — prove things about your data without showing your data. The raw output never leaves your disk.
Cloud SI Your machine
──────── ────────────────────
"Did the tests pass?" ──signed──────▶ ┌─────────────┐
│ The Oracle │
"Any secrets in there?" ────────────▶ │ │
│ output stays│
┌─────────────────────┐ │ on your disk│
│ signed attestation │ ◀──signed── │ │
│ { exit_zero: true, │ │ answers only│
│ no_secrets: true } │ │ what policy │
└─────────────────────┘ │ allows │
└────────────┘
The SI learns answers. Never the raw output.
Queries are declared upfront (no adaptive probing), each job gets a
privacy budget bounding total leakage, and the disclosure ceiling is
set by you in config — never by the SI. For debugging,
oracle_enabled: false drops back to the plain v1 pipe.
The Oracle also grows two optional capabilities (spec'd, not yet built):
- Local inference. Point it at a model running on your own box and
the SI can
askquestions in plain English — "why did the tests fail?" — answered locally, with only the answer crossing the boundary. The endpoint must be loopback (fail closed), and every answer is truncated, redacted, and secret-scanned before it's sent. Your data gets intelligent answers without ever leaving the machine. - Execution confinement. Tell the Oracle the one folder jobs may
touch (
workspace_root) and it attests the job stayed inside —confined_to_workspace, signed like everything else. Today's enforcement is attested; OS-level sandboxing is the stronger future.
Full spec in PROTOCOL.md (sections 9–12).
Layout
PROTOCOL.md— the full wire protocol (message formats, signatures, verification rules, security model).TRANSPORTS.md— email is just Transport #1. Alternatives that satisfy the transport contract: Cloudflare Queues (the pick), NATS, MQTT, Redis Streams, Postgres, Durable Objects.SAFEGUARDS.md— how to run this without getting burned: the non-negotiables, containment, the Oracle as a safeguard, and what to do when something goes wrong.runner/runner.py— the agent. Polls IMAP, verifies, executes, replies via SMTP. No dependencies beyond Python 3.9 stdlib.runner/install.py— installs it as a boot-start service: systemd (Linux), launchd (macOS), cron @reboot (FreeBSD), Scheduled Task (Windows).runner/config.example.json— copy toconfig.jsonand fill in.cloud/send_job.py— cloud-side helper: builds signed job emails, verifies result emails. (Your SI uses this logic when dispatching jobs.)tests/test_runner.py— end-to-end tests with fake IMAP/SMTP servers.RUNBOOK.md— step-by-step instructions (written for a 5-year-old): cloning, pushing, cutting a release, installing, sending a job.examples/hello-world.json— unsigned example job payload.
Setup
Easiest: download a release. No Python needed — each release ships standalone binaries built by GitHub Actions:
| Platform | Download |
|---|---|
| Windows (x64) | homing-pigeon-windows-amd64.zip → homing-pigeon.exe + install.exe |
| Linux (x64, Debian/RHEL/Ubuntu/…) | homing-pigeon-linux-amd64.tar.gz → homing-pigeon + install |
| macOS (ARM64) | homing-pigeon-macos-arm64.tar.gz → homing-pigeon + install |
The Linux binary is built on ubuntu-22.04 (glibc ~2.35). It runs on Ubuntu 22.04+, recent Debian, and most current distros. Older systems (Debian 11, Ubuntu 20.04, RHEL 8) are not supported.
- Extract the archive on the target machine.
- Run
install(orinstall.exeon Windows). It copies the runner binary, writes a defaultconfig.json, and registers the boot-start service (systemd / launchd / cron @reboot / Scheduled Task). - Edit
config.json: setmailbox_userandmailbox_password— your dedicated Gmail address + app password, in plaintext, on purpose (your explicit choice for this throwaway account). Leaveallowed_senderempty — the runner then only accepts job mail from the account it is logged into (hard rule). - When you want an SI to send jobs, paste that same app-password string to it in chat. It doubles as the HMAC shared secret.
From the Python packages (needs Python 3.9+): each release also
ships an sdist (.tar.gz) and a universal wheel (.whl) — the easiest
path on machines where Python is preinstalled, like a Raspberry Pi:
- Download the
.whlfrom the release. pip install pigeon_home-1.2.0-py3-none-any.whl— this installs three commands:homing-pigeon,homing-pigeon-install, andhoming-pigeon-send-job(the cloud-side helper).homing-pigeon-installcopies the runner, writes a defaultconfig.json, and registers the boot-start service.- Edit
config.jsonand share the app-password string with your SI, same as above.
See The $35 pigeon coop for the full Raspberry Pi walkthrough.
From source (needs Python 3.9+):
- Create a dedicated Gmail account for the runner, e.g.
myname-runner@gmail.com. Never use a personal inbox. - Enable 2-Step Verification, then create an app password (Google Account → Security → 2-Step Verification → App passwords).
- Copy
runner/to the target machine. - Copy
config.example.jsontoconfig.jsonand fill inmailbox_userandmailbox_passwordas above. - Test one cycle:
python3 runner.py --config config.json --once. python3 install.pyregisters the boot-start service for your platform (see below).
Per-platform install
| Platform | Mechanism | Notes |
|---|---|---|
| Linux | systemd user unit (--system for system-wide) |
systemctl --user enable --now homing-pigeon |
| macOS | launchd agent | ~/Library/LaunchAgents/com.homing-pigeon.runner.plist |
| FreeBSD | cron @reboot |
simple; wrap in rc.d if you prefer |
| Windows | Scheduled Task | runs at startup as SYSTEM |
install.py --dir DIR chooses the install location (default
~/.homing-pigeon); install.py --uninstall removes the service.
Run it in a VM
For testing — and honestly for everyday use — run the agent inside a Linux VM on your own hardware rather than directly on the host:
- Snapshots beat regret. About to hand an SI the keys to a machine? Snapshot first. If a job goes sideways, roll back in seconds.
- Containment. The agent only sees what the VM sees. Your host, your files, and your other projects stay out of reach of anything a job does.
- Disposability. Experiment freely: spin up a fresh VM for a new idea, tear it down when you're done. Nothing lingers on the host.
Any hypervisor works — KVM/QEMU, VirtualBox, Hyper-V, VMware. Give the VM internet access (NAT is fine) so it can reach Gmail's IMAP/SMTP, install the Linux release binary inside the guest, and point your jobs at it. The host never needs to know.
The $35 pigeon coop (Raspberry Pi)
Every homing pigeon needs a coop — a cheap, always-on box to come home to. A Raspberry Pi is the perfect one:
- Always on, barely sipping power. The runner just polls Gmail on a loop; a Pi idles at a few watts while your laptop sleeps.
- A machine you can afford to lose. This project's whole security posture is "dedicated throwaway account on a box you don't care about." A $35 Pi is that box.
- Python's already there. Skip the frozen binaries entirely:
pip install pigeon_home-1.2.0-py3-none-any.whl
homing-pigeon-install # registers the boot-start service
# edit ~/.homing-pigeon/config.json, then test one cycle:
homing-pigeon --config ~/.homing-pigeon/config.json --once
Grab the .whl (or the .tar.gz sdist) from any
release.
Your SI sends jobs from the cloud; the Pi in the corner executes them.
That's the whole trick.
How scripts run
Jobs carry a script (or script_url) plus an optional interpreter
list, e.g. ["bash"] or ["pwsh", "-File"]. The runner writes the
script to the job's work dir and runs <interpreter...> script <args...>.
Defaults: PowerShell on Windows, sh elsewhere; override per-install
with default_interpreter in config.json, per-job with interpreter
in the job (send_job.py new --interpreter '["bash"]').
Sending a job (any SI)
The runner is SI-agnostic: anything that can send email as the runner's Gmail account and sign the body can drive it — your SI of choice, a cron script, whatever. The SI needs the shared secret string from step 5 (which is the app password itself, your choice). Then:
python3 cloud/send_job.py new --kind ping --secret <the-string>
# prints: subject line, blank line, signed body -> send as email to the runner mailbox
python3 cloud/send_job.py new --kind exec --script-file job.sh \
--interpreter '["bash"]' --file data.csv=https://... --timeout 300 \
--secret <the-string>
Or just paste the string to your SI in chat — it composes, signs, and sends the job email itself, and reads the result email when it arrives. Large files are uploaded to cloud storage first; the job email carries the URLs.
Verify a result body:
python3 cloud/send_job.py verify --secret <the-string> result.txt
Testing
Watch before you trust. The runner is fully inspectable — these flags exist so you can audit everything it would do before it does it:
python3 runner/runner.py --config config.json --once --dry-run --verbose
# verifies signatures, prints what each job WOULD do, executes nothing,
# sends nothing, changes nothing (mail stays unseen)
| Flag | What it does |
|---|---|
--dry-run |
Verify and describe; execute nothing, send nothing |
--workers N |
Concurrent job executions (default 1) |
--verbose |
Debug logging: every accept/reject decision |
--pdb |
Drop into pdb on unhandled exceptions |
--no-send |
Execute, but log results instead of emailing them |
--once |
Single poll cycle, then exit |
--poll-interval SEC, --timeout SEC, --max-output-bytes N |
Override config |
--no-oracle |
Disable the Oracle safety layer (v2) |
install.py accepts the same flags and bakes them into the installed
service, so the agent on boot runs exactly what you tested:
python3 runner/install.py --verbose --poll-interval 30.
python3 -m pytest tests/ # or: python3 tests/test_runner.py
The suite drives the real runner code against fake IMAP/SMTP servers: ping roundtrip, script execution, timeout kill, poison-job rejection, sender/signature/tamper rejection.
End-to-end without your SI:
python3 cloud/send_job.py new --kind ping --secret <the-string>→ copy the subject + body.- From your own Gmail (matching
allowed_sender), send it to the runner mailbox. - On the target machine:
python3 runner.py --config config.json --once. - A result email with
stdout: "pong"should arrive in the mailbox.
Security notes
- The runner is a full-trust execution environment: a valid signed job email means arbitrary code as a privileged user. The app password sits in plaintext in config.json and doubles as the HMAC secret (your explicit choice: throwaway account, fresh box). Anyone holding it gets Gmail access and job-signing ability. Don't use this setup with an account you care about.
- Every job is HMAC-SHA-384 signed; sender address, timestamp window, and replay protection (seen-set) are all enforced before anything runs.
- Delivery is at-least-once: if a result email fails to send, the job stays unseen and will be re-executed next cycle. Make jobs idempotent or check the seen behavior accordingly.
- Hardening ideas for v2: encrypted credentials at rest,
per-job encryption, result file upload for large outputs — plus the
Oracle (spec'd in PROTOCOL.md): result policies, signed attestations,
and a query budget, so a cloud SI learns answers about your output
without ever seeing it.
oracle_enabled: falsedisables it anytime.
Status
v1.2.0. Runner rewritten in cross-platform Python; crypto tested
(sign/verify roundtrip, tamper rejection, CRLF tolerance); end-to-end
suite passes against fake mail servers (103 tests). Debug/audit flags
(--dry-run, --verbose, --pdb, --no-send), concurrent job
execution (--workers), and SHA-384 release hashes. Releases ship standalone
binaries (Windows/macOS/Linux) plus a Python wheel and sdist. The v2
protocol — the Oracle, privacy-preserving results via signed
attestations — is spec'd in PROTOCOL.md and awaiting implementation.
Not yet proven against a live Gmail mailbox on all four platforms —
that's the next proving ground.
Metadata
Release files for pigeon-home 1.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pigeon_home-1.2.0.tar.gz | 46.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pigeon_home-1.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 76.3 kB
Release files / pigeon_home-1.2.0.tar.gz
| Download URL | pigeon_home-1.2.0.tar.gz |
|---|---|
| Size | 46.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
a1f16818144155cf1fd2e23aef0ad048511d94eb8526b5e1ce7b8a098088422a
|
|
BLAKE2b-256 checksum How to use checksums |
8efb753632bad6e0e9edbacf3c06c2b3f0e03b812db03968a54d9d5e6c068fba
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.3
|
Release files / pigeon_home-1.2.0-py3-none-any.whl
| Download URL | pigeon_home-1.2.0-py3-none-any.whl |
|---|---|
| Size | 30.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ff9e0dd871d0a237d79e15cce569ff0816f35cd40d1ced9101da41025c2a13c3
|
|
BLAKE2b-256 checksum How to use checksums |
f53365564c0af9245f3f0106ddf60f8b579bc54ebff5f1cd8db122091f2ec3d7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.3
|