Run a team in Slack
Set up a hi team, a virtual team of agents in one Slack channel. A Hermes lead talks with the people in plain language and keeps the project's memory; a coder and a reviewer do the work in boxes, and the lead merges what the reviewer approves.
A hi team is a virtual team of agents that lives in one Slack channel and builds one project for the people in it. The people may not read code: they say what they want in plain language, try what the team makes, and say whether it works.
- The lead is Hermes Agent, from Nous Research. It talks in the channel, agrees with the people what will change and how they’ll know it works, writes the specs, and keeps the project’s memory. It doesn’t write code.
- The members do the work, one task at a time, each in a box of its own on a branch: by default a coder (Claude Code) and a reviewer (Codex), so the code is checked by an agent other than its author.
- The lead merges a task only after the reviewer approves its latest commit.
One team has one channel, one Slack app, and one machine. A new project is a new team.
The lead and the members can reach any website, so the lead can read the
links people give it and the members can install what they need. Their
boxes are the boundary: they hold no keys or sign-ins (hi’s proxy adds the
model keys on the way out), and they see only the team’s folder. To narrow
a member, give it a network in team.json (see below).
Before you start
On the machine that will run the team:
hi install hermes claude codex podman restic
Hermes needs an OpenRouter key (hermes setup), Claude Code and Codex need
to be signed in, and podman (or Docker) runs the boxes. The keys stay on
the machine: the boxes reach the models through hi’s proxy, which puts the
keys in. restic keeps the team’s snapshots.
Set up a team
hi team new payments
The setup asks seven things, one at a time:
- Purpose. One sentence; the lead starts from it.
- Slack app. hi prints a manifest named after the team
(
payments). In Slack, create a new app from it, install it to the workspace, and paste its bot token (xoxb-…, under OAuth & Permissions) and an app-level token withconnections:write(xapp-…, under Basic Information). Neither shows as you paste it. - Channel. Invite the app to the channel (
/invite @payments) and paste the channel’s ID (C…). - People. The owners’ Slack member IDs (
U…), and who else may ask for work; Enter lets everyone in the channel. - Members. Each role’s agent and model; Enter keeps what’s offered.
- Models. An OpenRouter key of the team’s own, so its use is billed and limited apart; Enter uses this machine’s, from Hermes. No box ever holds it (see Keys and settings).
- Code. Enter starts a new web app for the team to build on: a
welcome page on Bun that walks people to the helper’s chat, with a
database and tests (the
apptemplate in Projects). Typeemptyfor a repository with only a README, or give one to clone: a URL, with this machine’s git credentials, or a folder on this machine. A new repository can go on GitHub too: giveowner/name, and hi creates a private repository with this machine’sghsign-in.
hi checks both tokens, that the app is in the channel, and that the bot
token has every scope the lead needs, before it writes anything. An app
made from an older or another manifest can lack one, such as files:read,
and then the lead gets an error on every file people share; hi names the
missing scopes, and adding them under OAuth & Permissions and reinstalling
the app fixes it. hi team up checks again and warns, for teams made
before. Without a terminal, give the answers in a file and the tokens in
HI_TEAM_SLACK_BOT_TOKEN and HI_TEAM_SLACK_APP_TOKEN (and the team’s
OpenRouter key, if it has one, in HI_TEAM_OPENROUTER_API_KEY):
hi team new payments --from payments.json
Start and stop it
hi team up payments # start it now, and at boot
hi team up # pick the team to start or restart from a list
hi team status payments # up or down, its members, its tasks
hi team logs payments # the lead's log
hi team down payments # stop it; the folder stays
hi team rm payments # stop it and remove it, folder and all
hi team up installs a systemd user service, hi-team-payments, which
restarts the team if it stops. For it to run while you’re logged out and
start at boot, run sudo loginctl enable-linger $USER once.
hi team rm asks first, then removes everything hi made for the team: the
service, its name in ~/.local/share/hi/teams, the members’ boxes, and the
folder. With --keep-folder the folder stays, and hi team up <folder>
brings the team back. Deleting just the folder leaves the service and the
name behind. The Slack app and channel, and a GitHub repository, are yours
to remove in Slack and GitHub.
How the team works
Someone in the channel asks for something. The lead agrees with them what
will change and a short list of things they can try, writes it down in
docs/specs/, and briefs the coder. When the coder finishes, the broker has
the reviewer check the work; if it needs changes, the broker gives the coder
the reviewer’s notes on the same branch, up to three reviews, without the
lead in between. The lead hears once: approved, or changes still needed.
Once the reviewer approves, the lead merges and tells the people what
changed and how to try it. A feature is done when they agree it works.
Work that adds packages gets a closer look. The reviewer’s brief lists what
the task adds, removes, and updates, and the reviewer checks each new
package and runs the audit. Bun runs a package’s install script only when
the project trusts it (trustedDependencies) or Bun vets it, so a task
that trusts a new package can’t be merged until an owner says yes: the
lead asks in the channel what the package is and why the app needs it.
A member sees only its brief and the project, so the broker sends what the people gave along with it: the thread the work was agreed in, in their words with their links, and the files shared there (screenshots, mockups, documents), in a read-only folder in the member’s box. The lead attaches anything else, such as a file shared earlier in the channel. A follow-up and its review get the same files.
Work starts only after someone says yes. The lead proposes the plan and the list of things to try, and waits; the broker refuses a new task unless the lead names the message that approved it, a reply from one of the team’s people after the lead asked, in the same thread. Small fixes, such as a typo, may start without asking, and the channel is told when one does.
The lead answers only when someone @-mentions it (@payments), even
in a thread it’s part of, so people can talk among themselves in the
channel. It still follows everything: hi keeps a log of the channel,
threads and shared files included, in the team’s channel/ folder, and
every 15 minutes the lead reads what’s new and keeps what matters in its
memory: a decision made in passing, a link or a video someone shared, who
cares about what. hi team channel payments --since 2h shows you the log.
The lead’s long-term memory is Hindsight:
a memory server that runs beside the lead, keeps the facts, people, and
decisions from every conversation, and recalls the ones that matter before
the lead answers. It runs in a container of its own on the team’s private
network, with its database in the team’s memory/ folder. Its model calls
go through the team’s proxy like the lead’s, so it holds no keys. The
first hi team up downloads its image, about 2.6 GB. To do without it, set
"memory": "off" in team.json; the lead then keeps Hermes’ small
built-in notepad only.
The lead puts members to work with hi team task, review, wait,
merge, release, and discard in its box. These go to a broker that hi team runs on the
machine: it checks each request against team.json, starts the member as
hi agent, runs the review rounds, and refuses a merge that no review approved, and a release that no owner approved.
The lead never holds the agents’ keys or the repository’s credentials, and
nothing it asks for widens a member’s box.
Try what the team built
After a merge, the lead shows the new version on the team’s preview link, so the people can try the work, on their phones too. The link carries its own key, so there is no password to copy:
- hi starts the project in an app box of its own, from the merged code (or
a task’s branch, before it is merged), with
make run, or thestartscript:bun run startwith abun.lock, elsenpm start. The app listens on$PORT; its port reaches this machine only. hi net expose --linkputs it on a temporary HTTPS address through this machine’s NetBird (see hi net), which only the link with its key opens. The machine must be enrolled withhi net, and Peer Expose must be on in NetBird.- The link stays the same from one preview to the next: the new version starts beside the old one, and the address moves to it once it answers, so the people just reload. A link lasts a day; a preview gets a new link only when the old one has less than 12 hours left. If the new version doesn’t start, the link keeps showing the old one.
A new team’s app is on the preview as soon as hi team up starts the
team, so the links people get from /hi work before anything is built.
It is a welcome page that points at the chat button and, once the chat is
open, says what it is for; the first feature replaces it. After the first
merge, the lead makes the previews.
Production has a link of its own, which shows the latest release. hi renews it before it runs out each day, and keeps one message in the channel with production’s current link, edited whenever the link or the version changes. Each release’s announcement links to that message, so nobody has to hunt for the latest link.
The app box holds no credentials: no Slack token and no API keys. The app
finds its own link in the file named by $HI_PUBLIC_URL_FILE, which hi
rewrites whenever the link changes. Anything the people want posted on a
schedule, such as a morning digest, is the lead’s scheduled job: a script
that makes the message from production’s code, and the lead posts what it
prints.
Help, feedback, and the lead in the app
Every page of the preview and of production has a round button in the corner. What it opens depends on who you are.
Anyone with the app’s link gets help and feedback:
- Ask how something works, and get an answer from the app’s guide,
HELP.md, which the lead keeps current with each change. The answer comes from hi, not the lead, through a model with no tools: it only explains, so there is nothing to talk it into. Up to 60 questions a day per address, 400 for the team. - Feedback goes to the team’s channel as a message of its own, “Feedback f-12”, with what you pointed at, your files, and a picture of the page as you see it (untick it to leave it out). Your name is optional, and marked as not checked. Nothing changes until someone in the channel asks the lead to “do f-12”, and that needs a yes as any work does.
People who may ask the team for work type the team’s command in the
channel, /hi-<team>, such as /hi-payments. hi
sends them a private link in a DM, and with it the button opens a chat
with the lead, as them, checked through Slack:
- The link’s key never reaches the app: hi’s relay keeps it in a cookie
only
/__hi/gets, so neither the app’s server nor its scripts see it. It works for 14 days; the command again gives a new link and ends the old one. - When an address changes, hi updates the link in the DM.
- Each team’s command is its own: Slack gives a command that two apps in a workspace share to the one installed last. Type it in the team’s channel or in the bot’s DM; hi answers it there, privately, and the lead still ignores DMs otherwise.
- The Slack app needs the
/hi-<team>command and thecommandsandim:writescopes.hi team manifest <name>prints the manifest that has them;hi team upsays when they’re missing. Until then, ask the lead in the channel to send you your link.
Talk to the lead in a DM. People who may ask for work can also write
to the team’s bot directly, one to one, so the channel stays for the
group. The lead answers there without being @-mentioned, and a yes in the
DM approves its work; hi checks that yes in the DM itself. Work started
from a DM is announced in the channel, with who asked, and each release
says who asked for what it holds. Releases still need an owner’s yes in
the channel. Anyone else’s DM gets no answer. The Slack app needs
im:history, im:read, and the message.im event for this
(hi team manifest <name>).
In the chat with the lead:
- The lead answers as it does in Slack, with the same memory, and knows which page you are on and which version you see.
- Point at what you mean. The target button beside the message box,
or
Alt+Sanywhere on the page, lets you click part of the page: it is outlined as you hover,Alt+↑widens it to the element around it, and a click adds it to your message as a chip,#1 span "Paid". Shift-click adds more. The app ignores those clicks. The lead gets each element’s text, classes, and HTML, and the coder finds it in the code from them. On a phone, tap it, then “Use this”. A picture of each part you point at goes with it, so the lead sees what you saw. - Show it. Paste a screenshot into the message box, drop files on the chat, or use the paperclip: up to 6 files a message. The lead sees the pictures, and the coder gets the files with the work.
- The lead comes back. When work you asked for is ready to try, the lead says so in the same chat. If the chat is closed, the button gets a dot and a preview of the message.
- hi copies each chat into a thread in the channel, files included, so the team sees it.
- The lead comes back in your DM too, when it posts in your chat and you don’t have the app open.
- A “yes” in the chat starts the work the lead proposed there, as one in Slack does. A release, a rollback, or turning production’s chat off still needs an owner’s yes in Slack, where the team sees it.
The app holds none of it. hi adds the button on the way to the browser, in
the relay in front of the team’s links, so it works whatever the app is
written in, and /__hi/ on those links is hi’s. To take it out of
production, an owner asks the lead in Slack, or on the machine:
hi team helper payments # where it is on
hi team helper payments production off # from the next page load; on brings it back
A link made before your hi had the helper gets it when the link is next
renewed. An app whose Content-Security-Policy forbids inline styles
(style-src 'self') still gets the button and the panel, but the pictures
of what you point at, and of the page with feedback, are left out there.
Keys and settings
Nobody pastes a key into Slack: the lead puts what it is given into briefs, and the coder into the code, where every member and every copy of the project can read it. Keys go to the team on the machine instead.
Models. The app calls OpenRouter’s API at $OPENROUTER_BASE_URL with
$OPENROUTER_API_KEY, in previews and production. That key is a
placeholder: the call goes through a socket to hi’s team service on the
machine, which puts the team’s real key on it. The lead’s own model calls
take the same key, through its proxy. Give the team a key of its own, or
change it, at any time; it applies to the next call:
hi team key payments openrouter # asked for; it shows as *
hi team key payments openrouter --unset # this machine's key again
Slack. When Slack gives the app a new token (after a reinstall, or a token you revoked), give it to the team; it asks for the bot token and the app token, Enter keeps one, and a running team restarts with them:
hi team key payments slack
Settings. Other keys and settings the app needs reach its boxes as environment variables, and nothing else: not the lead, not the members, not briefs. The lead sees their names, so it can tell the coder which variable to read.
hi team env payments WEATHER_API_KEY=… # or WEATHER_API_KEY alone, to be asked
hi team env payments # their names
hi team env payments --unset WEATHER_API_KEY
The next preview has a change, and hi starts production again on the same link within a minute. A setting is readable by the app’s code, which the agents write; only the OpenRouter key never is.
The lead itself never joins your NetBird network: the machine runs the app and the address, and the lead only asks for them.
New team repositories tell the coder to give the project a make run that
listens on 0.0.0.0 at $PORT, so every project starts the same way.
Dev and production
The project has two versions, and the lead always says which one people are looking at:
- Dev is where every merge goes. The people try it in previews, and what doesn’t work out can be thrown away.
- Production is the version people rely on, on a
productionbranch of its own, with a link of its own. It changes only when an owner says yes. Each release gets the next number: v1, v2, and so on, taggedrelease-1,release-2.
When the people are happy with dev, the lead asks an owner whether to
release it, and only that owner’s reply lets the broker move production
(hi team release). If a release goes badly, an owner may ask to put
production back on an earlier version.
Every change to production is posted in the channel itself, not in a thread: “Production v4 is out (it was v3), approved by Anna”, with the lead’s notes on what changed, or “Production is back on v3”. When the people want everything in dev since the last release gone, the lead sets dev’s code back to production’s (after someone says yes). That is posted too, with what was set aside, and it stays in the project’s history.
Unmerged ideas are capped at five. Each new task starts one, and its
follow-ups continue it. The broker refuses a sixth until one is merged or
discarded, so ideas can’t pile up beyond what anyone can follow in Slack.
hi team status payments shows production’s version, what dev has on top
of it, and the ideas still open.
The team’s folder
Everything a team knows and has made is in one folder: team-<name> in
the folder where you ran hi team new, or wherever --dir puts it. hi
finds it by name from anywhere, through a link in ~/.local/share/hi/teams.
| Path | What |
|---|---|
team.json |
Purpose, channel, owners, members |
secrets.json |
The Slack tokens, readable only by you |
lead/ |
The lead’s Hermes home: SOUL.md, its memory, its sessions |
repo/ |
The project’s git repository; repo/docs/specs/ holds what the lead and the people agreed, one file per feature |
tasks/ |
Each task’s brief and report |
channel/ |
The channel’s log, a file per day, and the files people shared |
memory/ |
The lead’s long-term memory: Hindsight’s database |
app/ |
Production’s data: what the app keeps in $HI_DATA_DIR |
The containers are rebuilt from the folder on every start, so a copy of the
folder is a copy of the team. To move it, stop the team, move the folder,
and start it by its new folder: hi team up ~/projects/team-payments. The
same works for a folder copied from another machine. secrets.json is
listed in the folder’s .gitignore, in case the folder sits inside a
repository. lead/SOUL.md
is the lead’s identity: edit it to change how the lead works, then
hi team up payments to restart it.
Snapshots and restore
The code is in git, but the lead’s memory, the tasks, the channel’s log, and the app’s data are only in the team’s folder. hi snapshots them:
- every day, around 03:00, while the team is up;
- before every release, and before production goes back to an earlier one, so each version has the data and memory as they were when it went out;
- when you ask:
hi team snapshot payments.
The team keeps running. hi waits until no merge or release is half done, pauses the lead, its memory, and production for about a second while restic copies the folder, and lets them go. A snapshot of an idle team adds a few kilobytes: restic stores only what changed, encrypted.
hi team snapshots payments
ID TAKEN WHY SIZE NEW
cfd8fa7c 2026-10-13 03:12 daily 226.2 MB 208.6 MB
2938aa9e 2026-10-14 03:40 daily 226.2 MB 30.1 KB
5d1e0b77 2026-10-14 11:02 before v4 226.3 MB 41.9 KB
To put the team back as it was, name a snapshot by its ID, latest, the
version it was taken before (v4), or a day (2026-10-13, its last):
hi team restore payments v4
hi says what it puts back and asks first. It stops the team, takes a
snapshot of how things are now, puts back the lead’s memory, the tasks, the
channel’s log, and production’s data, and starts the team again. The
channel hears what happened, with the snapshot that undoes it. The code
stays as it is unless you add --code, and so do the team’s settings
(team.json, secrets.json).
Snapshots live in ~/.local/share/hi/team-snapshots/<name>/, outside the
team’s folder, with the key they are encrypted with. They stay when the
team is removed. hi keeps the last 7 days, 4 weeks, and 6 months, every
snapshot from the last week, and every snapshot taken before a release.
When a snapshot fails, the owners hear about it in the channel, and
hi team status says so until one works.
Rollback. When a release goes wrong, production can go back to an earlier version two ways:
hi team release v3(the lead, once an owner says yes) puts back v3’s code and keeps production’s data as it is. That’s right when the new version is merely wrong.hi team rollback v3puts back v3’s code and production’s data as v3 left it, from the snapshot taken when production moved on. That’s for a release that damaged the data. What was entered since is set aside in a snapshot, and rolling forward the same way puts it back.
The lead proposes a rollback in the channel and says what will be set aside; only an owner’s yes carries it out. On the machine, an owner runs it directly:
hi team rollback payments v3 # asks first; --code-only keeps the data
Neither touches the lead’s memory, the tasks, or the channel’s log; that
is hi team restore.
Offsite. A copy on the same disk doesn’t survive the disk. Give the team an S3-compatible bucket (Hetzner, Cloudflare R2, Backblaze B2, AWS, and others), and every snapshot is copied there too:
hi team backups payments --offsite s3:https://fsn1.your-objectstorage.com/acme-backups/payments
hi asks for the bucket’s access key without echoing it, copies the
snapshots taken so far, and copies each new one in the background. Add
--region for a bucket outside us-east-1, and --keep 14,8,12 to keep
more daily, weekly, and monthly snapshots. The bucket can only be read with
the snapshots’ key, so keep a copy of it somewhere other than this machine:
hi team backups payments --show-key
The app’s data. An app box keeps what it must not lose in
$HI_DATA_DIR (/data). Production’s is the team’s app/ folder, which
outlives every release and is in every snapshot. Each preview has a data
folder of its own, apart from production’s. The app’s own folder is a
fresh copy of the code at each release, so anything written there is
lost. The lead and the coder are told to keep data in $HI_DATA_DIR.
Put the project on GitHub
hi team github payments imanhabib/payments
hi creates a private repository with this machine’s GitHub sign-in (gh),
or connects an existing empty one, and pushes the project. From then on the
team pushes every merge there. No GitHub credential enters a box: the
pushes run on the machine, so nobody can talk the lead into revealing a
token. A team whose project was cloned from a URL pushes there already.
The specs go with the code: the lead writes them in the project’s
docs/specs/, and hi commits its changes whenever a task starts and
before a merge, so each member’s branch starts with the latest specs. They
are the only commits that skip review: documentation, not code.
Change a member
Edit members in team.json and restart the team. A member may have a
bundle (see bundles),
and a network to narrow what it reaches: dev (code hosts and package
registries, plus any allow) or locked:
"coder": { "agent": "claude", "model": "opus", "bundle": "web", "network": "dev", "allow": ["cdn.example.com"] }
The lead is always Hermes, and the reviewer must be a different agent or model from the coder.