A control plane for teams of AI coding agents: chat, work dispatch, consoles and a dashboard for Claude Code, Codex, Hermes, OpenCode and Oh My Pi (deprecated) agents running on Windows, WSL, Linux or remote machines.
You run one service, start aify-env on each machine that should run agents, open the dashboard,
spawn agents into workspaces, and give them work by messaging them.
browser: Dashboard (:8801)
|
v
+-------------------------------------------------+
| aify-comms service (Docker, API :8800) |
| messages, channels, runs, agents, sessions, |
| terminals, files; SQLite; pushes changes live |
+-------------------------------------------------+
^ claims spawns, streams consoles ^ MCP tools: comms_send, comms_inbox, ...
| |
+---------------------+ +---------------------------+
| aify-env (per host) |--runs--> | coding agents |
| owns processes/PTYs | | claude-aify, codex-aify, |
+---------------------+ | hermes-aify (launchers) |
+---------------------------+
- The service stores everything and is the only source of truth. A message is the unit of work: sending one to an agent creates a run that the agent claims, works on and closes by replying.
- aify-env (repo) is the host tier. It claims spawn requests, starts agents in real terminals, streams their consoles to the dashboard and carries your keystrokes back. One per machine.
- Launchers (
claude-aify,codex-aify,hermes-aify, from aify-wrapper) start a runtime with its agent identity and the aify-comms MCP server, so the agent can message, read its inbox and reply. - Managed agents are started and owned by aify-env and driven from the dashboard. Resident
agents are a terminal you opened yourself (
claude-aify --aify-agent <id>) that receives messages live.
| command | what it does |
|---|---|
bash scripts/stamp.sh && docker compose up -d --build |
start or update the service (API :8800, dashboard :8801); the stamp is how /version and the doctor know which build is running |
bash install.sh --client <claude|codex|hermes> http://<host>:8800 |
run the guided client install: launcher, MCP servers and skills; Codex unread notifications need the standalone hook below |
aify-env |
run the host tier in the current directory (the directory is the allowed workspace root) |
aify-env attach <agent> |
take over an agent's terminal; Ctrl+] detaches and leaves it running |
claude-aify --aify-agent <id> |
open a resident agent in this terminal (same for codex-aify, hermes-aify) |
aify-comms doctor |
verify what is actually running matches what you installed (--json, --strict) |
./redeploy.sh |
after git pull: re-run install.sh for every client installed here (it keeps the service URL and aify-env endpoint, not --mcp-transport sse) |
aify-comms itself only verifies (doctor, --check, --version, --help); any other invocation
exits 2 and points at aify-env. Inside an agent, the everyday tools are comms_send, comms_inbox,
comms_read, comms_agents and comms_console_tail; the full list is in
.claude/skills/aify-comms/SKILL.md.
If you have a coding agent, point it at this repo and ask it to install aify-comms. It will find
.claude/skills/aify-comms-install, inspect host roles, clients, endpoint and versions, then show
missing/outdated/unknown items and ask only about the gaps, plus whether you want optional herdr.
Follow agent-led onboarding for the install/update, verify-only and
plan-only workflows. Each repository owns its own installer; nothing silently installs another product.
Prerequisites.
- Service host: git, and Docker with Compose v2 (
docker compose). - Agent host: git, bash (Git Bash on native Windows), Node.js and npm (
install.shstops without them), network access for npm, and the runtime you install for (claude,codexorhermes) on PATH. Hermes needs Node 22 or newer; see install.hermes.md.
By hand:
git clone <this repo> && cd aify-comms
bash scripts/install-state.sh # what this machine already has; run it first
./setup.sh # service host only: generates .env + config
bash scripts/stamp.sh && docker compose up -d --build # service :8800 (API), dashboard :8801
curl http://localhost:8800/health # {"status":"healthy"}
bash install.sh --client claude http://localhost:8800 # once per coding-agent client
.\install.cmd --client claude http://localhost:8800 # the same from PowerShell or cmd
git clone https://github.com/zimdin12/aify-env # agent hosts only; then its own ./install.shLegacy notification hook (optional). The plain client command uses the guided-install chain. An existing readable, recognized notification hook is refreshed automatically. To add a new one, explicitly choose this repository's standalone installer instead of the chain:
bash install.sh --standalone --client claude http://localhost:8800 --with-hookFor a fresh Codex client, use --client codex with this standalone command to include unread-message
discovery: the guided chain does not carry --with-hook yet. From PowerShell or cmd use
.\install.cmd with the same arguments.
This unread-message hook is separate from Claude's channel plugin and the turn-state hooks.
Then, on each agent host, start aify-env from the directory that contains your workspaces, open
http://localhost:8801, go to Environments, fill in Spawn Session (environment, runtime,
agent id, workspace), click Spawn, and message the new agent. To start an agent that already
exists, press Start agent in the top bar or Ctrl+K and search for it: Enter starts a stopped
managed agent, opens a live one's console, or says why not.
Order: service, then install.sh --client on each agent host, then aify-env's install.sh, then
start aify-env. The client install writes this service into ~/.aify/services.json, and aify-env
reads that registry once, when it starts: an aify-env started before the entry existed claims no
spawns until it is restarted, and restarting it stops its managed agents. install.sh asks for the
service URL when run in a terminal and otherwise uses http://127.0.0.1:8800; aify-env's installer
asks for the service key it is missing. Running either again is how you update. Use aify-env's own
install.sh rather than npm install -g, which cannot check the service credential.
Starting or restarting aify-env is a deliberate action. A second instance supersedes the first,
and the one it replaces stops its managed agents. Ask a running host with aify-env doctor instead of
starting one to find out.
Every deploy path here can fail silently: the container keeps serving the previous build, a running
agent keeps the bridge code it loaded at startup, an agent launched without --aify-agent works but
has no status. So a flow is done when aify-comms doctor says ok: true, not when nothing errored.
| flow | do | done when |
|---|---|---|
| install a client | bash install.sh --client <runtime> http://<service>:8800 |
doctor bridge-installed green, then every agent that was running before the install relaunched |
| install / update the service | git pull && bash scripts/stamp.sh && docker compose up -d --build |
doctor service reads build <sha> == repo HEAD (/health alone does not say which build) |
update clients after git pull |
./redeploy.sh (or install.sh per client; a host installed with --mcp-transport sse needs install.sh with that flag, because redeploy.sh re-renders it as stdio) |
bridge-installed green, then every agent that was running before the install relaunched |
bridge-current names any live bridge still running an older build, and reads unknown-all until
bridges started on 0.7.0 or later report one; on Linux bridge-running also names running bridges started
before the install.
Rules that cost real hours:
- Installing does not reload a running agent. Restart agents after a bridge change;
--resume <handle>keeps the conversation. - Launch a registered agent with its id (
--aify-agent <id>), or it has no status. - A dead managed worker's console holds the answer.
comms_console_tail(agentId=...)returns the last recorded output with the fatal line first, markedNOT LIVE. - After updating Hermes itself, re-run
install.sh --client hermes: a hermes update deletes the prebuilt web bundle its console needs. - Upgrading to 0.8 moves agent definitions to aify-env. With aify-env 0.8 running,
aify-env agents importshows what it would define from this service's registry;--writedefines them, andaify-env agents remove <id>drops one you no longer run. Run thisinstall.shbefore aify-wrapper's own installer: its 0.8 launchers need this bridge's definition reader. From 0.9 a managed agent no host defines is not started at all, so run--writewhen you upgrade. A spawn of a new id asks its host to define it first.
Some doctor rows watch the live fleet rather than an install and can turn red on a quiet day:
tier-version, spawn-queue, session-handles, usage-openai,
api-exposure, external-keys, client-api-key. Each one reports and never acts; its detail
says what it found and its fix what to do (aify-comms doctor --json prints both). The host
rows, env-processes, managed-orphans, context-window and claude-login, are in
aify-env doctor since 0.9.7, which also answers env-code-currency as code-current, and
gateway-orphans since 0.9.10.
By default the service has no API key, CORS *, and publishes its port on 0.0.0.0 — a
trusted-LAN setup. Anything that can reach port 8800 can drive every agent, including typing into
consoles.
Turn on a key:
bash install.sh --client claude http://localhost:8800 --with-api-key # generates or reuses API_KEY in .env
docker compose up -d # the service reads it at startupRe-run install.sh for every other client (it reads the key from .env). The dashboard on :8801
then asks for the key once per browser (or open http://<host>:8801/?api_key=<key>), and keeps it as
an HttpOnly cookie holding a hash of the key; changing API_KEY signs every browser out. An existing
key is never rotated, because a new one would lock out every client already installed.
Always on: a request or WebSocket from a page on another site is refused, so a web page you visit
cannot drive the fleet. aify-env binds 127.0.0.1 only and refuses browser requests.
Agents on another machine send here without registering. Give that machine its own key rather
than yours: add EXTERNAL_KEYS=pc2:<key> to .env (openssl rand -hex 32), restart, and have the
other machine send with it:
curl -X POST http://<this-host>:8800/api/v1/messages/send -H "X-API-Key: <pc2 key>" \
-H "Content-Type: application/json" \
-d '{"from_agent":"pc2-manager","to":"<local agent>","subject":"hi","body":"...","origin":"<its address>"}'Its messages show external: pc2, and that name is proven by the key. The origin is what it said
about itself. The key can only send messages, and cannot send as an agent that lives here. It needs
API_KEY set; /health reports externalKeys.enforced.
The operator key is off by default. Anything holding API_KEY (not an external key) may delete
other agents' messages, channels and shared files as the operator, and act under the name dashboard
or operator anywhere: send messages agents read as the operator's own, and start, stop, restart or steer work in the
operator's name. Set OPERATOR_KEY in
.env and no route accepts those names without it; the dashboard then sends it automatically. No agent
can be registered, renamed or spawned under either name, key or not: they belong to the operator. The key
proves the operator's NAME. It does not restrict most actions: an API-key holder can still stop or
restart a worker under its own name, and the audit trail then says so; deleting a defined agent, which
cannot be undone, needs the key once it is set. Once a key is set, these routes are
the operator's alone and refuse a caller without it: PATCH /agents/{id}/effort, /usage-source and
/session-mode, POST /agents/{id}/environment, and POST /spawn-requests with envVars (which can pick the
program a launcher runs). Changing or removing an agent a host defines is the operator's through any route,
key or not. Registration is not one of those routes: POST /agents, which every bridge calls to report its
own state, still rewrites an UNDEFINED agent's mode, effort and usage source without the key (KNOWN_ISSUES). The dashboard sends the key;
the comms_* tools do not, so an agent holding it must call the API directly with the header.
A host proves which machine it speaks for, once it has enrolled. aify-env keeps a secret in
~/.aify/host-secret and sends each service a proof made from it. The proof from a machine's first successful
heartbeat is recorded, and from then on that machine's heartbeats, agent-definition routes, spawn and terminal
claims, spawn updates and terminal-control reports need it. If a host's secret is lost or replaced, its aify-env is refused with the way
back: POST /api/v1/host-proofs/<machineId>/reset as the operator. What this does NOT give you: it is trust on
first use, so a machine that has never presented a proof is open, and whoever presents the first one owns it.
With no OPERATOR_KEY the reset route is open to any API_KEY holder too, so the proof does not protect against
a hostile key holder; it keeps a stale or misconfigured host from speaking for a machine. Like every key on a
host, any process that can read that file can make the proof.
A key does not change the bind address or CORS: bind 127.0.0.1:8800:8800 in
docker-compose.yml if the LAN should not reach it, and set CORS_ORIGINS in .env to the
dashboard origins you use (comma-separated, for example http://localhost:8801), then
docker compose up -d. .env overrides config/service.json, and the default .env sets
CORS_ORIGINS=*. With a key set, /health, /ready, /version, /docs, /redoc and
/openapi.json still answer without one; /ws checks the key itself. Details:
KNOWN_ISSUES.md.
Both are off by default, and only messages addressed to you (to: dashboard) or channels you joined
notify; repeats from one sender on one subject coalesce to one alert per 90 seconds.
- Desktop: enable notifications in the dashboard and grant browser permission.
localhostworks over plain HTTP; a LAN address needs the HTTPS proxy (docker compose --profile https up -d), which serveshttps://<host>:8443(HTTPS_PORTin.env). - Phone: set
AIFY_NTFY_URL=https://ntfy.sh/<private-topic>in.env, rundocker compose up -d, and subscribe to the topic in the ntfy app. The topic URL is a credential; keep it in.env.curl -s localhost:8800/health | jq .ntfyshows whether alerts are going out.
The HTTPS proxy signs with its own local CA, so browsers warn until you trust that CA once per device:
docker cp "$(docker compose ps -q https-proxy)":/data/caddy/pki/authorities/local/root.crt ./aify-root.crt| where | how |
|---|---|
| Windows | certutil -addstore -f ROOT aify-root.crt in an admin shell |
| macOS | sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain aify-root.crt |
| Linux | copy to /usr/local/share/ca-certificates/, then sudo update-ca-certificates |
| Firefox | Settings → Privacy & Security → Certificates → Authorities → Import |
| Android / iOS | install the file as a CA certificate |
A hostname or IP that is not listed in HTTPS_SITES (.env) still warns after the CA is trusted; add
it there and restart the proxy.
- Managed (the normal team): spawned from the dashboard, owned by the aify-env that started them,
restarted, stopped, compacted or reset from the dashboard. An idle managed agent is started
automatically when you message it.
onlinemeans a live worker is ready to claim work. - Resident: a terminal you opened with
claude-aify --aify-agent <id>(orcodex-aify/hermes-aify). Add--sharedso aify-env owns the terminal and it survives closing the window. One instance of an agent runs per host; starting one by hand replaces the running instance. - Switch an agent between the two with Switch to managed / Switch to resident, in its details drawer or on its row in Sessions.
Agent statuses, delivery paths per runtime, compaction, handle repair and the runtime settings are described in docs/OPERATING_MODES.md. Setup for remote hosts and workspace roots is in docs/BRIDGE_SETUP.md; Hermes specifics in install.hermes.md.
VERSION is the single release version. The dashboard header shows the running build and turns into
a warning when that running build is behind origin/main on GitHub (the checkout is not read); GET /version and aify-comms --version report
the same. Updating is always manual: git pull, bash scripts/stamp.sh && docker compose up -d --build,
./redeploy.sh, then relaunch the agents that were running.
0.7.0 removed two old paths, and neither is supported: the v1 JSON-volume to SQLite migration
(scripts/migrate-v1-to-v2.sh, service/export_v1.py, service/import_v2.py), and the sub-service
compose template (scripts/add-service.sh, the SUB_SERVICES setting, services/). A host still on v1
JSON data migrates with v0.6.22 first; extra compose services go in docker-compose.override.yml,
which make up (scripts/compose-up.sh) includes when it exists.
| path | what |
|---|---|
service/ |
FastAPI service, SQLite schema, dashboard (service/new_dashboard/). Rebuild the container after changes. |
mcp/stdio/ |
host-side MCP servers loaded by agents. Re-run install.sh and restart agents after changes. |
install.sh, redeploy.sh |
client installer and its update helper |
.claude/skills/, .agents/skills/ |
agent skills (usage, debug, install), mirrored for Codex |
docs/ |
design and reference; docs/README.md says which documents are current |
Start with docs/ARCHITECTURE.md before changing code, and docs/TARGET_ARCHITECTURE.md for where it is heading. DECISIONS.md records why things are the way they are, KNOWN_ISSUES.md what is still wrong, docs/UNINSTALL.md how to remove it.
MIT — see LICENSE. Issues and questions welcome; for larger PRs please open an issue first to discuss direction.