Release notes¶
What changed in each release, why it matters to a self-hoster, and whether you need to do anything beyond upgrading.
These notes begin at 0.1.70. Earlier releases were not accompanied by customer-facing notes; this page is where they start rather than a reconstructed history, and it will carry each release from here.
Migrations are called out where they happen. A release that adds database migrations says so in its own section, with how many. A section that does not mention migrations adds none, with one exception: 0.1.77 was released without a tag, so for one migration we cannot tell whether it first shipped in 0.1.77 or in 0.1.78, and the 0.1.78 section says so. Otherwise, when you are working out whether a release can be undone by downgrading the image, the absence of a migration note is itself the answer.
To upgrade, see Upgrade. The current release is 0.2.1.
0.2.1 ¶
An agent reports what it used, and a run that reported nothing never reads $0 [runner image] [server image]¶
A one-shot agent run on Claude printed its answer and nothing else, so it reported no usage. Its page then showed $0 and 0 tokens beside real work.
- A one-shot agent now reports its usage (tokens, cost, model) when it finishes. Input tokens include the cached input the model read, not only the new input. This is the runner image: it needs the runner at this release.
- The agent page and the Costs page read not reported for a run that reported no usage, never $0 and 0 tokens. This is the server image, and it applies as soon as you upgrade.
- An agent's answer is still redacted before it reaches the live log, now as one piece before it is split for posting, so a key cannot slip through across a split.
To see agent usage: re-pin the runner to this release's image; an upgrade does not move it (see Upgrade). Until you do, agent runs keep reading not reported, which is accurate.
Saving an agent's transcript no longer follows a link the agent planted [runner image] (security)¶
When an agent finishes, the runner copies its transcript into your repository's
.hivemind/transcripts/ folder. The runner does this outside the agent's sandbox, and it
read the transcript from a folder the agent can write to. An agent could put a link there
in place of its transcript, and the runner would follow it: any file the runner can read,
such as one of its keys, was copied into your repository.
The copy now follows no link, in the transcript's path or in the destination, and copies only an ordinary file. Anything else is refused with a warning in the runner's log, and nothing is copied. The agent's run and its result are unaffected.
Re-pin the runner to this release's image; an upgrade does not move it (see Upgrade). Until you do, the runner copies transcripts as before.
The runner reads only the messages of agents on its own machine [server image] (security)¶
An agent runner's key could read every message on the installation. It now reads the messages it sent or received, and those to or from an agent on a machine it has claimed. The list, a single message and a deliberation thread all apply the same rule. Delivery to your agents is unaffected: an agent is claimed onto the runner's machine before its messages are relayed.
Reading a message that does not exist and reading one you may not see now return the same "not found" answer, so the answer no longer tells you which it was.
Nothing to do.
A question the Commander could not receive says what to do [server image]¶
When a question to the Commander could not be delivered, the chat said why but not what to do. It now adds the step that fixes it, where the cause is known:
- No Commander is set up: set one up, then ask again.
- The Commander is not running: start it, then ask again.
- The Commander did not pick the question up in time: if its window is showing a question or a prompt, answer it there, then ask again.
A question queued for a workstation that is offline, or has never checked in, now also names the workstation step that delivers it. It does not tell you to ask again: the question is already queued, and a second one would be delivered twice.
When the cause is unknown, the message says only what it said before, rather than guess.
The Ask the Commander card reads as words [server image]¶
The card for a question to the Commander showed its raw parameters and a content hash. It is now titled Ask the Commander and shows your question as plain text. The parameters and the full hash are still there, collapsed under details for the record. Other cards are unchanged.
An agent that was never started says why [server image]¶
When the runner refused to start an agent, its Agent result card showed unrelated log text and never the reason. The card now says the agent was never started and quotes the runner's reason word for word, which names the cause and the remedy.
A refused Facet tool names itself [server image]¶
Every tool Facet was not allowed to use was refused as "proposing a dispatch is limited to an administrator", even a read such as a knowledge search, so Facet passed on the wrong reason. A refusal now names the tool and the cause: your role, or that nobody is signed in.
A command for an agent is not taken for a verb [server image]¶
Asking Facet for an agent that runs a command, such as "send an agent to curl this URL", could fail with "curl is not a supported verb", and you had to ask again. Facet now proposes the agent with the command as its task. If it still picks a word that is not a verb, it corrects itself once in the same answer. Who may do what is unchanged.
Settings shows when your Commander workstation last made contact [server image]¶
Settings > Commander link now shows, for administrators, when the linked workstation last made contact and whether it is online or offline. A workstation that has never made contact says so, and a failed read says it could not be read rather than "no contact".
A completed agent run leaves an outcome record [server image]¶
An agent that finished its work left no record in /api/v1/outcomes. Each completed
run now writes exactly one, with its role, project, duration, cost and tokens, in the
same step that marks it completed; a repeated completion writes no second one.
This covers completed runs. A run that fails, is stopped or killed, or is never started still writes no outcome record.
Release notes say which releases add database migrations [docs]¶
Every release that added database migrations now says so in its own section, with how many, and the introduction above explains how to read their absence.
0.2.0 ¶
0.2.0 adds 26 database migrations, so this upgrade is FORWARD-ONLY: once you are on 0.2.0 you cannot go back to 0.1.95 by updating. An older image does not undo them: returning to 0.1.95 needs the database backup taken before you upgraded, restored with that release. Take a backup first; the Upgrade page describes the snapshot the updater keeps for you.
Live updates reach only the people allowed to read them [server image] (security)¶
The live connection that feeds the dashboard used to send most events to every signed-in user, whatever their role: an agent's instructions and first task as it started, task and directive text, approval requests, internal error messages, and the ids of other people's Facet conversations. Each event now goes only to the people who could read its content anyway:
- An agent's instructions and task go to administrators and to the agent runner of the machine the agent runs on, never to the runner of another machine. An agent not yet placed on a machine goes to the runners that could take it (a runner that has claimed at least one machine).
- A container restart goes to administrators and to the runner of that machine.
- Facet conversation updates go to the conversation's owner only.
- A message between agents goes to the agents involved, to the agent runner of the machine that hosts the recipient (it delivers the message), and to administrators.
- Task, directive, approval, error and agent log events go to administrators.
- Agent status changes still reach everyone.
Nothing to do on a default install. If you turned off the live-connection sign-in
check (HIVEMIND_GATE_WS_LIVE=false), connections are anonymous. They still get agent
status changes, but no longer Facet conversation updates or agent messages: turn the
check back on to get them.
Facet works out what you mean [server image]¶
Facet, the dashboard assistant, now decides for each message what you want and does that:
- It answers an ordinary question itself, straight away, with no card.
- It asks one clarifying question when your message could mean two things. The question comes as a card with two or three options and "something else", instead of a guess.
- It proposes an agent when the work needs one, and brings in the Commander when you ask for it, each behind a card you confirm.
- It keeps the conversation's context, so a follow-up such as "and the other one?" is read against what was said before.
Facet's default OpenAI model is now gpt-6-sol. An assistant profile still on the previous default (gpt-4o-mini) moves to it on upgrade. A model you chose yourself is left alone.
Nothing to do. To choose another model, set model on the assistant profile
(PUT /api/v1/agent-chat/profiles/assistant).
Facet files work on your intake board [server image]¶
When you ask for work to be done ("the export button on invoices is broken", "please
update the pricing page"), Facet now files it as a work item on your team's intake
board, instead of asking you where it should go. It shows a card first: "File this on
- The body is written by the server: Facet's short summary, then your own words quoted, then a link back to the conversation.
- Afterwards, the conversation says what was filed, with its reference and a link when the board gives one. If the board setting names a status tool, ask Facet about the item later and it reads its current status from the board.
- Facet still brings in the Commander for live work (something is down now, work wanted tonight, several hosts at once) and for anything on your own machine. Those are never filed. A how-to question is answered directly, as before.
- A reply that read the web or an agent's output cannot file in the same reply.
The intake board is off until an administrator connects one. Until then, Facet says that no intake board is connected and names the setting. It never files anywhere else.
To connect one, go to Settings, Facet: work intake board (administrators only). Choose an MCP server the assistant may use (see "The assistant can use your MCP servers"), its create tool and arguments, where the item's reference and link are in the result, and optionally a status tool. You also set the name shown to people.
Who may file: administrators, by default. The same setting can allow everyone who is signed in. Whoever the setting allows files from the card itself, File it or Not now, in the dashboard chat or in Facet on any other page. The card shows who the item is filed for, and once filed it shows the board's reference instead of a button, so nothing can be filed twice. Filing needs a signed-in browser: an API key cannot file (see "Confirming a Facet card needs a signed-in session").
Facet on every page [server image]¶
Facet is now on every page you sign in to, not only the dashboard. A Facet button opens it in a panel on the page. The dashboard keeps its full chat, and the Commander page has no dock.
- Each browser tab has its own conversation. Moving between pages in a tab keeps
that tab's conversation; another tab can hold a different one. A link with
?c=opens a given conversation. - Facet knows where you are. A chip under the reply box names the page, and the selected item where the page has one (an agent, an MCP server, a bot). Your question is answered about that item, read by the server with your own permissions: an item you may not see reads the same as one that does not exist. Remove the chip and the question is sent with no page at all.
- An agent's own output is never part of what Facet reads from the page, so "stop this agent" still works on an agent's page. Ask what the agent is doing and Facet reads its output then (see "Facet does not act in the same answer that read an agent's output").
- Send becomes Stop while a reply is being written.
If you run your own reverse proxy, serve Hivemind over HTTP/2. A browser opens at most six HTTP/1.1 connections to one site, so several tabs streaming replies at once would wait on each other. The bundled proxy already uses HTTP/2.
Facet can search the web and read a page [server image]¶
Facet can now look things up. For today's facts (news, what is trending, anything you ask for as "latest" or "right now") it searches the web. It can also read a public page you name. It makes at most three searches and reads at most three pages per message. The pages it used are listed under its reply. For general knowledge it answers from what it already knows and does not search.
A reply that read the web cannot start, message or stop anything: web pages can contain text written to look like an instruction. Facet answers from what it read and offers the next step as a question, and your next message can act.
Settings (in .env):
HIVEMIND_FACET_WEB_ACCESS(defaulttrue): setfalseto turn off both search and page reading.HIVEMIND_WEB_FETCH_ALLOWED_CIDRS(default empty, meaning public addresses only): private ranges the page reader may reach, as comma-separated CIDRs. Loopback, link-local and metadata addresses, and CGNAT are always refused.HIVEMIND_OPENAI_API: OpenAI's own endpoint now uses the Responses API, which carries the web search. Setchatto pin chat completions instead: web search is then unavailable, and reading a page you name still works. A custom endpoint always uses chat completions.
A searched reply counts toward the daily spending limit as unpriced usage (see "The assistant has a daily spending limit").
Facet introduces itself [server image]¶
Facet now opens with who it is and how it works, in one consistent voice. An empty conversation greets you and offers only what works on your installation. "What can you do?" is answered as a short tour of the tools it actually has.
If you never edited the assistant's instructions, the upgrade brings them up to the new voice. If you edited them, yours are kept.
Nothing to do.
The Facet chat: conversations, streaming, and "What I did" [server image]¶
- A conversation list in a sidebar, with names, pins, archive, unread marks and whether a reply is running.
- Replies stream as they are written, with a working indicator while Facet thinks. Stop asks the server to end the reply.
- Markdown renders, and each reply has Copy.
- "What I did" under a reply lists the tools Facet used for it.
- Regenerate and Edit are offered on the last exchange.
Nothing to do.
See, message and stop your agents from Facet [server image] [runner image]¶
Ask Facet "what are my agents doing?", about one agent's progress, or what your agents did today. It reads the answer from the agents themselves. You can also ask it to send an agent a message or to stop one. Each message and each stop is a card you confirm in a signed-in browser; nothing is sent or stopped until you do (see "Confirming a Facet card needs a signed-in session").
-
A person sees and steers only the agents they started. An administrator can see and steer any agent on the installation. Facet says what that covers: every agent Hivemind launched on this installation. Sessions a workstation runs outside Hivemind are not included, so it is not everything running on your machines.
-
A running agent shows a live strip in the conversation, and a finished one shows its result as a card.
- A stop now stops the agent. Before this release, stopping an agent that was started from Facet did not stop it: it kept running, and when it finished its status changed from stopped to completed. Now a stop ends the agent's process and every process it started, the agent stays stopped, and its credential is revoked. This part needs the runner at this release; re-pin it (see Upgrade).
A reply that read an agent's output cannot act in the same reply (see "Facet does not act in the same answer that read an agent's output").
Nothing to do.
Facet can watch a URL and tell you when it answers [server image]¶
Ask Facet to "watch https://example.com/health and tell me when it returns 200". After you confirm the card (in a signed-in browser, like every Facet card), the server checks the address on a schedule and posts a message in the same conversation when it returns the status you asked for. A check makes no AI call and costs nothing. Ask Facet to stop watching, and the monitor is removed. While a monitor is watching, its conversation shows monitor active in the Facet sidebar.
Settings (in .env):
HIVEMIND_MONITORS_ENABLED(defaulttrue): setfalseto turn monitors off.HIVEMIND_MONITOR_ALLOWED_CIDRS(default empty, meaning public addresses only): private ranges a monitor may reach, as comma-separated CIDRs. Loopback, link-local and metadata addresses, and CGNAT are always refused.HIVEMIND_MONITORS_MAX_ACTIVE(default200): the most monitors active at once on the installation.
Agents started from Facet or the dashboard can run on OpenAI [server image] [runner image]¶
The runner image now ships the OpenAI Codex agent runtime, next to Claude Code.
- Agents that a person starts from Facet or the dashboard use the installation's default runtime, which is OpenAI. They use it only when a running agent runner has reported that it is ready for OpenAI, meaning it has the Codex runtime and an OpenAI agent key. Otherwise they run on Claude Code as before, and the card says why.
- Agents started by the Commander, Spawn or another agent stay on Claude Code.
- An administrator can choose the runtime for one agent through the spawn API. Nobody else can.
- An OpenAI agent runs one-shot only, contained exactly like a Claude agent. A run whose model has no price in Hivemind's table is shown as unpriced, never as $0.
Settings:
- The runner's OpenAI key is a new Docker secret:
<install-dir>/secrets/openai_agent_key, mode0600, read by the runner asHIVEMIND_AGENT_OPENAI_KEY_FILE. It is a different key from the assistant's (HIVEMIND_OPENAI_API_KEY_FILE). An empty file leaves OpenAI agents off. agents.default_runtimein the settings table:openai(the default when unset) orclaude_code.
To use OpenAI agents: place the key file, then re-pin the runner to this release's
image (an upgrade does not move the runner; see Upgrade). To keep every
agent on Claude Code: leave the file empty, or set agents.default_runtime to
claude_code.
What each Facet reply and each agent costs [server image] [runner image]¶
- Each Facet reply records its real usage: tokens, model and cost.
- A one-shot agent reports what it used and cost, and how it ended (its exit code), when its run finishes. The agent's page shows them. This needs the runner at this release.
- The Costs page shows spend against your caps, and stays accurate over time.
- Usage Hivemind cannot price (a model missing from its price table, or a web search) is marked unpriced, never shown as $0.
- A new Activity page (
/activity) lists in one place what happened: agents starting and ending, cards proposed, confirmed or refused, decided approvals, MCP tool calls per conversation, and update applies.
Spend is visible to administrators only (see "The installation's spending is visible to admins only").
To see agent costs: re-pin the runner to this release's image.
Long-running agents run in the same sandbox as one-shot agents [runner image]¶
Agents that stay running in a terminal session (persistent agents) now run inside the same sandbox as one-shot agents. They can reach their own worktree and nothing else. Each agent's state directory is private: no agent reads another agent's state. When a persistent agent exits, the shell left in its session is sandboxed too.
Re-pin the runner to this release's image; an upgrade does not move it (see Upgrade). Until you do, persistent agents run as before.
A sandboxed agent is not started if it could reach a privileged socket [runner image]¶
The sandbox that contains an agent does not control connections to a Unix socket by
path. So before every sandboxed launch (one-shot and long-running), the runner now tests,
from inside the agent's own sandbox rules, whether it could connect to any privileged
socket on the machine: Docker, containerd, rootful Podman, CRI-O, libvirt, rootless
Docker or Podman for the runner's user, a Unix DOCKER_HOST, and any path you add in
HIVEMIND_AGENT_PRIVILEGED_SOCKETS. If one is reachable, the launch is refused, and
the refusal names the socket. If the test itself cannot run, the launch is refused too.
Where this matters: machines where agents are started directly on the host, next to such a socket. The shipped runner container mounts none of these sockets (it reaches Docker through its socket proxy), so a standard install is unaffected.
If a launch is refused: remove the agent's reach to that socket (for example its
permissions or group membership). If you accept the risk for one socket, list its exact
path in HIVEMIND_AGENT_ALLOW_PRIVILEGED_SOCKETS in the environment of the process that
starts agents; every launch then logs a warning naming it.
For the agent runner container on an existing install: an upgrade never rewrites your
docker-compose.yml, so the runner does not see these two variables until you add them.
Add both lines, by hand, to the environment: block of the hivemind-runner service:
HIVEMIND_AGENT_PRIVILEGED_SOCKETS: ${HIVEMIND_AGENT_PRIVILEGED_SOCKETS:-}
HIVEMIND_AGENT_ALLOW_PRIVILEGED_SOCKETS: ${HIVEMIND_AGENT_ALLOW_PRIVILEGED_SOCKETS:-}
then set the values in .env and recreate the runner. A fresh install's
docker-compose.yml already has both lines. You need them only to add a socket to the
check or to accept one; the check itself runs without them.
An agent's terminal pane is checked before anything is sent to it [server image] [runner image] [agent daemon (host binary)]¶
An agent that runs in a terminal pane used to be found by its session name alone. Anything on the same tmux server could replace that session, or start another process in its pane, and the next message, screen capture or stop would reach that process instead.
Now the runner records, on the agent's row, the pane the agent was launched in: tmux's pane id, the process id, and that process's start time (so a reused process id does not match). Every message, capture and stop is checked against it first, whether it comes from the runner, the terminal UI, or a daemon that reattaches after a restart. If the pane there is not the one that was launched, the operation is refused, and the log says the pane was replaced, respawned by someone else, or its process id was reused.
Agents started before this release have no recorded pane. The first operation on one adopts whatever pane answers to its session name, later operations are checked against that, and the log warns once for each such agent. Restart such an agent to give it the full check.
What to do: re-pin the runner to this release's image, and replace your host binary if you run the agent daemon yourself; an upgrade of the server moves neither (see Upgrade). The server adds three columns to the agents table on upgrade.
Agent credentials no longer appear in process lists [runner image]¶
A persistent agent's credentials now reach its session through a private file that
only the runner's user can read (mode 0600). Before, they were passed as
command-line arguments, which any process on the runner could list.
Re-pin the runner to this release's image.
An agent's message says where it went, and a message to nobody is refused [runner image]¶
The send_message tool an agent uses used to reply "sent to broadcast" for a message
with no recipient, although no agent receives such a message. It accepted a made-up
agent id and "sent" the message to no one, and it refused a word such as "commander"
only as an invalid id. Now:
- No recipient, or
operator: the message is posted for you (the operator) in the dashboard's activity feed, and the reply says exactly that, and that no agent and nobody outside Hivemind receives it. - An agent of the same project: delivered, as before.
- Any other word, an id that is not an agent of the project, or an agent of another project: refused, naming what was asked for; nothing is written.
- If the agent cannot be checked: nothing is sent, and the reply says so.
Re-pin the runner to this release's image; the tool runs beside the agent.
Agents run with an allow-list of commands, and can no longer bypass permission checks [runner image] [server image]¶
Every agent now launches with an explicit permission mode (dontAsk) and its role's
list of allowed tools and commands. Nothing launches with the permission bypass any
more. A tool or command the list does not cover is refused without asking, and the
refusal shows in the agent's transcript. An agent has nobody to ask, so it never waits
on a question it cannot answer.
The shipped lists cover common build and test tools: git; cargo; npm, npx, pnpm, yarn and node; python, pip, uv and pytest; make; go; and the usual text and file tools.
If your agents need a command that is not listed, extend the role's list in the
settings table, under agents.tool_allow.<role> (for example
agents.tool_allow.builder), as a JSON array of entries:
- a built-in tool by name (for example
ReadorWebFetch); - or
Bash(<program> ...)naming a program (for exampleBash(bazel *)).
Bash(*), a bare Bash, and patterns that start with * are refused, because each
would allow every command. MCP tools are not added here: they are granted per server
in Content Studio. The server writes the list into each launch, and the runner checks
it again; an entry that fails either check is dropped with a warning.
Upgrade the server and re-pin the runner together: the new launch is built by the server and carried out by the runner, and this release's runner also ignores any older request for the bypass.
An agent whose launch settings fail a check is held, not retried [runner image] [server image]¶
Before it starts an agent, the runner now checks the agent's role settings and the settings file it wrote. If they do not parse, or the written file does not carry the permission mode, the guard, the role's hooks and its allow and deny lists, the launch is refused. This applies to Claude Code and OpenAI agents alike. Before, a role setting that did not parse fell back to an empty value and the agent started anyway.
A refused launch ends the agent as failed and holds it. The reason is recorded on
the agent (launch_blocked_reason in GET /api/v1/agents/{id}). A held agent is not
picked up again and is not restarted automatically, so a broken setting cannot become
a launch loop. An ordinary failure is retried as before.
You see it in the dashboard. A held agent's row on the Agents page says "Blocked" and gives the reason. The agent's own page shows the reason, and anyone allowed to start agents sees an Unblock button there.
To release a held agent once the setting is fixed, an operator presses Unblock
on the agent's page, which calls POST /api/v1/agents/{id}/unblock from their signed-in
session. Releasing a held agent is a decision, so an API key cannot do it (see
"Confirming a Facet card needs a signed-in session"). The agent stays failed but may be
started again, and its next launch runs every check again.
This release adds a database column for the reason. Re-pin the runner to this release's image to get the checks.
Agents that start with their own identity [server image] [runner image]¶
With HIVEMIND_PER_AGENT_IDENTITY=true (off by default), each agent acts under its own
credential, not the runner's shared key. The server now tells the runner that the
setting is on when the runner claims or creates an agent. The runner then obtains that
agent's own credential, and the server issues it only for an agent on a machine that
runner owns. The credential is revoked when the agent is torn down or stopped.
If you turn it on: set it on the server service and re-pin the runner to this
release's image.
The runner stops retrying when its credential is refused [runner image] [server image]¶
When the server refuses the runner's credential, the runner now treats that as final for that credential and stops retrying, instead of repeating the refused request. The Machines page shows administrators the refusals this server gave to runner credentials, so a revoked or wrong runner key is visible where you manage machines.
Re-pin the runner to this release's image to get the new runner behavior.
Confirming a Facet card needs a signed-in session [server image]¶
Confirming a Facet card, filing a work-item card, and answering a question card are decisions. They now need a signed-in session (your login, or single sign-on) and a request from Hivemind's own page, on the dashboard or any page with Facet open. An API key can no longer confirm, file or answer, whatever role it has, because an API key is the kind of credential a program, including an AI, can hold. A request from another website is refused as well.
What to do: nothing, if you confirm cards in the dashboard. If you wrote a script
that confirms proposals with an API key, it now gets 403 with the code
operator_decision_requires_interactive_session; confirm in the dashboard instead.
The same rule covers releasing a held agent (Unblock). And every request a page
makes to change an agent (stop, kill, message, and the rest of /api/agents and
/api/v1/agents) now carries the page's token, so another website cannot make your
browser do it (403, code browser_submit_csrf). Scripts and tools that use an API
key, such as hm stop and hm kill, the runner, and the terminal UI, work as before.
The rule now covers every change a Hivemind page makes, not only the agent
controls: the Updates page's check and apply, the admin settings, bots, knowledge,
content, memories, tasks and the rest. Each page sends its own token, so nothing
changes when you use the pages. What changes is a script or browser extension that
writes to Hivemind through your signed-in browser (it rides your login cookie):
without the page's token it now gets 403 with the code browser_submit_csrf.
What to do: nothing, if you use the pages or call the API with an API key. If you have a browser-side script that writes to Hivemind, give it an API key instead of your login.
A second install on one host gets its own names, or is refused [install script]¶
install.sh now reads HIVEMIND_CONTAINER_PREFIX, API_PORT, DB_PORT and the image
variables from the environment on a new install, and writes them into .env together
with COMPOSE_PROJECT_NAME. Before, it ignored them, and the compose project was the
directory's name, so a second install on a host could start inside the first one's
project. It also refuses to install over another directory's running install of the
same project or container names (--allow-live-project overrides).
An image that a signed bundle pins is not replaced from the environment; a different
value is refused. On an existing install nothing changes: its values are kept, it gains
a COMPOSE_PROJECT_NAME line naming the project it already runs as, and a different
value in the environment is refused rather than applied.
What to do: nothing for a single install. To run a second one, see "Running a second instance on the same host" in the install guide.
Everyone can confirm a monitor or a message for their own work [server image]¶
Before, only an administrator could confirm a Facet card. Now a signed-in user (not a read-only one) can ask Facet to watch a URL or stop one of their monitors, and send a message to an agent they started, and confirm that card themselves. They can never confirm someone else's card. Starting or stopping agents stays with administrators, and administrators work exactly as before.
Nothing to do.
Every user's Facet and bot conversations were readable by any account [server image]¶
Three reads served everyone's chat history to any caller: the list of conversations,
one conversation's messages, and the security audit log, at /api/agent-chat/... and
/api/v1/agent-chat/.... They checked only that the caller was signed in or had an API
key, whatever its role. So any signed-in user, any API key, and the keys agents run with
could list every conversation on the install and read it: each person's Facet
conversations, every bot conversation, and the audit log's events and visitor ids.
These reads are now for administrators only; anyone else gets 403. Nothing else
changes: an administrator reads them as before (the admin API was already admin-only),
and each person still reads their own Facet conversations. On the AI Agents page, a
non-administrator now sees "Conversations and the security audit log are visible to
administrators." instead of a failed read. One request returns at most 100 rows.
Who was exposed: every release that has these routes, including 0.1.95 and every release these notes cover. On an install where only administrators sign in and no other API keys exist, nobody else could read them. Otherwise, anyone with any account or key, including an agent using its own key, could have read any conversation.
What to do: upgrade. If people other than administrators use your install, or agents run with their own keys, treat what was said in Facet and bot conversations as readable by them until you upgrade: if anyone pasted a password or key into a conversation, change it.
Chatalot direct messages no longer forget the conversation a day after it started [server image]¶
A bot's conversation expires a set time after it STARTED (the profile's conversation time to live, 24 hours by default). A direct message on Chatalot always continues the same conversation, so once it expired, Hivemind started a new one for the message, and then another for the next message, and so on: from a day after the first message, the bot forgot everything in that direct message, on every message, for good.
Now, when a direct message's conversation has expired, the next message starts a new period of the conversation, and the messages after it continue that period. When that period expires in turn, the same happens again.
Direct messages that are already stuck recover on their next message. What was said before a conversation expired is still NOT given to the model: expiry still means the bot forgets the old period. Nothing is deleted; the expired messages stay in the database.
Nothing to do. Public bots, API callers and Facet are unchanged.
The installer runs with sudo, and the first admin can be created as documented [installer] [docs]¶
The install guide said to run the installer as an ordinary account, and the installer
could not finish that way: it has to hand each file in secrets/ to the container account
that reads it (the server runs as uid 1000, the updater as uid 1001), and only root can do
that. It stopped and asked for a re-run as root, and that re-run then refused too, because
the first attempt had already written .env.
Now there is one path: run the installer with sudo. The guide and the installer
agree. Run as an ordinary account, the installer now says so before it writes anything,
and prints the exact sudo command to use. If it does stop partway through a first
install, it removes what it wrote, so the next run starts clean.
After an install, .env and secrets/ belong to root. That is expected; run day-to-day
docker compose commands in the install directory with sudo.
The guide's command for creating the first admin account also did not work. It now sends
username as well as email and password, with your installer's ADMIN_API_KEY in the
X-API-Key header. There is no registration form in the web UI.
What to do: new installs, follow the install guide. If an older installer stopped and
asked you to re-run it as root, and the install never started (sudo docker volume ls
--filter name=_pgdata lists no database volume), remove .env and secrets/ from the
install directory, then run the bootstrap again with sudo. If a database volume exists,
the install has run: keep .env.
The installer also no longer installs as new over an existing database. With .env
missing and the install's database volume still present, it used to generate a new
database password, which cannot open the existing data, and --cleanup-on-fail then
deleted the volume. It now refuses, names the volume, and names the two safe choices:
restore the .env the install had, or remove the volume deliberately. --cleanup-on-fail
now removes only the volumes the failed run itself created.
The agent runner works as the account that owns your repository [installer] [runner image] [docs]¶
Since the installer runs as root, it had stopped choosing which account the agent runner
works as, so the runner fell back to uid 1000. On a host where your repository belongs to
another account, the runner could not write it, and every agent failed to start with a
message telling you to check your Anthropic key. When you placed the key in secrets/
yourself before installing, the installer said nothing about it at all.
Now, whichever way you supplied the key, the installer sets RUNNER_UID and RUNNER_GID
to the owner of your repository, or to the account that ran sudo, and says which. It
never changes values you already set. If the runner's account cannot write the
repository, the installer says so by name and does not start the runner. The runner's own
message for a failed agent now says when the repository is not writable, instead of
pointing at the key. The install guide describes both settings.
What to do: nothing on an install whose agents already start. The installer change reaches new installs and re-runs of the installer; the runner's message reaches every install that runs this runner image. If agents fail to start because the repository is not writable, the install guide says how to set the two values.
A chat caller could join someone else's conversation [server image]¶
Before you upgrade: turns planted in Facet conversations can come back¶
This release changes how much of a Facet conversation Facet reads. Up to 0.1.95 Facet sent the model only the conversation's last 10 messages, so a turn someone planted dropped out of Facet's view after five later exchanges. From this release Facet sends up to about 24,000 tokens of the conversation, drawn from its newest 500 messages, plus a summary of what is older. A planted turn that had dropped out can come back into the model's context the first time the conversation is used after the upgrade, and be written into that summary.
If more than one person uses your installation and you cannot rule out that someone
planted turns, run steps 1 and 2 after the upgrade and before anyone uses Facet, then
step 3. Nothing runs these for you; they are for you to run with psql against the
Hivemind database, and none of them deletes anything.
Step 1. List the planted turns. Facet's own turns never record a client key, so every usage
row on the Facet profile that has one is a turn that came in through the chat route.
Each row is paired with the reply stored just before it and the message before that.
Read message_start before going on: if the owner was using Facet at the very moment
of a planted turn, a pair can be the owner's own.
WITH planted AS (
SELECT u.conversation_id, u.created_at AS turn_at, a.id AS reply_id, q.id AS message_id,
left(q.content, 120) AS message_start
FROM agent_usage u
JOIN agent_profiles p ON p.id = u.profile_id AND p.slug = 'assistant'
JOIN LATERAL (SELECT m.id, m.created_at FROM agent_messages m
WHERE m.conversation_id = u.conversation_id AND m.role = 'assistant'
AND m.created_at <= u.created_at
ORDER BY m.created_at DESC, m.id DESC LIMIT 1) a ON true
JOIN LATERAL (SELECT m.id, m.content FROM agent_messages m
WHERE m.conversation_id = u.conversation_id AND m.role = 'user'
AND m.created_at <= a.created_at
ORDER BY m.created_at DESC, m.id DESC LIMIT 1) q ON true
WHERE u.client_key IS NOT NULL AND u.conversation_id IS NOT NULL
)
SELECT conversation_id, turn_at, message_id, reply_id, message_start
FROM planted ORDER BY turn_at;
Step 2. Hide them. This marks each listed message and reply as superseded for
remediation: Facet's model history and the conversation's transcript leave them out,
and the rows are kept. Running it twice changes nothing.
WITH planted AS (
SELECT u.conversation_id, u.created_at AS turn_at, a.id AS reply_id, q.id AS message_id,
left(q.content, 120) AS message_start
FROM agent_usage u
JOIN agent_profiles p ON p.id = u.profile_id AND p.slug = 'assistant'
JOIN LATERAL (SELECT m.id, m.created_at FROM agent_messages m
WHERE m.conversation_id = u.conversation_id AND m.role = 'assistant'
AND m.created_at <= u.created_at
ORDER BY m.created_at DESC, m.id DESC LIMIT 1) a ON true
JOIN LATERAL (SELECT m.id, m.content FROM agent_messages m
WHERE m.conversation_id = u.conversation_id AND m.role = 'user'
AND m.created_at <= a.created_at
ORDER BY m.created_at DESC, m.id DESC LIMIT 1) q ON true
WHERE u.client_key IS NOT NULL AND u.conversation_id IS NOT NULL
)
INSERT INTO assistant_superseded_messages (message_id, conversation_id, reason)
SELECT id, conversation_id, 'remediation' FROM (
SELECT message_id AS id, conversation_id FROM planted
UNION SELECT reply_id, conversation_id FROM planted) marked
ON CONFLICT (message_id) DO NOTHING;
Step 3. Clear the summary of each affected conversation. The summary is rebuilt from the messages that are kept, on the conversation's next turn. It is rebuilt from the newest 500 messages only, so anything older than that is no longer summarized.
WITH planted AS (
SELECT u.conversation_id, u.created_at AS turn_at, a.id AS reply_id, q.id AS message_id,
left(q.content, 120) AS message_start
FROM agent_usage u
JOIN agent_profiles p ON p.id = u.profile_id AND p.slug = 'assistant'
JOIN LATERAL (SELECT m.id, m.created_at FROM agent_messages m
WHERE m.conversation_id = u.conversation_id AND m.role = 'assistant'
AND m.created_at <= u.created_at
ORDER BY m.created_at DESC, m.id DESC LIMIT 1) a ON true
JOIN LATERAL (SELECT m.id, m.content FROM agent_messages m
WHERE m.conversation_id = u.conversation_id AND m.role = 'user'
AND m.created_at <= a.created_at
ORDER BY m.created_at DESC, m.id DESC LIMIT 1) q ON true
WHERE u.client_key IS NOT NULL AND u.conversation_id IS NOT NULL
)
UPDATE assistant_conversation_memory
SET summary = '', covers_through = NULL, updated_at = NOW()
WHERE conversation_id IN (SELECT conversation_id FROM planted);
Turns planted in bot, direct-message and public conversations¶
From this release a bot also leaves superseded messages out of what it sends its model,
so the same marking hides a planted turn in any bot conversation: direct messages,
public-bot visitors and API callers. It uses the same table as Facet,
assistant_superseded_messages, which now serves bot conversations too. Bots keep no
summary, so there is no third step.
Step 1. List the candidates. Each conversation records a digest of the caller that
started it, and each turn records a client key ending in a digest of the caller that
sent it. A turn whose digest differs from the conversation's was sent by someone else,
or by the owner from a different address. This query only lists; it marks nothing.
Review every row, message_start in particular, and note the message_id and
reply_id of the ones you decide were planted.
SELECT u.conversation_id, p.slug, c.visitor_id, u.created_at AS turn_at, u.client_key,
q.id AS message_id, a.id AS reply_id, left(q.content, 120) AS message_start
FROM agent_usage u
JOIN agent_conversations c ON c.id = u.conversation_id
JOIN agent_profiles p ON p.id = u.profile_id AND p.slug <> 'assistant'
JOIN LATERAL (SELECT m.id, m.created_at FROM agent_messages m
WHERE m.conversation_id = u.conversation_id AND m.role = 'assistant'
AND m.created_at <= u.created_at
ORDER BY m.created_at DESC, m.id DESC LIMIT 1) a ON true
JOIN LATERAL (SELECT m.id, m.content FROM agent_messages m
WHERE m.conversation_id = u.conversation_id AND m.role = 'user'
AND m.created_at <= a.created_at
ORDER BY m.created_at DESC, m.id DESC LIMIT 1) q ON true
WHERE u.client_key IS NOT NULL AND c.visitor_ip_hash IS NOT NULL
AND split_part(u.client_key, ':', 2) <> c.visitor_ip_hash
ORDER BY u.created_at;
Step 2. Hide the ones you reviewed. Put the message_id and reply_id values you
chose in place of the placeholder. Only those rows are marked; they are kept, and the bot
no longer sends them to its model.
INSERT INTO assistant_superseded_messages (message_id, conversation_id, reason)
SELECT id, conversation_id, 'remediation' FROM agent_messages
WHERE id IN ('00000000-0000-0000-0000-000000000000')
ON CONFLICT (message_id) DO NOTHING;
What this cannot find, or gets wrong:
- turns planted before 0.1.78, which recorded no client key, in Facet or a bot. A bot conversation started before 0.1.78 recorded its caller in an older form, so every later turn in it is listed; review those like any other;
- a planted turn whose model call failed: its message was stored, but no usage row was;
- a planted turn sent from the owner's own address. The bot listing compares
addresses, so it cannot see a turn that came from the same one. That is exactly the
case of a public bot behind a reverse proxy with
HIVEMIND_TRUSTED_PROXY_HOPSat0, where every visitor has the proxy's address. The same is true, whatever the setting, behind a proxy that adds neitherX-Forwarded-FornorX-Real-IP: configure the proxy to addX-Forwarded-For, then setHIVEMIND_TRUSTED_PROXY_HOPSandHIVEMIND_TRUSTED_PROXY_HEADER=x-forwarded-for. The listing also shows an owner whose address changed (a visitor on another network, a service with several outgoing addresses), which is why you review it before marking anything.
If you stay on 0.1.95, or on a release without this entry, there is no remedy that keeps the messages. Facet and bots read only the last 10 messages, so a planted turn drops out after five later exchanges; or the person can start a new conversation.
Both chat routes, POST /api/v1/agent-chat/chat/{slug} (any API key or signed-in
session) and the public POST /api/v1/public/agents/chat/{slug}, let the caller say
whose conversation a message belonged to, in the request's visitor_id. A caller that
knew another person's conversation id and visitor id could send a message into their
conversation: the conversation's recent messages went to the model with it, so they
could come back in the reply, and the message and reply were stored in that person's
conversation, where they became part of what the bot remembered. This reached each
person's Facet conversations (through the Facet profile), bot direct messages, and
public-bot conversations. Conversation ids are not secret (the live updates stream
carries Facet's, and a bot's direct-message ids can be computed).
Now a conversation belongs to whoever the server authenticated: the API key or the
signed-in user on the first route, and, on the public route, the visitor's
visitor_token (see the next entry). A visitor_id you send on the first route only
separates conversations within your own key, so a service can keep its end users apart,
but it can never reach another key's or user's conversations. The public route ignores it.
Facet conversations can no longer be opened through the
first route at all (403); Facet is used through its own chat. A new conversation
always gets an id from the server.
Who was exposed: every release that has these routes, including 0.1.95. Anyone with an API key or an account could add to, and read through the model's replies, another person's Facet and bot conversations if they had the ids. Anyone on the internet could do the same to another visitor's public-bot conversation.
What to do: upgrade. If you run a service that calls
/api/v1/agent-chat/chat/{slug} with a visitor_id, its existing conversations start
fresh once after the upgrade; later turns continue normally. If you embed a public bot,
see the next entry.
Public bot conversations now belong to a visitor token, not an address [server image]¶
If you embed a public bot, you must now thread
visitor_token. Every response that starts a conversation carries avisitor_token. Send it back, withconversation_id, on every later turn. A turn without it always starts a new conversation, so a widget that does not thread it gets a bot that forgets the visitor between messages. After the upgrade, every existing public-bot conversation starts fresh once.
On the public route, POST /api/v1/public/agents/chat/{slug}, a conversation belonged to
the address the visitor connected from. An address is not a person. Behind a reverse proxy
with HIVEMIND_TRUSTED_PROXY_HOPS at its default 0, every visitor appears to come from
the proxy's address. Visitors behind one home or office router, or one mobile carrier's
shared address, have one address too, and so do the visitors of a proxy that does not pass
their address on. In each of those cases anyone who had another visitor's conversation id
could read that conversation through the bot's replies and add to it.
Now a public conversation belongs to a visitor_token: a random secret the server
creates for each new conversation and returns once, in the response that starts it. The
server stores only a hash of it, never the token itself, and never writes it to a log or
the audit log. A turn that sends the token back continues its conversation. A turn without
it, or with an empty one, starts a new conversation with a new token, whatever
conversation id it names. The visitor's address is used only to count the rate limit.
Who was exposed: every release that has the public route, including 0.1.95.
What to do:
- Thread
visitor_token(above, and the public chat section of the API reference). - Tell the rate limit which address is the visitor's. Set
HIVEMIND_TRUSTED_PROXY_HOPSto the number of proxies in front of Hivemind, andHIVEMIND_TRUSTED_PROXY_HEADERto the header they write (next entry: an existing install must add that line to itsdocker-compose.yml).
An IPv6 visitor could use a new address for every message and never be rate limited [server image]¶
The public route's per-visitor rate limit counted each full IPv6 address as its own
visitor. A visitor on IPv6 usually has a whole block of addresses (a /64) and can
send each message from a different one, so the per-minute limit never applied to them,
and the messages they sent used up the bot's daily cost cap for every other visitor. An
IPv4 address written in its IPv6 form (::ffff:198.51.100.9) also counted separately
from the same IPv4 address.
Now an IPv6 visitor is counted by their /64, and an IPv4 address written in IPv6 form
counts as that IPv4 address.
Nothing to do.
The rate limit could be keyed on an address the visitor wrote [server image]¶
The public route counts its rate limit per visitor address. With
HIVEMIND_TRUSTED_PROXY_HOPS set, that address came from the forwarding headers, and in
four cases a visitor could choose it, and so get a fresh rate-limit allowance on every
request:
- a proxy that sets
X-Real-IP: Hivemind readX-Forwarded-Forfirst, and a visitor can send their own; - a visitor who sent a long
X-Forwarded-For: only its first 32 entries were read, so the entries the proxy appended at the end could be pushed out; - a proxy that adds its own
X-Forwarded-Forline instead of appending to the visitor's: only the first line was read; - a proxy that wrote something other than an address into its entry: the entry was skipped, and the one before it, which the visitor wrote, was used.
Now Hivemind reads only the header you name in the new HIVEMIND_TRUSTED_PROXY_HEADER
(x-forwarded-for or x-real-ip), never the other. When it is not set, Hivemind trusts no
forwarding header and counts every visitor at the address of the connection. From
X-Forwarded-For it reads exactly the last HIVEMIND_TRUSTED_PROXY_HOPS entries, across
every line, from the right; if one of them is not an address, it uses the address of the
connection instead. At startup the server logs which proxy setting it is using, and warns
when a proxy is declared but no header is named.
An upgrade does not add the setting to an existing install. docker-compose.yml is
written once, at install, and upgrades do not change it. A new install's file sets
x-forwarded-for; an existing one has no line for it.
What to do:
- If you set
HIVEMIND_TRUSTED_PROXY_HOPS, this release CHANGES your rate limiting until you add one line. Without it, Hivemind trusts no forwarding header, so every visitor shares one rate-limit bucket at your proxy's address, as ifHIVEMIND_TRUSTED_PROXY_HOPSwere0. Add this line to theenvironment:list of theserverservice in yourdocker-compose.yml, next toHIVEMIND_TRUSTED_PROXY_HOPS, and restart the server:
If your proxy sets X-Real-IP (a common nginx setup) rather than appending to
X-Forwarded-For, use x-real-ip in place of x-forwarded-for.
- If you never set HIVEMIND_TRUSTED_PROXY_HOPS, nothing changes.
Every bot's configuration, its instructions included, was readable by any account [server image]¶
The bot profile reads, GET /api/agent-chat/profiles, GET /api/v1/agent-chat/profiles
and GET /api/v1/agent-chat/profiles/{slug}, returned every bot's full configuration,
including its system prompt (the instructions it follows), to any signed-in user and any
API key, including the keys agents run with.
They are now for administrators only; anyone else gets 403. The administrator API
is unchanged. The Bots and AI Agents pages are now in the navigation only for
administrators, and anyone else who opens them sees "Bots are managed by administrators."
Who was exposed: every release that has these routes, including 0.1.95. Anyone with an account or an API key, an agent's own key included, could read every bot's instructions and settings.
What to do: upgrade. If a bot's instructions contain anything you would not show every user of your install, such as a password, a key or an internal address, treat it as having been readable and change it.
A setting's name could run script on the Settings page [server image]¶
The Settings page put each stored setting's name inside the page's interactive controls, where the browser runs it as script. Anyone who could store a setting (an administrator, or a tool using an administrator's API key) could store one whose name ran script in the browser of whoever next opened Settings, with that person's session. A stored value containing a backslash or a line break also stopped the Settings page's script from working. Setting names and values now reach the page only as data; neither is ever run.
Who was exposed: any signed-in user who opened Settings after such a setting was stored. Every signed-in user can open the Settings page, not only administrators. Storing the setting needed administrator rights, so an install with a single administrator who is its only user was not exposed to anyone else.
What to do: nothing beyond upgrading. If you want to check, list your settings as an
administrator (GET /api/v1/settings) and look at the names: Hivemind's own settings use
only lowercase letters, digits, dots and underscores. A name with quotes, brackets or
parentheses in it was not created by Hivemind. After the upgrade it can no longer run on
the page, but whoever stored it had administrator access.
Any account could create a project in someone else's name, or rename anyone's [server image]¶
Creating a project took its owner from the request, so a signed-in account, a read-only one included, could create a project owned by another user, an administrator for example. Changing a project's name, description or repository URL needed no permission at all, on any project. A project is now owned by the account that created it, and whatever owner the request names is ignored. Creating one needs the user role or higher, so a read-only account is refused. Changing an existing project needs its owner or an administrator. On the Settings page a read-only account now sees why it cannot create a project instead of a form that fails.
Projects stay visible to every account, because tasks, memories and agents are
filed under them. A project's repository path on the server is now shown to
administrators only, in the API and in the pages; anyone else sees it as null.
Every page that listed projects used to include that path in its source.
Who was exposed: any install with more than one account. The Settings page's Projects form, new in this release, put the create one click away for every account.
What to do: nothing beyond upgrading. If you want to check, list your projects
as an administrator (GET /api/v1/projects) and compare each owner_id with who you
expect created it. A tool that sent owner_id keeps working; the field is ignored.
Any account could change or delete anyone's task, or act as another agent [server image]¶
Several writes took WHO was acting from the request instead of from the account that signed in:
- Tasks. Any account could edit, reassign or delete any task, and could name any agent as a task's creator. Changing a task now needs the account that created it, the owner of its project, an agent working in that project, or an administrator. The same rule applies to a status change sent over the live connection. Every new task records the account that created it. The agent recorded as its creator is the agent the key belongs to; only an administrator can name another. A read-only account can no longer create tasks.
- Deliberation replies. A reply could be posted as any agent, into any deliberation. It is now posted as the replying agent, only into its own project's deliberations.
- Agent outcomes. Any account could record results in any agent's name, and those results feed the performance summary and learning. An agent now records only its own, and an administrator can record on an agent's behalf.
Who was exposed: any install with more than one account or more than one agent.
What to do: nothing beyond upgrading. Tasks created before the upgrade have no recorded creator account, so they can be changed by their project's owner, by an agent in their project, or by an administrator.
You can read the log of an agent you started [server image]¶
An agent's log (its transcript) was readable by administrators only, through the API
(GET /api/v1/agents/{id}/logs) and on the agent's page. Facet, meanwhile, already
showed you the output of your own agents when you asked about them. The two now agree:
- You can read the log of any agent you started, through the API and on its page, whatever your role, a read-only account included.
- Administrators can read every agent's log, as before.
- Everyone else is refused, including for an agent whose starter was not recorded (one started by a schedule, or before this was recorded).
This deliberately widens the earlier administrator-only rule to the agent's own starter.
What to do: nothing beyond upgrading.
Messages are readable only by the people they concern [server image] (security)¶
Listing or opening messages (GET /api/v1/messages, GET /api/v1/messages/{id}, and the
same routes the web interface uses under /api/messages) returned every message on the
installation to any signed-in account, whatever its role and whoever the message was
between. A message carries the same content as an agent's log, its instructions and the
results it reported, so this also bypassed the rule above for agent logs.
The same rule now applies to both:
- You can read a message you sent or received, as yourself or as your agent, and the messages of any agent you started.
- Administrators can read every message, as before.
- Any other message is hidden. Searching by sender, recipient or project cannot show it, and opening it by id answers "not found".
- A deliberation's thread follows the same rule: it shows only the messages in it that you may read.
- The agent runner still reads the messages it delivers, so agents keep receiving theirs.
Who was exposed: on an installation with more than one account, or with agents running under their own credentials, any account or agent could read every other agent's instructions and results. An installation whose only account is its administrator, and whose agents do not run under their own credentials, was not exposed to anyone else.
What to do: nothing beyond upgrading.
Only known settings can be saved, and every tool allow-list change is audited [server image] (security)¶
Saving settings (PUT /api/v1/settings, and the Settings and Learning pages that use it)
already needed an administrator. What an administrator could save was not bounded: any
setting name was stored, including names Hivemind does not use, and a change to an
agent's tool allow-list left no record of who made it.
- Only settings Hivemind uses can be saved. A save that names any other setting is refused as a whole, and nothing from it is stored. The Settings page shows only these settings.
- Every change to an agent's tool allow-list is audited. A change to
agents.tool_allow.<role>widens what that role's agents may run, so each change is recorded in the audit log with the administrator who made it and the value before and after. A change that cannot be recorded is refused. - The Learning page's model choices now take effect. The page saved each role's model and thinking level under names the server never read, so a choice made there changed nothing. It now saves them where the server reads them.
What to do: review Settings for values you did not set. If more than one person or tool holds an administrator key, check the audit log for tool allow-list changes you do not recognize. If you chose a role's model or thinking level on the Learning page before this release, choose it again: the earlier choice was never applied.
Saving settings is all or nothing, and provider keys always read masked [server image]¶
Three fixes to how settings are saved (PUT /api/v1/settings, and the Settings and
Learning pages that use it):
- Saving several settings at once saves all of them or none. Before, when one setting in a save failed (for example an LLM provider key saved when no encryption key is configured), the settings before it in the same save had already been stored. Now the save fails as a whole and nothing from it is stored. The Learning page also saves a role's settings in one request, and tells you when a save was refused instead of showing it as saved.
- An encrypted provider key always reads masked. LLM provider keys are stored
encrypted, and are now also marked sensitive, so the Settings page and the settings list
show
********rather than the stored ciphertext. A value that is already encrypted, such as a row a page sends back unchanged, is stored as it is and never encrypted a second time. A key encrypted twice cannot be used. - A change to an agent's tool allow-list needs a signed-in administrator. Each change
to
agents.tool_allow.<role>is recorded in the audit log with the administrator who made it. A change that cannot be attributed to a signed-in user is refused, rather than recorded without a name.
What to do: nothing beyond upgrading. If an LLM provider key stopped working after the Settings page was saved, enter the key again.
News feed text is plain text, and CDATA feeds are read [server image]¶
Content Studio reads news from RSS and Atom feeds. Two things were wrong with the text it took from them:
- Feeds that wrap their text in CDATA lost it. An item whose link was in a CDATA section was skipped without a word, and a CDATA description was dropped. Many feeds write their items this way. Both are now read like any other text.
- A feed's text could carry a link that is not a web link into a draft. "Create draft"
copies a news item's text into the new draft. Markup encoded twice, and markdown links
such as
[click](javascript:...), survived into that text. No page showed them as links, but a draft that is later shown as markdown or HTML would have. Feed titles and descriptions are now stored as plain text: every layer of markup is removed, and a markdown link whose target is not anhttporhttpsaddress is turned into plain text.
A draft created from a news item gets the same treatment when it is created, which also
covers news stored before this release. One side effect: in a draft created from a news
item, a markdown link to anything other than an http or https address, such as a
mailto: link, becomes plain text. You can add it back while editing; drafts you write
yourself are not changed.
What to do: nothing beyond upgrading.
Smaller security fixes [server image]¶
- A new directive records who created it from the credential that made the request, never from a value in the request body.
- Changing an assistant profile (
/api/v1/agent-chat/profiles) now needs an administrator. - An agent's MCP configuration file (
.mcp.json) is written in a way that refuses to follow a symbolic link planted in its place. - An agent's cost can be reported only by that agent, the runner it runs on, or the Commander.
- A refused downgrade no longer tells you to use a rollback option that does not exist.
Nothing to do.
The Anthropic key can be read from a file [server image]¶
HIVEMIND_LLM_API_KEY_FILE names a file holding the server's Anthropic API key, for
example a Docker secret. It is the same _FILE form that HIVEMIND_OPENAI_API_KEY_FILE
already has. It is checked first: when it is set and the file is unreadable or empty,
the server does not fall back to HIVEMIND_LLM_API_KEY.
Nothing to do unless you want the key out of .env.
A broken OpenAI key file no longer falls back to another key [server image]¶
When HIVEMIND_OPENAI_API_KEY_FILE is set, the assistant's OpenAI key now comes only
from that file. If the file cannot be read or is empty, the server loads no OpenAI
key and says so in its log. Before, it quietly fell back to OPENAI_API_KEY from the
environment, so a broken mount kept running on a key you believed you had taken out of
your configuration. OPENAI_API_KEY is still read when no key file is configured.
What to do before you upgrade: if you set HIVEMIND_OPENAI_API_KEY_FILE, check that
the file is mounted and holds the key. If it is broken and you also have
OPENAI_API_KEY set, the assistant stops answering after the upgrade until the file is
fixed (or HIVEMIND_OPENAI_API_KEY_FILE is removed).
Choose which MCP servers a Facet conversation uses [server image]¶
Every Facet chat box (the dashboard, the Facet panel on other pages, and a reply) now has an MCP button showing "MCP: on" or "MCP: off" for the conversation. It opens a panel with:
- the conversation's switch for using MCP tools at all (the same setting as before);
- one row for each MCP server you may use, with its health, the number of its tools you are offered, and the time that health was observed, and a switch to turn that server off for this conversation only. Turning a server off only removes it from what Facet is offered; it can never offer a server you were not already granted;
- for administrators, a Manage servers link; everyone else sees "Your administrator manages these."
Who sees what. The list shows only servers that are enabled, allowed for Facet and granted to you. A person who is not an administrator is counted, and offered, only the tools that run directly (not the ones behind a confirm card), as in a Facet turn. With no servers granted, the panel says "No MCP servers are connected for you."
Health is what was last observed, not a live check. Opening the panel contacts no server. A server shows "ready" when its tools were discovered and you are offered at least one, "no tools offered" when it was discovered but offers you none, and "no discovery recorded" when no discovery has been recorded for it.
Servers you connected before this release show "no discovery recorded" until their
tools are discovered again: the time of a discovery was not stored before. An
administrator runs Discover tools for the server on its bot page (for Facet's own
servers, /bots/assistant); the row then shows "ready" or "no tools offered" with that
time.
If the stored off list cannot be read (for example a database error), the turn is offered no MCP tools at all, never every server, and the server logs it. The panel says "The connected servers could not be read." and keeps its per-server switches disabled until it has read the list.
Nothing to do beyond the upgrade. The server adds one column to the conversations
table (mcp_servers_off, empty by default), so no server is off in any existing
conversation.
The assistant can use your MCP servers [server image]¶
The dashboard assistant can now use MCP servers you connect, when you allow it:
- Off until you turn it on. In Content Studio, MCP servers, mark a server "the assistant may use this server" and list who may use it (users or roles). In a conversation, switch MCP tools on. A new conversation starts with it off.
- Reads run; changes wait for an administrator. A read-only tool runs directly. A tool that can change data becomes a card an administrator confirms or declines, and nothing runs until then. You can set any tool to always need a card, or never be offered.
- External output is treated as data. After a tool's result arrives, the assistant makes no further tool call in that message, so text inside a result cannot make it act.
- At most five tool calls per message.
- Tool results are sent to your configured model provider.
MCP servers are configured in Content Studio, and credentials are sealed [server image]¶
Content Studio has a new MCP servers tab. You add, edit and remove the MCP servers your agents are given, choose which of each server's tools agents may call, and enable a server with a visible switch. Until now this was an API call or a database row.
Credentials now live in a sealed secret store. An environment value can reference a stored secret instead of carrying the value, and the secret is never shown again, not even to an admin. A credential written in plaintext into a server's environment or arguments is refused, with a message pointing at the store.
What happens on upgrade, automatically. The server looks for credentials already stored in plaintext in your MCP server configuration:
- With an integration encryption key configured, each one is moved into the secret store and referenced from where it was. Your agents keep working.
- Without one, nothing is deleted. The server is disabled, and Content Studio says "set the integration key to seal N credentials". Once the key is configured, the next start seals them automatically; then enable the server.
- A credential passed as a command-line argument is kept and its server disabled in either case, because an argument cannot hold a reference. Move it to an environment secret reference, then enable the server.
Do you need to do anything? Only if a server was disabled; Content Studio says which, and what to do. An upgrade never deletes your configuration.
Also changed:
- A new server is created disabled with no tools granted. An existing
server keeps its state when the set is saved without
enabled. - Every MCP configuration change is recorded: who, when, and which servers and secrets by name, never a value.
- Two admins editing at once can no longer silently overwrite each other: a stale
save is refused. Scripted API callers can opt in with
If-Match. - An admin API key no longer reads back a secret; only the agent runner receives secret values.
- The audit page lists agent-side MCP servers separately. Agents call those directly, so their calls are not in the audit chain.
MCP secrets no longer reach the agent's files [runner image]¶
Until now the agent runner wrote an MCP server's environment, secret values included, into the MCP configuration file it gives the agent.
- A secret now travels only in the agent's process environment. The configuration file names a variable, never the value.
hivemind-mcp-launch, a small launcher in the runner image, starts each MCP server. It gives the server its own secrets under the names it expects, and removes every other secret variable, so no server sees another server's secret. It also removes the runner's own credentials (its API key, its repository write token, the agent's model key) from the server's environment.- An MCP server now inherits only a short list of variables from the agent:
HOME,LOGNAME,PATH,SHELL,TERM,USER, the locale (LANG,LC_*),TZ,TMPDIR, the proxy variables and the CA bundle variables. It also gets its own configuredenv. A server that needs another variable must have it added to its configuredenv. - The agent runtime's shell snapshot is turned off, because it copied the agent's environment, secrets included, into a file.
- A literal setting that mentions
HM_MCP_, the prefix reserved for these variables, is refused. - A runner that cannot find the launcher refuses to start an agent that needs a secret, and says why.
Reaches recreated (re-pinned) runners only. An older runner keeps writing the values into the file as before. Independently, an agent can always read the credentials of the servers it runs.
A question to the Commander is not delivered twice [server image]¶
A question you confirm for the Commander is picked up and delivered by your workstation. Until now the instance kept offering an ask that had already been delivered, and a workstation delivers anything it is offered that it holds no record of. So a workstation that was replaced, or that lost its local state, could deliver the same question to the Commander a second time — and the Commander is a person, being asked the same thing twice with nothing marking it a repeat.
A delivered ask is no longer offered. It is still answerable: the workstation that delivered it can post the answer as before, because delivering a question is not answering it.
You need Spawn 0.1.7 or later on the workstation for this to change anything. Before that version no delivery was ever recorded, so from this instance's point of view nothing had been delivered and there was nothing to stop offering. On an older Spawn this release is harmless and inert.
If the answer never comes back — for example the workstation that delivered your question is replaced before the Commander replies, so the one that could answer no longer has the instance's trust — the question now ends on its own, and the conversation says what happened rather than waiting silently: that it was delivered, when, and that it expired without an answer, so you can ask again.
Nothing to do beyond upgrading.
Replies are labelled Commander only when your Commander sent them [server image]¶
A retired chat path could label a reply "Commander" based on a claim carried in the message's own metadata, with nothing checking that claim against who actually sent it. Nothing in this product writes that claim any more, and the one thing that used to — a chat feature from a component this product no longer runs — has been removed rather than trusted more carefully. The Commander's label now only ever comes from a reply your own Commander actually sent.
Nothing to do beyond upgrading.
Moving a machine's agent runner, from the machine's own row [server image] [updater image]¶
0.1.95 made a runner left behind visible: the Machines section on the Infrastructure page says which machines are running an older runner, or have never reported a version at all. It offered nothing to do about it beyond a link to the procedure.
Those rows now carry a Re-pin runner action. It asks the instance what a re-pin would do and shows you the answer before anything happens: which machine, the version its runner reports now, the image reference it would move to, and whether agents running on that machine will be stopped. It also says when it will not offer to move one — a runner that has never reported a version, a machine that is not reporting in, agents still running — each with its own reason, because each needs something different from you.
Confirming records the decision — including the version to roll back to — and then carries it out. Rolling back is the same action run the other way: the version the runner was on before its last re-pin is kept, and shown to you when you start a re-pin on that machine.
The action is performed by your updater service, so it starts working once you
have recreated it. An update pins the new updater image but never replaces the
running updater container — that is deliberate, and unchanged — so shortly after
upgrading, your updater is still the previous one and has no way to do this yet.
Recreate it once (docker compose up -d hivemind-updater) and the action works
from then on. Until you do, the row says so and shows you the manual steps
instead; it never reports a move it did not make.
It can only act on the machine your instance runs on, so a runner on another machine — a developer workstation, for example — is still re-pinned there. You do not have to keep track of which is which: your runner records its own identity, and the updater checks that identity before doing anything, so picking another machine's row is refused rather than acted on. A runner old enough not to record one is the single case this cannot confirm; on a one-machine install it goes ahead, and otherwise you are shown the steps. Recreating that runner once gives it an identity.
One clarification while we are here, because the previous wording invited the wrong conclusion: an update does keep your runner's image reference current. What it has never done, and still does not do, is replace the running runner container — so the version your runner reports is unchanged until something recreates it, which is what this action now does for you.
Only an admin can confirm a re-pin, and the instance checks that itself, whether or not the action is shown.
See Moving your agent runner to a new version.
Health check targets are configured on the instance [server image] [runner image]¶
An agent's health check can test your endpoints and your TLS certificate
expiry — once you tell it what to check. Until now you told it by setting
HIVEMIND_HEALTH_URLS and HIVEMIND_TLS_DOMAINS in your environment, and
that only worked if you set them on the agent runner, because that is where
the tool runs. Set on the instance, which is the natural place and where our own
compose put them, nothing read them: the check reported "no targets configured"
however carefully you had configured it.
They are now configured on the instance, through the API, and handed to each agent as it starts. Admin only to write, and the whole list is replaced on each call. Internal and private addresses are fine — these are your own services.
Your existing environment still works. While the list is empty the runner's own variables are used unchanged, so upgrading changes nothing until you choose to move. Clearing the list returns you to them.
Agents pick this up when they start, so a runner from before this release ignores it. Upgrading the instance does not replace your runner; see Upgrade, "The agent runner is NOT upgraded by an apply".
See "Health check targets" in the API reference.
Settings written True instead of true now work, and say so when they do not [server image]¶
Two settings — HIVEMIND_PER_AGENT_IDENTITY and
HIVEMIND_REQUIRE_PER_AGENT_KEY — only accepted their on/off words in lower
case. HIVEMIND_PER_AGENT_IDENTITY=True left the setting off, and nothing
said so: the behaviour it falls back to is the behaviour you had before the
setting existed, so there was no message, no counter, and nothing in the
interface to notice. Both now read 1/true/yes/on and 0/false/no/off in any
case.
If either is set to something that is neither — enabled, say — your instance
now says so once at startup, naming the setting, the value, and which default is
therefore in force. The default itself is unchanged; it simply is not silent any
more.
Nothing to do unless you set one of these. If you did, check the case — you may find a setting you believed was on has not been.
The assistant has a daily spending limit [server image]¶
The assistant's profile has always carried a daily cost cap, but nothing enforced it for the assistant. It is now enforced. Every assistant turn is counted, and once today's spend reaches the cap the assistant replies that it has reached its limit and does not call the model. The limit resets at midnight, server time.
Every turn means every turn: replies on the default model (gpt-6-sol), streamed replies, and the summaries Facet writes to keep a long conversation's context all count. Usage that Hivemind cannot price (for example a reply that searched the web) counts at the highest rate Hivemind knows, so the limit errs toward stopping early rather than late.
There are two limits:
- For the whole installation:
cost_cap_daily_centson the assistant profile. The default is now 2000 cents ($20 a day), up from 500. If your assistant profile still had the old default of 500, the upgrade raises it to - If you had set your own value, it is left alone.
- For each user:
cost_cap_daily_per_user_cents, new, and empty by default, which means no per-user limit below the installation's.
Change either one on the assistant profile:
PUT /api/v1/agent-chat/profiles/assistant with, for example,
{"cost_cap_daily_cents": 5000} or {"cost_cap_daily_per_user_cents": 500}
(null clears the per-user limit). The assistant's refusal names which limit was
reached and which setting raises it.
Nothing to do unless your assistant is busy enough to spend more than $20 a day, in which case raise the cap before you upgrade.
Schedules act as the person who created them [server image]¶
A schedule's creator is now the signed-in user who created it. A created_by
in the request is ignored, and an edit makes the editor the creator, so what a
schedule does always runs as the person who last defined it. Reading schedules
now requires signing in. A user who is not an administrator sees their own
schedules and those in projects they can reach, and can change or delete only
their own. A schedule also fires once when more than one server instance shares
the database.
Workflow schedules are not supported yet. Creating or editing a schedule
whose metadata names a workflow_id is refused with a message saying so,
because nothing on the instance runs a scheduled workflow. An existing schedule
of that kind is kept. Its firings create nothing, and the server logs one
warning naming it. Delete it, or retarget it to a directive.
Nothing to do unless a script reads or writes schedules without signing in,
or sets created_by itself.
The Updates page says whether the update service is current [server image]¶
An update replaces the server but never the update service (the hivemind-updater
container) that applies it. The Updates page now asks the update service which release
it runs and compares it with the server's own:
- The same release: it says so.
- A different release: it names both, without guessing which is older, and gives
the command that recreates the update service on its pinned image:
docker compose up -d --force-recreate hivemind-updater. - An update service too old to say: it says that, with the same command.
- No answer (for example while it restarts): it says "Not read" and why, and does not warn, because a missing answer is not evidence of anything.
The page only reports; it never acts. It needs nothing new on the update service.
The Updates page says what an update does before you apply it [server image]¶
When an update is offered, the Updates page now shows one card above the Install button: the version you are on and the one you would move to, whether the update changes the database, whether your agent runners need a re-pin afterwards, what the restart means, and a link to these notes.
The database line is read from the release's signed manifest, which now carries the number of database migrations the release adds. A release that adds any says it cannot be rolled back, because migrations only run forward. A release that adds none says you can go back. A release published before manifests carried the count says it does not know, and asks you to read the notes first; it is never shown as "no migrations".
The runner line is read from the same manifest: if the release ships a runner image, the card says your runners need a re-pin, which recreates them.
Nothing to do. The card appears the next time an update is offered.
Empty pages say what is missing and what to do next [server image]¶
On a new install, the Agents and AI Agents pages used to show a blank area or a bare "No agents yet." Each now says what is missing and the one thing to do about it, such as asking Facet on the Dashboard to send an agent, or creating a bot. A bot nobody has talked to shows No usage yet instead of a $0.0000 figure that read like a measurement, and a failed read of bot usage now says the read failed instead of showing an empty page.
A metric counts replayed estate reports [server image]¶
When a workstation's signed estate report arrives a second time (the same source and
nonce), the server still answers it exactly as it answers a first delivery, on purpose,
so a caller cannot tell a replay from ordinary traffic. Until now the only trace of a
replay was a log line. /metrics now also counts them:
hivemind_estate_ingest_duplicate_envelopes_total, labelled by source, so you can
graph a replay burst or alert on one.
Nothing to do. It is an ordinary series, not a spend series, so it is served without
HIVEMIND_METRICS_TOKEN.
The metrics token is checked in constant time [server image]¶
When you set HIVEMIND_METRICS_TOKEN, /metrics now compares the token a
caller presents without stopping at the first wrong character, so response
times no longer reveal how much of a guess was right. Nothing to change on your
side.
The Fleet page lists each workstation session and who it reports to [server image]¶
The Fleet page (/estate-fleet, administrators) used to show only how many
sessions each workstation was running. It now also lists them in a table: each
session's role, its project, which session it reports to, and its state. The
Commander is highlighted. The count is unchanged, and a workstation that does not
report the detail shows the page's usual "not reported" instead of an empty table.
Nothing to do. The page only reads what the workstations already report.
Facet does not act in the same answer that read an agent's output [server image]¶
An agent's output, its error text and its report can contain things the agent read on the web or in a repository, including text written to look like an instruction. When Facet reads them to answer you (for example "what is this agent doing?" or "what did my agents do today?"), that answer can no longer stop, message or dispatch anything. It works the same way as after Facet reads a web page: it answers from what it read and offers the next step as a question, and your next message can act.
What changes for you: "check agent X and stop it if it is stuck" in one message now takes two: the first answers, the second stops it. Asking Facet to stop an agent without reading its output first works as before.
Outside text cannot act: now also MCP reads, and Facet's notes [server image]¶
The rule above, that an answer which read outside text cannot act, now covers more of the ways that text could still get through.
- A read from one of your MCP servers counts as outside text. A tool you set to run directly (not behind a confirm card) returns your server's text, so an answer that used one cannot propose, steer or stop anything; the read itself still runs, and Facet answers from it and offers the next step as a question. A tool behind a confirm card only proposes, so it does not count.
- The whole step is checked before anything in it runs. When Facet asks for several tools at once, one of them reading the web, an agent's output or a direct MCP tool holds back every acting call in that step, whatever order they came in, and even if the read then fails.
- Facet's notes for the next message keep no outside text. Facet keeps a short note of each tool call to remember the conversation. A note for a tool whose result is outside text (any MCP or hosted tool, a web page, an agent's output) now keeps only what was asked and a fixed line saying the result was not kept; before, it kept up to 400 characters of the result, which reached the next message. And once an answer has read outside text, the notes of any later call in it keep no arguments either, since the model may have written them from what it read.
Nothing to do. An answer that uses an MCP tool set to run directly now takes one more message to act, as the rule above already does for web pages and agent output.
Facet proposes the work on the Anthropic backend too [server image]¶
When you ask for work and Facet's first reply only describes it, Facet asks the model
again, requiring it to use the proposal tool, so you get a card. On an install that runs
the assistant on Anthropic (HIVEMIND_ASSISTANT_PROVIDER=anthropic), that second request
failed, so the reply stayed a description and no card was offered. It now works as on
the OpenAI backend.
Nothing to do. Installs on the default OpenAI backend are unaffected.
Changing a bot's knowledge needs an admin key [server image]¶
Adding, changing or removing a bot's knowledge snippets through
/api/v1/bots/{id}/knowledge now needs an admin API key. A non-admin key
gets 403, and nothing is stored. A snippet is sent to the model word for word
as the bot's grounding, so writing one is as powerful as editing the bot's
prompt, which already needed an admin. Reading snippets is unchanged, and so is
the bot's Knowledge tab, which already required an admin.
Pausing or resuming a bot was already admin-only. Its audit entry now names the
role of the person who did it. If your instance has turned admin-role
enforcement off (HIVEMIND_ENFORCE_ADMIN_ROLE), a pause by a non-admin is
recorded as exactly that, instead of as an admin action.
Nothing to do unless a script writes bot knowledge with a non-admin key. Give it an admin key.
The installation's spending is visible to admins only [server image]¶
Any signed-in user could see what the whole installation spends: the Costs page, the dashboard's Cost tile, and the statistics and usage APIs behind them. Now:
- The Costs page shows a plain "for admins only" page to anyone who is not an administrator, and the Costs link is not offered to them.
- The dashboard shows the Cost tile to administrators only.
/api/stats/summary,/api/stats/costs(and their/api/v1versions) and/api/agent-chat/usage/summaryanswer403to a non-admin./metricsserves spend only withHIVEMIND_METRICS_TOKEN. With the token unset,/metricsstill serves every operational series, but leaves out the three spend gauges (hivemind_cost_usd_total,hivemind_agent_cost_usd_daily,hivemind_agent_cost_cap_usd). Set the token, and scrape with it, to get them.
Nothing to do unless a non-admin key reads these APIs, or your Prometheus
scrapes spend from /metrics without a token. For the latter, set
HIVEMIND_METRICS_TOKEN and add it to the scrape config.
An agent is finished only when it reports a result [server image] [runner image]¶
An agent used to be marked completed when a marker file appeared, when its terminal looked idle, or when anything with an API key said so. Completed is what starts a builder's review, so those shortcuts could start a review of work that was never reported.
Now an agent is completed only when the server finds a result the agent itself reported during its current run. Each completion leaves a receipt, and a builder's review starts from that receipt, once. What changes for you:
- An agent that goes quiet without reporting now ends as failed, with the reason "stopped responding". Its last screen is still kept, so you can read what it was doing.
- An agent that signals it is done but reports nothing ends as failed, with a reason that says so.
- Setting an agent's status to completed through the API is refused for every key, including an admin's.
- Each completion the runner reports is on the authorization audit log, as
its own
finalize_completionoperation, so you can tell which runner completed which agent. Each cost report from a runner is recorded the same way, asreport_agent_cost.
You must update the server and the runner together. Re-pin the runner to the same release as the server. A runner from an earlier release reports completion the old way, and this server refuses it with a message telling you to re-pin; the agent stays running until the runner is updated or the agent is stopped. It is never marked completed by mistake.
An agent that stops reporting is marked, and its conversation is told [server image]¶
The agent reaper is now on by default. Once a minute it looks for agents that are stranded: the agent's process is gone, or it finished a turn and then went quiet for five minutes. It marks such an agent completed if it had already reported its result, and failed otherwise, with the reason (for example "stopped responding"). It then posts a line in the conversation the agent was started from, so you hear about it instead of finding an agent still shown as running.
It only reports: it never stops or kills a process. An agent that is working stays running.
This is what makes the "goes quiet" and "stopped responding" behavior in "An agent is finished only when it reports a result" happen on every install. Before, the reaper was off unless you turned it on.
To turn it off, set HIVEMIND_AGENT_REAPER_ENABLED=false in .env and restart the
server. An empty or missing value means on.
One reply at a time in a Facet conversation [server image]¶
If the same Facet conversation is open in two browser tabs, and a message is sent in one while the other is still getting a reply, the second message is now refused with a plain explanation instead of being answered alongside the first. Before, both were answered against the same history, and neither answer knew about the other question. Different conversations are not affected and still run side by side.
Facet also now records which page a message was sent from, so later releases can show it in your conversation history. Nothing is shown yet.
When you ask Facet something on an agent's page, what it reads from the page is the agent's name, state, times, exit code, model and cost. It does not read the agent's own output or error text from the page, because that text can contain things the agent read on the web or in a repository. Ask what the agent is doing and Facet can still look up its latest output with its agent status tool.
Nothing to do.
0.1.95 ¶
0.1.95 adds 4 database migrations, so this upgrade is FORWARD-ONLY: once you are on 0.1.95 you cannot go back to 0.1.94 by updating. All four are additive: one adds a table that records who each assistant conversation is talking to, one fills it in for conversations that already had a question to the Commander, one adds a column that records the model each agent ran with, and one adds the table that holds your MCP tool grants. None deletes anything. If you want the option of returning to 0.1.94, take a database backup first. The Upgrade page describes the snapshot the updater keeps for you.
Each part below says how it reaches you:
- server image: arrives when you upgrade the server (Install here, or the updater).
- runner image: the agent runner container. An upgrade does NOT move it; you re-pin its image yourself (see Upgrade, "The agent runner is NOT upgraded by an apply").
- agent daemon (host binary): a daemon you run yourself changes only when you replace its binary and restart it.
- updater image: the managed-update sidecar. An apply fetches it but does not replace the
running sidecar; you recreate it yourself (
docker compose up -d --force-recreate hivemind-updater), and until then the update view shows it as pending. - installer:
scripts/install.sh. It reaches an existing install the next time you run it, from a new invite or a re-run. - docs: these pages.
Upgrading from 0.1.94: if Install here does nothing. The fix for that button is part of
this release, so it only works once your install runs it. On 0.1.94, press Check for
updates first and then Install here. If the button still does nothing, start the
upgrade from the API as an admin: POST /api/admin/updates/apply with the body
{"target_version": "0.1.95"}. This is needed once, for this upgrade.
Agents run contained [runner image] [agent daemon (host binary)]¶
Agents the dashboard assistant launches are run by the Hivemind agent daemon
(hivemind --headless): either the agent runner container (the compose profile runner)
or a daemon you run as a host binary. From this release the daemon runs every such agent
contained, or does not run it at all. Nothing falls back to running an agent
uncontained. An upgrade of the server changes neither: move the runner to this
release's runner image, or replace your host binary.
The agent runner container uses Landlock. Its image selects it
(HIVEMIND_AGENT_SANDBOX=landlock) and names the runner's Anthropic key secret as the key
file for its agents, so you change nothing in docker-compose.yml. Bubblewrap cannot run
inside the container under Docker's default security profiles, and none of them is
loosened.
- Kernel requirement. By default the host's kernel must offer Landlock ABI 6 (Linux 6.12 or later). ABI 6 is the first that also stops an agent from signalling other processes, the daemon included, and from reaching abstract unix sockets outside its sandbox.
- Older kernels. To accept Landlock ABI 3, 4 or 5, set
HIVEMIND_AGENT_LANDLOCK_MIN_ABIto that number in the runner's environment. That opt-in gives up the signal and abstract-socket protection, and it is logged at every launch. Any other value is refused, and nothing below ABI 3 is ever accepted. - Without it, a launch on an older kernel is refused, and the refusal names the setting.
A daemon you run as a host binary uses bubblewrap. It needs:
- bubblewrap (the
bwrapcommand) installed and on the daemon'sPATH. To use a binary somewhere else, setHIVEMIND_BWRAPto its path. - A key file for the agents. Put the Anthropic API key your agents should use in a
file only the daemon's account can read, and set
HIVEMIND_AGENT_ANTHROPIC_KEY_FILEto its path in the daemon's environment. The daemon only reads this file; it never creates or changes it.
If anything is missing, the launch is refused. The agent is recorded as failed, and its detail says what is missing. The daemon keeps running and keeps taking work.
What a contained agent can and cannot do.
- It can write only its own worktree, its own git directory and its own state directory. Everything else is read-only (bubblewrap) or refused (Landlock).
- It cannot see the daemon account's home directory or
/run, so that account's settings, keys, SSH configuration and other repositories are out of reach. Under Landlock the agent also cannot read/tmp,/var/tmp, the key file, or the rest of the runner's repository, including other agents' worktrees. - Its environment is rebuilt from a short allowlist. No push or forge credential is passed in.
- It commits into a private git directory. After the run the daemon brings its branch
back into your repository as data, and only as a fast-forward. Host git never runs
through a worktree's own
.git. A worktree whose.gitthe repository does not confirm is refused, and the worktree's.gitis put back after every run. - Under Landlock, a worktree that is a full clone (its own
.gitdirectory) is refused, because Landlock cannot make that directory read-only. The runner uses linked worktrees. - Its settings cannot be changed by the agent. The daemon builds them from its own
templates for the agent's role: what that role may do without asking, and its hooks,
together with a check that refuses pushes, tags, remote and config changes, and writes
outside its worktree. The default permission mode is
default. Under both Landlock and bubblewrap, Claude reads them from a read-only file and loads no other settings file, so a settings file in the worktree has no effect. - Under Landlock, file descriptors the daemon had open are closed before the agent starts.
- Under bubblewrap, file descriptors the daemon had open are also closed before the agent starts.
What stays reachable, stated rather than implied.
- The network. The agent needs the model API and your instance, so it has network access.
- Read-only views of the system, and the repository's refs, including other branches.
- Under Landlock, other processes' command lines are visible (there is no separate process view), though their environment is not readable.
The Anthropic key is in the agent's environment. Only the key FILE is withheld. The
key's value is in the agent's environment as ANTHROPIC_API_KEY, because Claude needs it
to call the model. The agent has network access, so an agent that is misled (for example
by text in a repository it reads) can read the key and send it anywhere it can reach. If
you bring your own key, treat any key you give agents as exposed to them. Use a key
dedicated to agents, with limits you can live with, and rotate it if in doubt.
Docker.
- The agent runner container never holds the Docker socket. It reaches Docker through a proxy that answers only liveness and version requests.
- Under Landlock, an agent can connect to a unix socket at a known path, even in a
directory it cannot read (HIVE b3f5a52d). Do not run a daemon with
HIVEMIND_AGENT_SANDBOX=landlockon a host where its account can reach the Docker socket (for example, a member of thedockergroup with/var/run/docker.sockpresent): its agents could drive Docker, which is equivalent to root on that host. The runner container as shipped has no such socket. Bubblewrap replaces/runwith an empty directory, so the default host daemon does not see it. - A Docker API exposed over TCP on the host is reachable from any agent, like any other network service.
What is not contained yet. Containment covers agents that run headless, which is how the
assistant launches them. An agent started in a terminal pane is not contained. If you have
turned headless dispatch off (HIVEMIND_ONESHOT_DISPATCH_ENABLED=false), agents the
assistant launches run in panes, and so run uncontained.
Follow-ups stay with who you were talking to [server image]¶
A conversation with the assistant now carries its target: the Commander, or one agent the assistant started for you. Once you confirm a question to the Commander, or confirm an agent, that becomes the conversation's target, and your follow-ups go there:
- With the Commander as the target, each message you send becomes a new question to the Commander, shown as a card that you confirm. It names the question it follows.
- With an agent as the target, each message becomes a message to that agent, again as a card you confirm. A headless agent reads it when it next checks its messages.
- A follow-up never starts a new agent by accident. While a conversation has a target, the assistant cannot propose a new agent in it. If the agent has ended, the reply says so and offers to start a new one or clear the target. It is never replaced silently.
The conversation header shows Talking to: with the target and a clear button. Clear it, or start your message with "start a new agent", to talk to the assistant again.
Conversations from before this release that ended on a question to the Commander are given the Commander as their target by the upgrade, so their next follow-up routes the same way. A target you have since cleared is left cleared.
A Commander reply is the Commander's words, not the assistant's [server image]¶
A reply from the Commander is now stored and shown as the Commander's, labelled Commander, while the assistant's own messages are labelled Facet. The assistant reads the reply as a quotation, never as its own plan, so a numbered list in the Commander's answer can no longer become a list of actions the assistant goes on to propose. The reply cannot break out of its quotation, and control characters are removed from anything relayed in either direction. Under each reply, a line says how to answer it: your reply goes to the Commander as a new question, sent only when you confirm it.
A question to the Commander says whether it can be delivered [server image]¶
Before you confirm a question to the Commander, its card now says when nothing can deliver it: no trusted workstation is configured, the workstation has never checked in, or it is offline (with when it was last seen). When the workstation is online, the card adds nothing.
You can still confirm. The question is queued, and a workstation configured or back online later collects it. A confirmed question does not expire: it waits until a workstation collects it. The one-hour limit applies only to confirming a proposal, not to a question you have confirmed. The confirmation line in the chat says the same.
The Commander link no longer logs a warning on every request about its optional shared key
(HIVEMIND_COMMANDER_LINK_KEY_FILE / HIVEMIND_COMMANDER_LINK_KEY) when you use the
trusted workstation key instead. The server logs which link authentication is active once,
at startup. A shared key you have configured but that cannot be read still produces a
warning.
Agents started from the chat are read-only, and their card says what they can reach [server image]¶
An agent the assistant proposes now always gets a read-only role: researcher, auditor or security. It is not given file-edit tools. A proposal from the chat can set only a name, a role, a prompt and a project name. Anything else the model supplies is dropped before the card is recorded, so what you confirm is what runs.
The card states what the agent can reach: its project, that its workspace is read-only, and that it runs contained on an executor running this release or later.
Naming the project. On an install with more than one project, a proposal from the chat could not say which project it meant, and was refused. It can now name one. The server matches the name exactly, ignoring case, and refuses an unknown or ambiguous name, listing the projects it could have meant. The card shows the project the server chose.
Release work is refused from the chat [server image]¶
The assistant now refuses, by name and before anything is recorded, to propose an agent or a message whose task is to cut, tag, publish or ship a release, or to bump a version. Reading release notes or listing tags is still fine. Releases are for you to run directly. This is a first filter; containment is what holds if a phrasing gets past it.
Updates page: Install here works, and an image is not offered as a download [server image]¶
- Install here on the Updates page did nothing on some installs, although the page offered it. It now starts the upgrade the page offered, and if something blocks it, the page says why instead of doing nothing. See "Upgrading from 0.1.94" above for this one upgrade.
- A Hivemind release is a container image that the updater pulls and installs. The page no longer shows a Download link for it (the link went nowhere). It says the release installs with Install here.
The runner's key can use memories in its agents' projects [server image]¶
Agents the runner launches use the runner's key. Searching memories with that key was refused (403) in every project. The runner's key now reaches the memories of every project that has an agent on a machine the runner owns. It can read, create, change and delete memories there, the same way it already reaches those agents. It still reaches no other project, and nothing on a machine it does not own.
Fixes from customer reports [server image]¶
- The watchdog's cleanup reported an error it had not made. Each time it deleted old logs or old agents, it logged a failure, although the deletion had happened. It no longer does.
- A report from an agent the runner launched shows the right sender. The dashboard looked up such a report's sender as an agent, and the runner's own account is not one, so the lookup failed on every render. It now names the agent that made the report, and does not repeat a lookup that failed.
hivemind-server --helpno longer prints secret values. It showed the values ofDATABASE_URLandHIVEMIND_APP_DB_PASSWORDfrom the environment. Both are hidden now. The server also removes the password insideDATABASE_URLfrom anything it logs.- An agent records the model it actually ran with. When a dispatch names no model,
the server picks one, and that choice was not recorded anywhere. It is now stored as the
agent's
effective_model, and returned with the agent by the API. Agents started before this release show no value. This adds one database migration, which adds a column and changes no existing data.
Agents can use tools from MCP servers you register [server image] [runner image]¶
You can now give a launched agent tools from an MCP server of your own. It is one journey in two steps, both admin-only, both through the API; there is no Settings page for this yet. The worked example is under "MCP servers for agents" in the API reference.
- Register the server:
PUT /api/v1/agents/mcp-serverswith each server's name, the command that starts it on the agent's host, and optional arguments, environment and description. The whole set is replaced by what you send, so a server you leave out is removed. - Grant its tools:
PUT /api/v1/agents/mcp-grantswith the server's name and either every tool or a list of tools. An empty list grants none, and that is kept distinct from a server with no grant. The whole set is replaced here too.
A registered server with no grant is visible to the agent with every tool denied, and a grant names nothing until its server is registered: you need both.
- Read-only roles (every agent started from the chat) receive a grant only when you turn it on for that grant, so a grant made with a building agent in mind does not widen what a read-only agent may do.
- Names follow one rule for both steps, so every server you can register is one whose
tools you can grant: letters, digits,
_and-, no__, and nothivemind. A request that breaks a rule is refused with a400naming the value, and nothing is written. - Keep secrets out of
env. Put a path to a file there instead, as the example does.envvalues are returned in full only to an admin and to the agent runner; every other key sees the variable names with the values shown as[redacted]. - To confirm an agent received a grant, read the settings file the agent actually loads
under its state directory, as the API reference shows. The
.claude/settings.jsonin the agent's worktree is not that file. - A tool that needs the Docker socket still fails, whatever you grant: the runner container has no Docker socket, and under bubblewrap it is hidden. The exception is a daemon run with Landlock on a host whose account can reach the Docker socket; see the Docker caveat above.
How it reaches you. The two routes and the database migration arrive with the server image by upgrading. They take effect only when the agent daemon also runs this release: the runner image, which an upgrade does not move, or your replaced host binary.
- A server upgraded without the runner stores servers and grants that no agent uses yet.
- A runner updated before its server starts agents on their built-in tools and logs a warning that the server predates them.
- If the runner cannot read the registered servers or the grants for any other reason, it refuses to start the agent rather than start one whose tools would all be denied.
The Updates page checks for releases on its own [server image]¶
After a restart, the Updates page showed no release until someone pressed Check. The server now checks your channel when it starts and then every 6 hours, and the page shows "Last checked" with the time. If a check fails, the page says so and keeps showing the last release it knew about. A check only reads the channel; it never installs anything.
- To change how often it checks, set
HIVEMIND_UPDATES_CHECK_INTERVAL_SECS(in seconds, never less than 15 minutes). To turn the automatic check off, set it tooffor0; the page then says "Automatic checks are off", and Check still works. - That variable reaches the server only through the
docker-compose.ymlthis release ships. An update does not rewrite your compose file, so on an install created before 0.1.95 add it as described under "Re-layingdocker-compose.ymlafter an upgrade" in Upgrade. Without it the check still runs, every 6 hours.
Machines on the Infrastructure page, and a runner left behind is now visible [server image]¶
An upgrade does not replace the agent runner container, so a runner can fall behind the server while its machine still reads healthy. The server already worked this out, and showed it nowhere.
- A Machines section on the Infrastructure page lists each registered machine: its status, last heartbeat, the runner version it reports, and whether it is retired. When a runner's version does not match the server, or it has never reported one, the row says so and links to the procedure for bringing the runner up to date.
- The server log says it once per change, not on every poll: "runner version skew MISMATCH: recreate or re-pin the runner container", "runner version skew UNREPORTED", and "runner version skew resolved" when it catches up.
- Retire takes a machine you no longer use out of the list. It deletes nothing: retired rows are kept as history, and "Show retired" brings them back. Only an admin can retire a machine, and the server checks that itself, whether or not the button is shown.
Upgrade: bringing your docker-compose.yml up to date [docs]¶
An update replaces images; it never rewrites your docker-compose.yml. So a setting a later
release adds to compose never reaches an install created before it, and nothing said so.
Upgrade now has a section, "Re-laying docker-compose.yml after an upgrade":
compare first rather than overwrite, which settings recent releases added and what you lose
without each, and a one-line check for whether your file is behind.
The 0.1.93 notes below are corrected to match: that release does need the runner container recreated, and they now say how.
A runner's key no longer appears in URLs or request logs [runner image] [server image]¶
A runner sent its API key in the /ws URL, so a reverse proxy in front of your server
could record it in its access log.
- Runner image: the runner sends its key only in the
X-API-Keyheader, never in the URL, and identifies itself with ahivemind-runner/<version>user agent. - Server image: request logs record each query parameter's name, never its value. A runner that still puts its key in the URL keeps working, and the server logs one warning per runner key naming it: "a runner sent its API key in the /ws URL ... Update the runner image, then rotate this principal's key".
- If a runner reached your server through a reverse proxy, its key may already be in that proxy's logs. Update the runner image first, then rotate its key, in that order: rotating first puts the new key into the old runner's URL. See "A runner that connects through a reverse proxy: update it, then rotate its key" in Upgrade.
A runner reporting its own agent's lifecycle is no longer recorded as a kill [server image]¶
The ledger showed kill against an agent moments after it started and again when it
finished, although nothing was killed. Those rows were the runner reporting the agent's
own start and stop times. They are now recorded as lifecycle_report. What the server
permits is unchanged: the same request from anyone other than the agent's own runner is
refused, and is still recorded as kill. Rows written before this release keep their old
label.
Installing and running agents: corrections [server image] [installer] [docs]¶
- The installer no longer says your runner image is missing when it is pinned. A bundle
install that carried a runner image was told "Missing: a runner image. YOU CANNOT FIX THIS
ONE" and the runner was never started, because the installer looked for the image in
.envafter moving it intodocker-compose.override.yml. It now reads the pin where it lives. If you saw that message, your runner image was never missing: you do not need a new bundle or support. With an agent-runner credential and a repository in place, you can start the runner now with the pin you have (docker compose --profile runner up -d), and the corrected check arrives the next time you runinstall.sh. install.md's check for whether a runner image is pinned now asks compose itself. - A fresh install can register its first web admin again. Since 2026-09-08, a new install
without SSO answered
/auth/registerwith "no usable ADMIN_API_KEY", so no one could create the password admin for the web UI. The server now reads the key fromsecrets/admin_api_key, where the installer puts it, and the refusal, if you still see it, names that file. An install that already registered its admin, or that uses SSO, sees no change. HIVEMIND_OPENAI_API_KEY_FILEnow reaches the server. The 0.1.92 notes said you could keep the assistant's OpenAI key in a file with this setting, butdocker-compose.ymldid not pass it, so the file was ignored. This release's compose passes it. An upgrade does not rewrite your compose file, so on an existing install it arrives only when you re-lay compose, as "Re-layingdocker-compose.ymlafter an upgrade" in Upgrade describes (with a one-line check).- The runner's repository is set with
HIVEMIND_RUNNER_REPO_PATH. The install instructions, the installer's closing summary and the Settings runner panel told you to mount your repository at/workspace, which compose no longer uses; following them left the runner restarting. They now say to setHIVEMIND_RUNNER_REPO_PATHin.envto your repository's path, and the runner's refusals name that setting. - The installer's closing summary reports the runner correctly. On an install with its
own container prefix (a second instance on one host, for example) it looked for a
container named
hivemind-runnerand reported "NOT RUNNING" while the runner was up. Its advice for a missing repository now gives only the.envsetting, where it had mixed in the oldvolumes:instructions. - Install now tells you to create your first project. Agents belong to a project, and a fresh install has none, so the assistant would not start an agent. The step (create a project, owned by your own user) is now in Installing Hivemind, under "Supplying the agent-runner credential".
Facet: an ask that did not reach your Commander says why, and the card says what is live [server image]¶
- An undelivered ask says why. When an ask could not be delivered, the chat said "The Commander may not be running right now" whatever the cause. It now gives the reason your workstation reported: for example, that it has no Commander, that the Commander is not running, or that it could not confirm the ask arrived. The same sentence appears on the ask's card. A workstation running Spawn 0.1.7 or later sends that reason as a code; with an older Spawn, the sentence comes from the words it sends, and falls back to "could not deliver your ask" when they are not recognised.
- The card says whether the workstation is online before you confirm: "Workstation online, last contact 22s ago." An offline workstation was already warned about; the online line is shown as a status, not a warning.
- A follow-up to an ask that failed is no longer sent as if the failure were the Commander's answer. It now tells the Commander that the earlier ask did not reach it.
- With no project yet, asking for an agent now shows the step to create the first one, with a link, instead of a refusal only.
- An agent card is titled by the agent: "spawn ReadmeReader (researcher) in my-project". Its status line says whether the agent is running or completed, instead of repeating the "pending" recorded when it was dispatched.
The update view checks for the new OpenAI key-file setting [updater image]¶
The updater checks that your docker-compose.yml passes every setting the release needs,
and reports anything missing. Its list did not include HIVEMIND_OPENAI_API_KEY_FILE (see
above), so an install whose compose predates it was reported complete. The updater image
now checks for it. Like any updater change, it takes effect when you recreate the updater
(see the updater image note at the top of this release).
Smaller fixes [server image]¶
- The agent page and the assistant's first step said "this agent" when describing the assistant's own reasoning, which cannot run anything. It read as a limit on the agent that was then launched. Both now say it is the reasoning that cannot run anything, and that the agent is separate, with its own tools.
Commander link: the trusted-key setup [docs]¶
The 0.1.94 notes described only the shared key file for the Commander link. The normal setup, since that release, is a trusted workstation key: you generate it on the workstation and paste its public half into Settings, with no compose change and no restart. See Commander link, which also covers running the link behind SSO forward auth.
0.1.94 ¶
0.1.94 adds 0 database migrations, so the ordinary downgrade path stays open to you.
Most of this release arrives by upgrading. These parts do not:
- If you publish Commander status snapshots, update Spawn to 0.1.6 at the same time. The server now requires a signature over the snapshot's source. See the Spawn section below.
- The Commander link is off until you give this server its key. The shipped
docker-compose.ymldoes not declare the variable, and an upgrade never rewrites that file. - The update service's own image-pin check arrives only when the update service itself is recreated. An update replaces the server, not the update service. The server's new admin notice covers you until then; see the image-pin section below.
- The installer fix reaches you only if you run the installer again. Upgrading does not run it.
- If you replace
docker-compose.ymlyourself, the admin key file must exist first. See "The admin key is now a file" below.
Security fix: a connected agent could post a message under another agent's name [server]¶
Upgrade promptly. An agent connection allowed to steer work could write a message into your instance attributed to any agent, including a report or a handoff, and your operator views then showed it as that agent's confirmed report. Permission to act was checked; whose name went on the message was not. Four kinds of message were affected: sent messages, reports, handoffs and questions to the Commander.
The author of every such message is now the agent whose credentials made the connection, never a name the connection supplies. Arrives by upgrading (server image). Messages written before you upgrade are not re-attributed.
The admin key is now a file: what creates it, and what to do when it cannot [installer]¶
The current docker-compose.yml hands the server its admin key as a file,
secrets/admin_api_key, rather than an environment variable. An install set up before that
has the key only in .env as ADMIN_API_KEY. Laying the new docker-compose.yml over such
an install without the file makes Docker refuse to recreate the server:
Your running containers are left exactly as they were, and nothing is lost.
What creates the file for you. The installer, and scripts/deploy.sh for
build-from-source installs, create secrets/admin_api_key from the ADMIN_API_KEY already in
your .env before anything is recreated, when the account running them can see inside
secrets/. The key they write is the key you already have, so anything using it keeps
working. They never print it. scripts/deploy.sh and --provision-secrets-only never
replace a secrets/admin_api_key that already exists; the installer, as before, brings it
back in line with .env if the two differ, and says so.
When the account cannot see inside secrets/, which is common when you deploy through an
account that does not own the install, nothing is created and nothing is changed, and the
output says so in one line: cannot inspect secrets/ as <account>. It never reports a file as
missing when it simply cannot look. If Docker then reports the missing bind source above,
that is your signal to create the file as the owner, on the host:
Upgrade, "Replacing docker-compose.yml by hand", also has a by-hand command
that never shows the key.
The agent runner's key is never taken from .env. If you run the agent runner, it needs
its own Anthropic key in secrets/claude_api_key. The Anthropic key your .env may hold is
the server's own, and handing it to the runner would put it within reach of the code your
agents run. So when the runner is enabled and the provisioning step can see that file is
missing, it stops before anything is recreated and tells you how to add it.
Which upgrades meet this at all. The managed update never replaces
docker-compose.yml, so it never meets this. Re-running the installer handles it. Replacing
docker-compose.yml yourself needs the file first.
Ask your Commander from the dashboard assistant [server + Spawn]¶
You can now ask the assistant, in plain words, to put a question to your Commander. The assistant proposes it as a card; you confirm the card once, and the question waits for the Commander. Your workstation fetches it, delivers it into the Commander session, and posts the answer back into the conversation the question came from.
An ask carries your words, never your approval. It is a message, not an action: nothing is executed when you confirm it, and it never counts as an operator OK for anything the Commander would otherwise need you to approve. Your server never connects to your workstation and holds no workstation credential; the workstation dials out.
Turning it on is an operator step.
- Generate a key: 32 random bytes as 64 hex characters, for example
openssl rand -hex 32. - Put it in a root-owned, mode-600 file and point
HIVEMIND_COMMANDER_LINK_KEY_FILEat it, in your compose override. Recreate the server container. - Put the same key in a mode-600 file on the workstation and set
credential_pathandurlin Spawn'shive-ask.toml.
The key authenticates only the two endpoints the workstation uses, maps to no user, and cannot propose, confirm or decide anything. To rotate it, replace both files; the server reads the file on every request, so no restart is needed. Remove the server's key file to turn the link off.
A proposal card says what actually happened¶
A confirmed card now states the outcome that actually happened, and it says the same thing after you reload the page as it did the moment you confirmed. A card no longer reads executed when the action was refused, failed or deferred, or is still waiting for something to run it. A question to the Commander reads waiting on the Commander until the answer arrives, then the Commander answered, or no answer from the Commander if it could not be delivered.
The trail under an answer says what was proposed¶
Under each assistant answer, the trail of what the assistant did now lists reads as reads and states how many proposals the turn actually recorded, counted by the server. If the assistant says it proposed something it did not, the count on the same screen says so.
Persona fixes now reach installs whose assistant profile nobody has edited¶
Your assistant's built-in persona was written once, when your install was first set up, and never updated after that, so fixes to it in later releases never reached you. From this release, at startup, an assistant profile that no one has ever saved takes this release's persona. A profile you have saved in the admin UI is never touched: your edits win, and the server logs at startup that it left it alone.
One request to the assistant is one proposal, not two [server]¶
If the assistant called a tool on its own at the start of a turn and then answered, it could be forced to call a tool a second time: one request became two proposals, and a plain question could come back as an action. That second call now happens only when the assistant has not acted at all. Arrives by upgrading (server image).
Smaller fixes you will notice on the dashboard [server]¶
All of these arrive by upgrading (server image).
- A proposal waiting for your approval shows only in its own conversation. Before, a pending card appeared in every conversation, with its Confirm button live, so you could approve something from a thread you were not reading. Now it appears only where it was raised. Elsewhere you see a short line, "1 proposal waits for you in another conversation", with a button that opens it.
- Asking the assistant how things are now includes anything degraded. Its status answer now carries the same health verdict as your instance's health check, and names each connected service that is not healthy. A state that cannot be confirmed is never reported as healthy.
- One clock, in UTC, labelled. Messages that arrived live used your browser's local time, while the same messages after a reload used UTC, with nothing saying which. Every time on the dashboard is now UTC and marked as such.
- Clearer wording about approvals.
- The approvals panel used to say "Nothing is waiting on you" right beside a proposal waiting on you. It now says what it counts, approvals in the ledger, and where an unconfirmed proposal waits instead.
- The line under a question to the Commander now follows what happened: answered, no answer, or delivered.
- If your role cannot raise an action, the assistant now says that an administrator can, instead of sending you somewhere to do it yourself.
- Hidden characters in chat history are neutralized. Invisible formatting characters, such as the ones that reverse the direction of text, are removed when history is shown, so a message can no longer display differently from what it says.
- A Sign out control, on installs that use Hivemind's own sign-in. It ends your session and returns you to the sign-in page. It does not appear on installs where you sign in through your organization's single sign-on: signing out there has to end the session with your sign-in provider, and a button that could not do that would only appear to sign you out.
Your Updates page says whether your image pin can be checked [server]¶
The image pin is the file that decides which server a later docker compose up or reboot
starts. The Updates page now shows administrators a notice about it: whether it is fine,
needs attention (with the update service's own explanation and fix), or cannot be
checked, because your update service is older than the check. A panel that could not load
says not known. It never reads as fine when it could not look. Arrives by upgrading,
because the server is the one image every update replaces.
An update whose image pin cannot be trusted now reads "delivery incomplete", not "success"¶
Behaviour change, once your update service is recreated. This check lives in the update
service, and an update does not recreate the update service. Until it is recreated, the
notice above is how you find out. After that, the update service checks the image pin: the file
that decides which server image a later docker compose up or reboot will start. If the pin
is detached from the file on your host, names a different server than the one now running,
or is configured but unusable, the update now ends delivery_incomplete instead of
success. The new server is installed and running either way; only the record changes,
because a pin like that means the next recreate or reboot could quietly start an older
server. The delivery report names the pin finding and what to do about it, and
Upgrade, "Moving the image pin onto a directory mount", has the fix.
If your automation matches success: treat delivery_incomplete as "the new version is
installed and serving, and something needs your attention". Read the delivery report and fix
the pin; retrying the update will not change the outcome.
Re-running the installer no longer cuts the update service off its image pin [installer]¶
On an install older than 0.1.86, re-running the installer moved the image pin into its new folder by copying it. The running update service kept writing the old copy, so later updates changed a file your host no longer read: the outage the notice above exists to catch. The installer now moves the pin without breaking that link. If it cannot (the new folder is on a different filesystem), it says so and tells you to restart the update service. This reaches you only when you run the installer again; upgrading does not run it.
Commander status snapshots must now be signed over their source: update Spawn to 0.1.6 with this server [server + Spawn]¶
Security fix, and the two halves ship together. A status snapshot names the
Commander machine it came from (source), and this server uses that name to pick the
verification key, to attribute the snapshot, and to recognize a replay. Until now the
name was not covered by the signature, so on an install where two sources shared a key,
a snapshot signed for one could be relabeled as the other and still be accepted, and a
captured snapshot could be replayed under the other name as a new one. The signature now
covers the source and the envelope version, and this server accepts envelope version 2
only.
Update Spawn to 0.1.6 on every machine that publishes to this server at the same time as you update this server. Nothing breaks silently in either direction, but snapshots stop until both sides match:
- A Spawn older than 0.1.6 publishing to this server is refused with
400 unsupported_envelope_version, and the reason says to upgrade the sender. The Commander page reads Stale until Spawn is updated. - Spawn 0.1.6 publishing to a server older than this release is refused with
401 bad_signature, and Spawn's publish result names the version mismatch as the likely cause. Update the server.
Spawn does not queue a snapshot that was refused for its version, so updating does not
replay a backlog of old ones. A snapshot already waiting in Spawn's queue that this server
refuses for good (the wrong version, too old, or malformed) is moved aside into the queue's
quarantine folder with the server's reason beside it, and the rest of the queue keeps
delivering. Spawn's publish output names each one it set aside. If you have not enabled
Commander activity, there is nothing to do.
A Commander status snapshot older than ten minutes is refused [server]¶
Security fix. A snapshot's timestamp was signed but never checked, so a captured snapshot
could be replayed at any time later. This server now accepts a snapshot only if its
timestamp is at most 10 minutes old and at most 2 minutes in the future. Anything
outside that window is refused with 400 stale_envelope, and the reason says how old it was
and what to check: the sending machine's clock, or a snapshot that sat in a queue too long.
What that means for an upgrade: a snapshot your Commander machine could not deliver while this server was down or being upgraded is refused if it is delivered more than 10 minutes after it was taken. That loses nothing current: the next snapshot, taken fresh, is accepted. If the Commander page reads Stale after an upgrade, check the sending machine's clock first. Arrives by upgrading (server image).
0.1.93 ¶
One container to recreate beyond upgrading (the runner — see below), and 0.1.93 adds 0 database migrations, so the ordinary downgrade path stays open to you.
Most of this release arrives by upgrading — but one fix does not. The correction that makes your runner report its version ships in the runner image, and an update does not recreate the runner container. Until you recreate or re-pin it, your server will keep reporting that runner's version as unreported, which looks the same on every surface as a runner that is up to date.
To pick it up, recreate just that one container after the upgrade:
Then confirm the server stops reporting unreported for it. Nothing else in 0.1.93 needs a
container recreated or a file edited. The fuller procedure, including how to tell whether
your own docker-compose.yml is behind, is under
Re-laying docker-compose.yml after an upgrade.
Your agents' results are attributed to the run that produced them¶
When an agent finished a job and reported its result, the alert you received could still say the run completed with no result recorded — while the result was sitting in your instance the whole time. The report was stored correctly; nothing connected it back to the run.
That connection now exists, and the surfaces that tell you a run finished use it. A run that reported reads as having reported, on the phone alert, in the assistant chat, and on the agent's own page.
Two limits worth knowing. Runs that finished before you upgrade stay unlinked — the fix looks forward and does not reconstruct history. And where a report genuinely was never filed, you still get the honest "no result recorded", because that sentence now means what it says.
A bot whose credential died no longer reads as one you never connected¶
In the fleet view, a bot whose chatalot token had expired or been revoked was reported as "no active chatalot integration" — word for word what a bot you had never set up shows. Two opposite situations, one sentence. Following it, you would go and re-connect an integration that was already connected, and the failure was shown as degraded when the bot was in fact down.
It now says which of the two happened, and a dead credential is marked critical. Expiry and revocation are named separately, because what you do about them differs: an expired token is rotated, a revoked one is worth understanding before you issue another.
A bot with genuinely no integration reads exactly as it did before.
A repeated report from one of your machines is accepted, not rejected¶
If a machine reported its state and retried — after a timeout, a restart, or a flaky link — the second, identical report was answered with a server error. Nothing was wrong: the report had already been recorded. A retry that is refused looks to the sender like a fault, and the usual response is to retry harder.
An identical repeat is now accepted quietly, as it always should have been.
An unrecognised command tells you what it did not understand¶
Asking Hivemind to do something it has no verb for answered with a bare refusal that named neither the thing it could not do nor what it can. You were left guessing whether you had the wording wrong, the permissions wrong, or had found a defect.
The refusal now says what it could not understand.
0.1.92 ¶
0.1.92 adds 1 database migration, so this upgrade is FORWARD-ONLY: once you are on 0.1.92 you cannot go back to 0.1.91 by updating. Read this before you upgrade, not after. If you want the option of returning to 0.1.91, take a database backup first — the Upgrade page describes the snapshot the updater keeps for you and how long it is kept.
Most of this release reaches you by upgrading alone. Two items do not. They are in their own sections below, each with the exact command it needs. They are separate because they arrive by different routes, not because they are optional: the container images an update replaces for you, an image that is replaced only when you recreate it, and one file that was written when you installed and is never rewritten.
Agent results reach the right place¶
When you asked the dashboard assistant to relay something, the agent it dispatched was told to report using a command it was never permitted to run. It could not carry the instruction out, said so in its own words, and fell back to its normal reporting channel — so the message arrived, but not where the instruction claimed it would, and the agent's transcript carried a confusing refusal that read like a fault.
If you do not run the optional coordination tooling that instruction referred to, the refusal was pure noise: your agent was being told about a tool your installation does not have.
The instruction is gone. Agents are now pointed at the reporting channel they actually have, and nothing is addressed on their behalf that this installation cannot address.
One request no longer produces two agents¶
A single request could produce two approval cards. Accepting both started two identical agents seconds apart, doing the same work twice.
An identical request that is still awaiting your approval is now recognised as the same request rather than written a second time, and you are shown the proposal you already have. Requests that differ in any way — different wording, a different target, or the same request made again after the first was approved, declined or expired — still get their own card, as they should. This is enforced where the record is written, so a duplicate cannot exist even briefly.
The authorization audit is readable again¶
Routine liveness checks were being written into the same audit record as authorization decisions. On a busy installation the decisions you would actually want to review were buried under thousands of routine entries.
Liveness checks no longer enter that record. Nothing that was being audited has stopped being audited — the decisions are all still there, and the chain they form remains complete and verifiable. There is simply far less noise around them.
This is the change that adds the database migration.
Your assistant's API key can live in a file¶
The OpenAI API key backing the assistant could only be supplied as an environment variable, so it sat in plain text in your compose configuration and in the environment of a running container.
You can now point at a file instead. Set HIVEMIND_OPENAI_API_KEY_FILE to a path holding
the key — a Docker secret, or any file whose permissions you control — and leave the key
itself out of your configuration. The existing HIVEMIND_OPENAI_API_KEY variable still
works exactly as before, so nothing you have set up needs to change; if both are present,
the file wins.
The update checker can now see your agent runner — RECREATE REQUIRED¶
Before this release, the check that tells you whether an update can be applied examined only the main server container. It did not examine the agent runner, so a problem that would stop the runner receiving an update went unreported until the update failed.
This fix lives in the updater's own container image, and applying an update does not
replace that container. The update fetches the new updater image and records that it is
waiting — you will see an UpdaterRecreatePending status — but the running updater stays
as it is until you recreate it yourself:
Until you run that, the improved check is not doing anything for you. This is surfaced rather than silent — the pending status is visible in the update view — and nothing else in this release depends on it.
Agent health checks see the hosts you configured — EXISTING INSTALLS NEED ONE EDIT¶
If you set HIVEMIND_HEALTH_URLS or HIVEMIND_TLS_DOMAINS, the agent runner never
received them. An agent asked to check the health of your hosts reported that no targets
were configured, whatever your .env said. The values reached the server and stopped
there.
The shipped compose file now passes both through to the runner.
Your docker-compose.yml was written when you installed and is never rewritten by an
update, so a new shipped file does not reach an existing installation. New installations
get this automatically. If you already have Hivemind installed and you use either setting,
add this to docker-compose.override.yml in the same directory:
services:
hivemind-runner:
environment:
HIVEMIND_HEALTH_URLS: ${HIVEMIND_HEALTH_URLS:-}
HIVEMIND_TLS_DOMAINS: ${HIVEMIND_TLS_DOMAINS:-}
then run docker compose up -d hivemind-runner. If you use neither setting, there is
nothing to do.
0.1.91 ¶
Nothing to do beyond upgrading, and 0.1.91 adds 0 database migrations, so the ordinary downgrade path stays open to you.
Asking the assistant in plain language now works¶
Previously the dashboard Commander chat would only act if your wording already contained the word "propose". Asked to "send an agent" or "tell it X", it replied that it could only propose one and then did not — an answer that was not merely unhelpful but untrue, since it could, and had been asked to.
It now acts on the request however you phrase it. If a turn needs a tool and the assistant returns without using one, it is asked again with the choice no longer optional. Ordinary conversation is unaffected: a greeting still gets an answer, not an action.
Nothing has changed about approval. A dispatch is still proposed and waits for you to confirm it. This makes the assistant reach for the tool it already had; it does not let it act on its own.
Relayed messages reach the seat you are watching¶
0.1.90 fixed the addressing of assistant-relayed messages, and that fix did not work — it was applied at one entry point while the dashboard uses another, so nothing changed for anyone using the chat.
The addressing now happens at the single point every dispatch passes through, whichever way it was started. If you saw relayed messages arriving somewhere other than where you were looking, that is what this fixes.
0.1.90 ¶
Nothing to do beyond upgrading, and 0.1.90 adds 0 database migrations — so the ordinary downgrade path stays open to you.
Asking the assistant to relay something now reaches the right place¶
If you used the dashboard's Commander chat to ask the assistant to pass a message to your orchestration tooling, the message was being delivered — to the wrong destination. It went to the inbox of whichever project sits above the one the assistant was launched from, rather than to the operator's own. Nothing was lost; it arrived somewhere you were not looking.
The relay now addresses the recipient by role rather than by project name, so it reaches the seat you are actually watching. If more than one thing claims that role, it reports the ambiguity instead of guessing — picking one silently is how a message ends up somewhere plausible and wrong.
This half needs the matching orchestration tooling to be installed, because the instruction it sends names a capability the older tooling does not have. If your assistant relays are already arriving where you expect, nothing here changes for you.
0.1.89 ¶
Nothing to do beyond upgrading. Every change here ships in the server image, so a managed upgrade delivers the whole release. There is no runner image to repin.
0.1.89 adds 0 database migrations. A section that says nothing about migrations changes nothing about them — this one says it explicitly because the answer matters: with no migrations, the ordinary downgrade path stays open to you. That is a change from 0.1.88, which added 2 and is forward-only.
The dialog that flashed on every page load¶
If you refreshed the dashboard and saw a "New Directive" box appear and vanish, that was not a notification and nothing was arriving. It was the create-directive dialog itself being drawn on every page load and then hidden once the page finished starting up.
The dialogs were marked to stay hidden until the page's scripting takes over, but the styling rule that actually performs the hiding was never defined — so the marking did nothing. Nine of the thirteen pages that used it had no rule behind it. The rule now lives in the shared page template, so it applies to every page and every future one.
Both dialogs on that page were affected, not only the one you could see; the second was simply drawn underneath. If you saw this, it is fixed by upgrading.
Two smaller corrections¶
A confirmation now tells you what it is about to do rather than naming the thing it will do it to, so you can answer it without guessing.
An unreadable state no longer reports itself as an empty result. Previously "I could not read this" and "there is nothing here" looked identical on screen, and the first was being shown as the second — the more misleading direction, because an empty result reads like an answer.
0.1.88¶
Before you upgrade: read these first¶
You cannot go back to 0.1.87 by updating. 0.1.88 adds 2 database migrations, and migrations are forward-only (see Upgrade). The updater also refuses to install a version older than the one running.
Why 2 and not 1. One of them does nothing. 20260917120000 creates a table that has
existed since June, so it applies and changes not one row. It is still recorded as applied, and
for rollback that is what counts: the check that stops an older server starting compares
VERSION NUMBERS ONLY — it never looks at what a migration did. So a downgrade is refused naming
the migration that had no effect. Both constrain you equally; we would rather say so here than
have you meet it as a surprise.
If the migration fails on your box¶
The updater takes a pg_dump snapshot BEFORE it migrates, and a failed snapshot stops the
upgrade before anything is touched. If the migration itself then fails, the updater restores
that snapshot automatically and brings your previous version back up.
Each migration runs inside a single transaction together with its own bookkeeping, so a failure leaves your database at the version it started from — not half-migrated. No migration in this release opts out of that.
The one case that needs you. If the automatic restore ALSO fails, the updater stops and marks
the apply FrozenMaintenanceRequired rather than guessing. That state means what it says: it
needs a person. Your pre-upgrade snapshot is still on disk, and Upgrade has the
restore procedure.
0.1.87¶
Before you upgrade: read these first¶
You cannot go back to 0.1.86 by updating. 0.1.87 adds 14 database migrations, and
migrations are forward-only (see Upgrade). The updater also refuses to install a
version older than the one running. The updater takes a pg_dump snapshot before it applies, and
restores it automatically if the apply fails. Once 0.1.87 is running and healthy, returning to 0.1.86
means restoring that snapshot, and losing everything written after it.
Agents in some roles may start using a different model [server]¶
What changes. In 0.1.86 and earlier, the model router read per-role overrides from
llm.model.<role>. Anything you set on the older agents.<role>_model keys (and
agents.deliberator_thinking / agents.planner_thinking) was never read. Migration
20260915000103_hive1250_rename_per_role_model_keys moves those values onto the keys the router
actually reads. A value you set on an old key takes effect for the first time when you upgrade.
This is not a regression: the setting you chose is finally honoured. It is still a change in which model does the work, on the roles that do the deepest reasoning, and it arrives with the upgrade rather than when you chose it.
Who is affected. Only installs where someone changed an agents.<role>_model key. The shipped
values of those keys were the router's own defaults, so an install that never touched them sees no
change. If llm.model.<role> was already set, that value is kept and the old key is discarded.
| role | default model (used if you set nothing) | after upgrade, if you had set agents.<role>_model |
|---|---|---|
| deliberator | claude-opus-4-6 |
the model you set |
| planner | claude-opus-4-6 |
the model you set |
| reviewer | claude-opus-4-6 |
the model you set |
| meta | claude-opus-4-6 |
the model you set |
| builder | claude-sonnet-4-6 |
the model you set |
For example: if you had set every one of these keys to a Sonnet model, your deliberator, planner, reviewer and meta agents move from Opus to Sonnet on upgrade. They respond faster and reason less deeply.
How to check before upgrading: look at agents.deliberator_model, agents.planner_model,
agents.reviewer_model, agents.meta_model and agents.builder_model in Settings. To keep Opus
for a role after upgrading, set llm.model.<role> (for example llm.model.deliberator) to
claude-opus-4-6.
A cost cap you set earlier may start stopping agents [server]¶
Migration 20260915000104_hive1250_unset_cost_cap_defaults makes the cost caps live:
- A cap still at its shipped value (llm.cost_limit_per_agent = 2.00, llm.cost_limit_daily =
10.00) is cleared, meaning no cap.
- A value you changed is kept, and from this release it is enforced.
If you set a cap long ago and forgot it, check both keys before upgrading.
Settings that were never read disappear from the Settings page [server]¶
No behaviour changes, because nothing ever read these keys. You will simply no longer see them.
- Migration 20260915000101_hive1250_remove_vestigial_settings_keys removes:
agents.auto_deliberate, agents.auto_learn, agents.default_role, notifications.enabled,
notifications.telegram_bot_token, notifications.telegram_chat_id.
- Migration 20260915000102_hive1250_remove_inert_llm_provider_keys removes: llm.provider,
llm.default_provider, llm.endpoint, llm.model, llm.ollama_model, llm.ollama_url,
llm.api_key, llm.anthropic_api_key, llm.openai_api_key. The LLM provider is configured with
HIVEMIND_LLM_* environment variables; if you had put a value in one of these keys, that is where
it belongs.
Updating does not update your runner or updater containers¶
A managed update replaces the server container only. Fixes that live in the runner or updater images reach an existing install only when you recreate those containers on the new images yourself. Two such fixes shipped in 0.1.86 and have not reached an install that has only been updated: - Machine identity that survives a runner recreate (HIVE-1261). - The updater's report that a runner is behind (HIVE-1263).
Recreating the container is not enough on its own. The runner's image comes from
HIVEMIND_RUNNER_IMAGE, which the installer writes once into .env, and a recreate reuses whatever
that names. A managed update can move a pinned digest only inside docker-compose.override.yml; it
never edits .env. So bringing a runner up to a release takes two steps:
- Point the pin at the release's runner image. Each release manifest carries
registry.runner_imageandregistry.runner_digest(for example, 0.1.86's manifest nameshivemind-runner:0.1.86with its digest). SetHIVEMIND_RUNNER_IMAGEto that digest reference, wherever your install keeps it. - Then recreate, with the runner profile:
docker compose --profile runner up -d hivemind-runner. Without--profile runnerthe service is not selected at all.
A managed update still leaves the runner behind, and that is not fixed in this release (see Known and not fixed).
Every agent role's model and thinking override now appears in Settings [server]¶
The per-role keys the model router reads, llm.model.<role> and llm.thinking.<role>, existed
only for the roles that had an older agents.* row to be moved from. For the rest there was no
row, so Settings had nothing to show and the only way to set one was to know the key name and
create it by hand — which is how one self-hoster found that llm.model.auditor worked all along.
Migration 20260916171300_hive1257_seed_per_role_model_overrides seeds both keys for all twelve
roles, so every role is visible and editable in Settings.
This changes nothing on its own. The new rows are seeded empty, and an empty value means "use this role's built-in default", exactly as a missing row did. A value you set yourself, or one carried over by the migration described above, is kept as it is.
Reported, diagnosed and first patched by a self-hoster reading the settings from the outside.
AI-drafted content cannot be published without a person approving that exact text [server]¶
Before this release, any API key above read-only could publish a Content Studio
draft to a live target, or simply set its status to published, with no person
ever reviewing it.
What changes:
- No API key can publish, approve, or set a draft to
approvedorpublished, whatever its role. Those steps now require an operator in an interactive session (local login or SSO): approve the draft, then publish it. - The check happens before anything leaves the server. What is sent to the target is the text that was approved. If the draft is edited after approval, publishing is refused until someone approves the new text.
- Every approval, rejection, publication, and archival is recorded, with who did it and a hash of the text at that moment.
- A draft with a review record can no longer be deleted. Deleting it would
erase who approved and published it. Archive it instead:
POST /api/v1/content/drafts/{id}/archiveremoves it from the default draft list and keeps the draft and its record. Drafts nobody reviewed delete exactly as before.
Limitation in this release: there is no approve or publish screen. The
Content Studio Publish button now returns an error for every live target,
saying that human approval is required. Approving and publishing are available
only through the API, from an operator session:
POST /api/admin/content-review/drafts/{id}/approve, then
POST /api/admin/content-review/drafts/{id}/publish with {"target": "..."}.
These endpoints refuse API keys by design, so the requests need a logged-in
session cookie and its CSRF token. A review screen is planned; it is not in
this release.
What this gate is, and what it is not. It is enforced in Hivemind's
application code. It is not enforced by the database. On an install that
already exists, the server connects to PostgreSQL as the database owner, which
is also a superuser, so anyone holding those database credentials can write a
draft's approval state directly. On installs that run the server under the
separate hivemind_app role (HIVEMIND_APP_DB_PASSWORD), that role can add to
and read the review record but cannot change or delete it. That protection
covers the record only, not the approval itself.
An agent's dispatch now records the model it was asked to use [server]¶
Nothing recorded which model a dispatched agent was told to use — the field did
not exist. GET /api/v1/agents/:id (and the agent detail page) now carries
requested_model.
Read the name carefully: it is what the dispatch asked for, not what ran. When a dispatch does not specify a model, the server still routes one for the actual run (explicit override, then task-type routing, then your per-role settings), but that routed choice is a server decision, not the caller's request — recording it under this field would silently turn a caller's silence into a claim they never made. So an unspecified request renders as unknown on the page, not as the model that happened to run, and not as a blank field either — those are three different facts and this release does not conflate any of them.
One commit in this entry also touches a crate compiled into the runner image, for the runner's own self-registration path. It is inert there until you rebuild the runner: an un-updated runner simply keeps not setting the field, which renders as the same honest unknown described above, not as anything wrong. The field itself — reading it back on the API and the page — is entirely a server behaviour and arrives the moment you update.
Agent cost, tokens, and model attribution — the model half above ships, cost and tokens still do not, and now we can say exactly why¶
The complaint from 0.1.86's notes is still open for cost and tokens, and this time the reason is measured, not assumed. The value Claude Code actually spent on a dispatched agent's turns already exists — Claude Code writes it to a transcript file on the machine running the agent — but that machine is the runner, and the runner and the server share no filesystem. Nothing carries that transcript, or the numbers inside it, from the runner to the server today.
Recording it correctly requires the runner to start sending something it does not send today. That is a change to the code that ships inside the runner image — published and signed separately from the server, and not applied by a server update, the same fact that held this out of 0.1.86. Shipping it here would be exactly the failure mode you have told us about before: a changelog entry describing a fix you cannot receive. We are not doing that again.
What changed since 0.1.86 is precision, not verdict: we now know the exact mechanism (no shared filesystem, no existing wire message carries it) rather than inferring it from where the code would have to live. A follow-up ticket for the runner-side fix is open, and its own description states this same delivery constraint up front, so closing it will require answering "how does this reach an install that already exists" as part of the fix, not after it ships.
Commander activity on the Commander page — it ships, and until your Commanders publish to this server it will say "not received" [server]¶
The Commander page now shows what your Spawn Commanders have been working on over the last 24 hours. Each Commander gets a timeline (busy, idle, any other state, and the stretches where no snapshot arrived), plus a table with its current epic, what it is waiting on, its branch, how much of the window is covered, and the lanes it has dispatched. It is shown to administrators only.
It is drawn from status snapshots your Commanders publish to this server, and on most installs nothing publishes yet. Spawn's status publisher ships disabled, and this server needs the publisher's signing key. Until both are in place the panel says Not received and lists the steps. It does not draw an empty chart: an empty chart would say your Commanders did nothing, when the truth is that this server has heard nothing. The other ways the panel can be empty are named too. A key that is configured but has never received a snapshot reads as a broken pipe, a publisher that stopped reads as Stale, and snapshots that list no Commander say exactly that.
Turning it on is an operator step. Updating does not do it for you.
- On each machine that runs a Commander, enable a Spawn status sink that points at this server.
- Give this server the matching key as
HIVEMIND_INGEST_KEY_<SOURCE>_FILE. The shippeddocker-compose.ymldoes not declare this variable, and an upgrade never rewrites that file. Add it to your compose override and recreate the server container. Existing and new installs both need this step.
Every status snapshot is now attributed to the install that received it (a new migration). The server stamps its own identity on a snapshot when it verifies the signature, and never takes one from the sender. Both views that read snapshots, this panel and the estate fleet page, now show only snapshots attributed to this install. Snapshots received before you upgrade cannot be attributed after the fact, and we do not guess: they stay stored but are shown to nobody. If you were already publishing, both views read "not received" until your publisher's next snapshot arrives (five minutes at Spawn's default interval).
Two limits are marked on the page rather than hidden. A worker lane is linked to its Commander by project name, so when two Commander sessions share a project, their lanes are labeled as unattributable instead of being counted twice. And Spawn's publisher caps a snapshot at 64 lanes without saying so, so a Commander whose latest snapshot hit that cap is marked as possibly missing lanes.
A leaked API key can be rotated without editing the database [server]¶
Rotating a key used to mean running SQL against your database (HIVE-135). Now:
- On the host:
docker compose exec server hivemind-server recover rotate-api-key --username <name>prints the new key. It works for every account except agent credentials, includingadminand the runner. It refuses to run without a terminal. - Over HTTP, from a logged-in session:
POST /api/v1/me/api-key/rotate(your own key) andPOST /api/v1/admin/users/{id}/api-key/rotate(admin). Both refuse every API key. A key that could rotate keys would let whoever holds a leaked key lock out its owner.
The old key stops working the moment the rotation completes. Every attempt, including refused ones,
is recorded in a new api_key_rotations table, which never stores a key.
What it does not cover:
- Runner: rotating the runner's key leaves the rotation incomplete until the runner has the
new key. The command says so and exits 3. Set HIVEMIND_RUNNER_API_KEY in .env, recreate the
runner, then run recover rotation-status --username runner, which reports VERIFIED only after
the runner has authenticated with the new key.
- Web UI: there is no rotation button yet, so a browser session cannot call these routes without a
CSRF token. Use the host command.
- Step-by-step: Troubleshooting → API keys.
Your own secrets are redacted by value, not only by shape [server]¶
Assistant error excerpts and estate messages already redacted anything shaped like a Hivemind key
(hm-…). Two holes remained:
- The installer's admin and runner keys are bare hex, which no shape rule can recognise without also
redacting every checksum.
- Some secrets are not keys at all.
The server now also redacts the exact values it holds: the admin key, the runner key, the updater token and the application database password (HIVE-1283, HIVE-1287).
Who it reaches: this arrives by updating. Keys minted with a recognisable hm- prefix at install
time are a separate change that is not in this release, and would reach new installs only. On an
existing install, rotating the admin key (above) replaces a bare-hex key with an hm-rk- key.
Three reported security findings [server], one part [compose] (HIVE-1254)¶
- LLM API keys stored in Settings are encrypted at rest. Existing plaintext values are encrypted
when the server starts, so updating is enough. This needs
HIVEMIND_INTEGRATION_ENCRYPTION_KEY, which the installer has provisioned since 0.1.9. Without it, saving an LLM key is refused with an error that says so, rather than stored in plaintext. - The vault password is verified, rather than compared to a value an attacker who can read the database could supply.
- The claim that an
ENABLE ALWAYStrigger protected the audit tables was wrong, and the documentation and migration comments that made it are corrected. What actually protects them is that the database role the server connects as does not own them. That role separation reaches new installs only: it needsHIVEMIND_APP_DB_PASSWORDindocker-compose.yml, which an upgrade never rewrites. On an existing install the server still connects as the owning role, and moving to the separate role is a manual, operator-run procedure.
Corrections you can see¶
- Runner readiness reports a live runner as live. The readiness check read machine liveness as a count in a mismatched integer type, so a runner that was heartbeating was not reported as live. It now reads a yes/no answer (HIVE-1262). [server]
- The Updates button works after a previous apply. The page treated a finished apply as still running, because it judged "finished" by the wording of the outcome instead of by its end time (HIVE-108). [server]
- The approval ledger no longer says a person-approved action bypassed approval. An execution an operator confirmed now gets its own status (HIVE-1259). This corrects confirmations made after you upgrade. Rows already written keep what they said, because rewriting audit rows would hide the history too. [server]
- The operator ledger view recognises approval-path outcomes instead of rendering them as unrecognised and awaiting an operator (HIVE-609). [server]
- The dashboard assistant keeps a confirmation card that arrives before its conversation is known, instead of dropping it (HIVE-1258). [server]
- A one-shot agent can report its own completion without a separate claim step (HIVE-1278). [server]
- A duplicate machine row is retired, not deleted, and a standing grant moves to the surviving row by being re-issued (HIVE-1261). This part arrives by updating. The machine-identity part shipped in 0.1.86 in the runner image and needs a runner recreate (see Before you upgrade). [server]
- 26 of 27 seeded settings keys had no reader. They are now wired, removed or renamed (HIVE-1250); the effects you will notice are listed under Before you upgrade. [server]
- A section header on a bot's detail page wore the indigo accent that means "interactive". It is now the neutral heading style, like every other section header (HIVE-1310). [server]
- The LLM retry settings are documented honestly (HIVE-1274). The
HIVEMIND_LLM_RETRY_*variables were documented as settable, but the shippeddocker-compose.ymlnever passed them to the server. This part does not arrive by updating. On an existing install, add the variables to your compose override and recreate the server. [compose]
The dashboard no longer says "the server could not be read" while your conversation list is still loading [server]¶
If you left Previous conversations open on the dashboard, the Conversation header could read "local history only — the server could not be read" over a conversation list that had loaded perfectly well. On load, the page restores your most recent conversation. That restore could run while the panel's own list read was still in flight, treat "still loading" as "failed", and nothing corrected the header when the list arrived.
A change made earlier in this release cycle made this worse, and we are saying so. While fixing where the dashboard reads its conversation history from, we removed a network request that had been delaying that restore. Before the change, the wrong header depended on which response happened to land first. After it, the header was wrong on every load with the panel open. That worse form was never in a release: it was introduced and fixed inside 0.1.87's own development. The timing-dependent version was released, and it is what the contributor hit on 0.1.85. It was still ours, and his report is what caught both.
The fix is an external contributor's pull request, taken as written. The restore now waits for a list read that is already in flight, up to 15 seconds, before judging it. A read that genuinely does not answer in that time is still reported as one that could not be read.
Delivery: a template change. It reaches an existing install on a managed update, and no configuration is needed.
A slow conversation list no longer leaves the header saying the server could not be read [server]¶
If your conversation list took more than 15 seconds to load, the dashboard header settled on "local history only — the server could not be read" and stayed wrong after the list arrived. The header now says nothing about a read that has not answered, and a list that arrives late corrects it and rejoins your conversation. A read that genuinely fails still says so.
The dashboard assistant no longer shows an ended conversation as current [server]¶
No released version of Hivemind ever behaved this way, and no install was ever exposed to it. The gap described here was introduced and fixed inside 0.1.87's own development, between releases. It is written down because the fix is real and worth knowing about, not because you were affected.
What it was: while removing a dead legacy read from the dashboard, a case was left where, if your account had no live conversation, the panel kept the transcript cached in your browser, opened the fold on it, and styled the header as though the server had confirmed it — even though that conversation may have ended or been deleted.
An answer of "there are no conversations" now clears both the panel and the browser's copy. A read that FAILED still keeps your local copy and says so in the header, which is the trade this must never make (HIVE-1258).
What remains: for the moment between opening the page and the server's answer arriving, the cached transcript is still what you see.
Known and not fixed¶
- A server update still leaves the runner on its old version (HIVE-1263). 0.1.86 made this visible; it does not prevent it. Recreate the runner after updating.
report_result404s during agent completion (HIVE-1255). The defect fixed in HIVE-1278 is a different one. The agent's own "results have been reported" message is written by the model, not checked by Hivemind.GET /api/admin/agents/404s on the AI Agents page (HIVE-1264) has not been reproduced. The other endpoint in that report,POST /api/content/feeds/scrape, was fixed in 0.1.85.- Cost and tokens are not recorded per agent. See the entry above on why.
- The Conversation history symptom in HIVE-1258 is not fixed. What that report called "every message is a new conversation" could not be reproduced as a defect in our code, so it was deliberately left alone rather than guessed at. What this release does fix from that report is the dropped confirmation card and a false "the server could not be read" banner, and it introduces the regression above.
- Your machines list cannot yet tell you that a runner is up to date (HIVE-1306). The version-skew check added in 0.1.86 reads a version that no runner reports, so every machine reads version unreported and asks for attention, including one you just recreated on the current image. Treat it as "not known", not as "behind". The fix has to ship inside the runner image.
Your server's own secrets are now redacted even when they have no recognizable shape — one half reaches the install you have, the other reaches new installs only [server] [installer]¶
install.sh generates the admin API key, the runner key and every other secret
it writes as bare hex, with no prefix. The server's secret scrubbers recognized a
secret by its shape: an hm- prefix, a Bearer header, a key= assignment.
Bare hex has no shape a rule can use without also redacting every sha256 digest
in every log. So the most privileged key on your install passed through
redaction whenever it appeared on its own, in a log line or in text the server
scrubs.
The fix has two halves, and they reach different installs:
- Value-based redaction, in this release. It reaches the install you already
have, when you update. The server now removes the exact secret values it
holds from every line its logger writes and from the text it scrubs: ingested
estate events, assistant error excerpts and responses, and feedback sent to us.
That covers the admin API key, the runner API key, the updater token, the
integration encryption key and the app database password. It also covers the
main database password, the JWT signing key and the cookie secret when the
server loads them from a
.envfile itself (a direct binary run rather than compose). At startup the server logs which of these it is redacting, by name, never by value. A value shorter than 24 characters is not redacted, because it would match ordinary text, and startup warns you by name when that happens. - Prefixed keys at install, also in this release. It reaches NEW INSTALLS
ONLY. A fresh install now mints the admin and runner keys with an
hm-prefix, so a leaked copy is recognizable anywhere, including places the server never holds the value. Updating does not do this for you, and it cannot:install.shand.envare written once and never rewritten, so an install that exists today keeps its bare keys until they are rotated. Rotating a key (above) also replaces it with a prefixed one. Until then, the value-based half is what protects your install.
What value-based redaction does not cover: the database password on a compose
install, which reaches the server only inside its connection string; and secrets
you configure yourself, such as HIVEMIND_METRICS_TOKEN and the
HIVEMIND_INGEST_KEY_* values.
Delivery: the redaction half is the server image and reaches an existing
install on a managed update, with nothing to do beyond updating. The prefixed-key
half is install.sh and reaches fresh installs only. No compose or migration
change.
0.1.86¶
Before you read the rest: everything in this release reaches you by updating, and that was not true of the last two¶
Nothing to pin, nothing to recreate, nothing to copy out of a manifest. Every behavioural fix below lives in the server image and arrives the moment you update. No fix in this release requires you to move the runner.
That sentence is worth more than usual this cycle, because we have not been able
to say it cleanly for two releases, and at least one self-hoster was bitten by the
difference. Each entry is still marked with where it lives — [server] arrives
on update, [updater] advances your updater pin on apply but leaves the
container recreate to you, [runner] needs you to repin — and this release has
no [runner] entry that changes behaviour.
To be precise rather than reassuring, because this is checkable and you may well
check it: nine commits in this release touch crates that are compiled into the
runner image. Seven are text scrubs — a prompt string and comment text. The
other two add code to hivemind-core, which is a shared library linked into the
server and the runner: the new version-skew logic described below, and a test.
That code is consumed by the server and is dormant in the runner. So the runner binary does change, and its behaviour does not. We would rather tell you that than claim nothing moved and have you find nine commits that did.
If you are arriving from 0.1.83 or earlier, read this part¶
0.1.84 was an all-runner release. If you upgraded straight from 0.1.83 to
0.1.85, a managed apply moved your server and jumped clean over 0.1.84's fixes,
which live in the runner image and are still not applied on your install. 0.1.85's
notes said [server], nothing to do — true of 0.1.85, and false of an install
that skipped 0.1.84.
These notes are written per release, and the obligation is cumulative. That is a gap in how we communicate releases, not in the releases themselves, and we are fixing it. Until we do: if you have not repinned your runner since 0.1.83, you still owe 0.1.84's runner entries, regardless of what this page says about 0.1.86.
You can check what your runner is actually running — see the new skew warning below, which exists precisely because that was previously invisible.
Your machines list now tells you when a runner is behind the server [server]¶
A runner reports its version on no call today — registration does not carry one, the heartbeat has no body at all, and there is no version column. So there was nothing to compare, and a runner three releases behind looked identical to a current one.
GET /api/v1/machines now reports a version-skew verdict alongside each machine,
with the server's own version travelling beside it so you never see a verdict
without the other half of the comparison.
A machine that reports no version is treated as behind, not as unknown. A daemon that reports nothing was necessarily built before the release that started reporting, so it is necessarily older than the server judging it. That means the warning is useful the day you install this, for every runner you already have, rather than only once everything has been upgraded anyway.
It is reported beside your readiness verdict and never folded into it. A
runner can be perfectly ready and three releases behind — that is exactly the
state one self-hoster spent a day in — and a single combined verdict would have
rendered that install green.
An apply that leaves your runner needing a recreate now says so [updater]¶
The updater already told you when it had a pin waiting on a container recreate. It did not do the same for the runner, so a pin could advance while the container kept running the old image and nothing said a word.
The apply event log now carries a distinct pending-recreate event for the runner,
emitted only when the pin actually moved — a pending action and a clean apply no
longer render the same way. The warning names the compose profile, because the
runner and its socket proxy sit behind one and are invisible to a plain
docker compose command without it.
Per-role model settings: the control existed and you could not find it [server]¶
If you set llm.default_model and found your Auditor agents still running an
expensive model, you were not doing it wrong. That key is consulted only for the
Custom role; it does nothing for any other role, by design.
The keys that do work are llm.model.<role> and llm.thinking.<role> — for
example llm.model.auditor. They have always been read; they were simply never
seeded, never shown on the settings page, and never documented, so they only
worked if you already knew the exact key name.
You can set llm.model.auditor today, on the version you are already running,
and the Auditor's model changes on the next spawn. No upgrade required. This
release adds the test proving it end to end, and pins the surprising half — that
llm.default_model correctly does nothing for that role — so it cannot quietly
change into something accidental.
Making these keys visible on the settings page is the next step and is not in this release.
Runner heartbeats no longer bury your authorization records [server]¶
Every runner heartbeat was writing a row into the authorization audit chain — roughly 2,900 per day per runner. Real authorization decisions and refusals were a handful of rows among thousands of routine allows, in the one table you would read to reconstruct who authorised what.
Liveness telemetry carries no authorization decision and no longer records one. No rows were deleted and no retention policy was added — the chain is hash-linked, and pruning it is a different question with different consequences. Your existing history is untouched; it simply stops growing at that rate.
Prompt and admin-surface corrections [server]¶
The assistant persona no longer instructs agents to call a tool that the runtime may withhold from them, which surfaced as an agent that could not proceed rather than as anything diagnosable.
The Memory enabled toggle has been removed from the agent admin surface, label included. It was not connected to anything: conversation history was retained and recalled regardless of its setting, so an operator reading "Memory enabled: off" and concluding prior turns were not retained was wrong. Removing a control that does nothing is better than leaving a claim that is false. Whether conversation memory should be operator-controllable is a real question and is being answered separately.
Known and not fixed in this release¶
Stated plainly so you do not have to infer it from silence:
- Agent cost, tokens and model are still not recorded. Root cause found. The fix lives in the runner image, which means it would be published and not applied — we are not shipping it as a server release and calling it fixed.
- Recreating a runner container still registers a new machine. The old row remains, and standing grants reference machine identity. Both halves of the fix are written and in review; neither is in this release.
- The Facet dashboard conversation panel does not read its history correctly. Diagnosed; not fixed here.
- Settings keys that save successfully but are read by nothing. A check that fails the build while any remain is written but deliberately not merged yet, because merging it turns the build red before the keys are triaged.
0.1.85¶
Before you read the rest: no runner or updater pin this cycle — but two migrations you should understand before you apply it¶
The good part first: nothing to move. Every fix in this release lives in the
server image or in docker-compose.yml itself — none of it lives in the runner or
updater image, so there is no digest to copy out of a manifest this time. Each
entry below is marked with where it lives: [server] arrives the moment you
update, same as always. [compose] is new this release, for the one entry
where "where it lives" and "how it reaches you" are not the same question — its
own entry explains exactly what that means and what to do about it.
The part that needs reading regardless. This release carries two database migrations, and migrations are not something a version number can undo. Applying this release runs both automatically, at the server's next boot, the same as every migration always has — there is nothing you need to do. But if you ever need to roll back, know this first: reverting to an earlier server image does not reverse either one. Postgres does not un-apply a migration because an older binary started talking to it again. A true rollback of what these two changed means restoring your database from a backup taken before you upgraded, not just downgrading the image.
What each one actually does, because "a migration" covers very different levels of risk and neither of these is the risky kind:
orch_trigger_no_escalation_liesadds a rule that stops a specific contradiction — a ledger row claiming both "this was refused for a security reason" and "this ran with no refusal at all" — from ever being written again. It is addedNOT VALID, which is a deliberate choice: Postgres enforces it on every write from this point forward and does not scan or touch a single row that already exists. If your ledger already has a row carrying this contradiction, this migration leaves it byte-for-byte as it was written. Your audit history is not corrected by this release — it cannot be corrected without making your own audit trail lie about what it looked like on a given day — it is only prevented from happening again. See the entry below for what caused it.grandfather_unambiguous_machine_claimsis a one-time backfill, not a constraint. If you already had a runner registered before this release, it quietly recognizes that registration under a newer ownership model introduced a few releases ago, so upgrading does not silently strand a runner that has been working the whole time. It only acts when there is exactly one runner on your install to recognize — see the entry below for why that matters and what it does if you have ever had more than one.
A ledger row could claim both "refused" and "ran anyway" at the same time [server]¶
Every attempt Hivemind orchestrates is recorded twice — once when it arrives, once
when it's decided — and the two writes are supposed to agree by the time you read
the row. They could disagree. The write on arrival stamped a placeholder reason
(authz_failure, meaning "not decided yet") into a column meant to be corrected
once the real outcome was known; the write on completion corrected two other
columns and never touched that one. So a request that was in fact allowed
outright, with no refusal and nothing escalated to a human, could sit in your
ledger forever reading as if it had been refused for a security reason and run
anyway — a row that describes something that structurally cannot happen, because
it did not happen.
If you have already looked at your orchestration ledger and seen a row like that, it was wrong, and it was never a security incident. Nothing was refused and then executed regardless. The row was mis-labeled at the moment it was written; the request behind it went through the same authorization it would have gone through in a correctly-labeled world. The database now refuses to accept the same contradiction again — see the migration note above for exactly what that does and does not touch on your existing data.
A correct refusal could hand a stranger the two names to try next [server]¶
The assistant's job when it refuses a credential request it should refuse is to say no and point at who can help. It was doing exactly that — correctly refusing — and then, in the same breath, naming the specific people who hold that credential, pulled straight out of company knowledge that had been retrieved to help answer the question. The refusal was right. Naming names was not: someone who is legitimately allowed to ask already knows who to ask. Naming someone only informs a requester who does not legitimately know — turning a clean refusal into a list of who to target next.
The assistant is now told, everywhere retrieved context might carry a real name — your knowledge base, your shared vault, the conversation itself — to point at a role instead ("an administrator," "whoever manages database access") rather than a person, specifically at the moment it is refusing something. This does not change when it refuses or what it offers as a next step; it changes who it is allowed to name while doing so.
Every install's updater status check has 401'd, always, since the day it was written [server]¶
GET /v1/applies — the endpoint the server calls to show you a history of update
attempts — has never worked, on any install, since it was added. Not a
configuration problem on your end, not a token that needed rotating: the request
signer HMACs the request's raw path, including its query string
(/v1/applies?limit=25); the sidecar's verifier — correctly, per its own
documented contract — signs only the path itself, with the query string excluded
entirely. The two sides were never computing the same string, for this one
endpoint, on every single install, from the day it shipped. If you ever chased
this as a credential or provisioning issue, it could not have been: the mismatch
was in what got signed, not in what the signature was signed with.
Every other updater call (apply, apply_status, list_snapshots) sends a bare
path with no query string and was never affected — this is scoped to the one
endpoint that takes a query parameter. Fixed on the signer's side, verified
against the sidecar's real verifier rather than a second copy of the same logic
standing in for it (the kind of test that would have shipped this bug the first
time and called it covered). No sidecar change, no repin — the fix lives entirely
in the server's own client code.
A safety flag with no boot line, and a runner status that named the wrong problem [server]¶
Two separate findings, both about a control telling you less than it knows.
HIVEMIND_UNIFIED_GATE_ENABLED really did do something — just not on the path
being watched. Confirming a proposal the assistant made is a distinct action
path from the ones this gate covers, and it has never consulted this gate,
flag on or off — so watching that one specific path and flipping the flag was
never going to show a difference, correctly. What this gate currently covers:
a Spawn/Nudge/Kill/Reset verb from the Commander control surface, and an MCP tool
dispatch from the workflow harness. It does not yet cover a human confirming a
proposal the model made. Nothing we shipped said that before now — an operator
had no way to learn it except reading source, which is the deeper problem this
release actually fixes: the flag now reports its posture — enabled, disabled, or
set to a value we don't recognize — once at boot, the same as every other
flag like it, so you can at least confirm the server saw what you set. If you're
evaluating this gate, two things worth knowing before you flip it in anything but
a dev environment: it does not yet route a "needs approval" outcome to the
approval/escalation flow on the Commander control surface (it hard-refuses
instead), and an install with no per-target rules configured will start refusing
every Spawn/Nudge/Kill/Reset the moment the flag goes on, not once you get around
to configuring rules. Both are documented in-place now; neither is fixed by this
release.
A runner reporting attached_but_not_taking_work was telling the truth about
the symptom and the wrong story about the cause. No fresh heartbeat really was
arriving from that runner — that part was accurate. But the reason had nothing to
do with the authorization bug this status used to be attributed to (that one is
genuinely fixed, a few releases back). The real, current cause: a security change
a few releases ago permanently refuses a runner's registration if it was already
registered under the previous ownership model and nobody has since recognized
it under the new one — by design, so that a name someone else's runner already
holds can never be silently taken over. The runner kept working the whole time —
the connection that receives and completes work is separate from the one this
status watches — it just could never report so again. See the migration note
above for the fix; the status label itself was not changed, because it was not
wrong.
Operator alerts over Chatalot could never reach anyone, on any install [compose]¶
If you set up a Chatalot bot to receive Hivemind's alerts — an escalation, a
delivery finding, anything the orchestrator raised for a human to decide — the
server has always known how to send it. HIVEMIND_COMMAND_BOT_AGENT_ID,
HIVEMIND_COMMAND_CHANNEL_ID, HIVEMIND_COMMAND_OPERATORS, and
HIVEMIND_PUSH_MIN_SEVERITY were simply never declared in the shipped
docker-compose.yml. No value you put in .env could reach the container,
regardless of how correctly you set it — this could not have been turned on by
any customer, ever, on any install, no matter what was tried.
This is neither [server] nor [installer], and it will not reach an existing
install by upgrading. The four variables are declared in the docker-compose.yml
this repository ships, but an existing install's own copy of that file was written
once, at setup, and nothing about updating the server rewrites it — the same
reason the runner and updater pins in earlier releases needed a manual step.
Re-running scripts/install.sh does not rewrite it either. What does: re-running
the invite bootstrap (see Upgrading), or adding the four lines to
the server service's environment: list yourself:
- HIVEMIND_COMMAND_BOT_AGENT_ID
- HIVEMIND_COMMAND_CHANNEL_ID
- HIVEMIND_COMMAND_OPERATORS
- HIVEMIND_PUSH_MIN_SEVERITY
then recreating the server service. Full setup, including how to tell whether
your install is affected, is in
Feature flags — Operator alerts.
Also in this release, worth knowing even though it changes nothing for you¶
The CI gate that clears every branch before it merges used to list the
scripts/test-*.sh shell falsifier suite as something it covered without ever
actually running it — its own words, on its own page, were "claims until
something runs them." It now runs every one of them on every gate pass. Running
them for the first time found two pre-existing gaps we did not know we had —
a runner-credential test with real failing assertions, and a deploy-plan check
whose last assertion compares live prod against a version fixture that has since
gone stale — both tracked as their own tickets rather than fixed here, so this
release does not silently claim they're resolved. This changes nothing you run;
it changes what we catch before the next thing you'd have found for us.
0.1.84¶
Before you read the rest: this update still needs both pins moved by hand — and one of them always will¶
Same situation as 0.1.83, for the reason stated there: some of what changed lives in
the agent runner image, and applying this update does not move your runner or your
updater — your .env (or, from this release, your compose override) pins each by
digest, and nothing rewrites that file just because the server updated.
What changes from here is more specific than "the next release fixes this." The updater in this release can now keep the runner's pin current on every future apply, on its own — but the updater that performs this update is the one you already have installed, which cannot do that yet. So this update still needs the same manual step 0.1.83 asked for, once more, for the same reason: an update cannot deliver code that changes how updating works.
Take both digests from this release's signed manifest at
https://updates.seglamater.app/hivemind/releases/0.1.84/manifest.json :
in your .env |
take it from |
|---|---|
HIVEMIND_RUNNER_IMAGE |
registry.runner_ref_with_digest |
HIVEMIND_UPDATER_IMAGE |
registry.updater_ref_with_digest |
Then pull and recreate each one. Both services sit behind a compose profile, so name the profile on every command:
docker compose --profile runner pull hivemind-runner
docker compose --profile runner up -d hivemind-runner
docker compose --profile updater pull hivemind-updater
docker compose --profile updater up -d hivemind-updater
Pulling by digest is content-addressed, so each pull verifies itself.
What "the last time" actually means now, stated precisely rather than promised in general. From this point on, the two pins behave differently, and it matters which one you're asking about:
- The runner pin can become fully hands-off. Once the updater running on your
install is this release or later, every future apply keeps
HIVEMIND_RUNNER_IMAGEcurrent with whatever that release publishes — you will not need to open a manifest and copy a digest again. You will still choose when to rundocker compose --profile runner pull && up -dto actually pick it up; that command does not run itself. - The updater's own pin does not get the same treatment, and this is permanent by design, not a gap awaiting a future fix. The updater can now advance its own pin too — but the update mechanism deliberately never recreates the updater's own container as part of an apply. A sidecar swapping itself out mid-update is a failure mode this release chose not to introduce. So recreating the updater — the second command above — stays a step you run yourself, every time, from here forward. What's different is that you will no longer have to notice it on your own: an apply that advances the updater's pin now records an explicit event (visible on the apply's event history) naming exactly that action.
Each entry below is marked with where it lives, because that decides how it reaches
you: [server] arrives when you update; [runner] and [updater] each need
their own repin above; [installer] lives in scripts/install.sh, which is
published independently of every release and reaches you the moment it is
republished — not by upgrading your containers, and not by anything you need to do
beyond having a current copy of the script the next time you use it. This release
happens to carry no [server]-only change at all; every fix below lives in the runner,
the updater, or the installer.
A dispatched agent could not use a single tool, including the one that reports it was blocked [runner]¶
This is the blocker behind this release. A change meant to remove a permissions list
that was never actually being enforced also removed the list that grants a
read-only agent anything at all. The two lists look similar and are not the same
thing: one restricts what a command can do, the other says what an agent is allowed
to try in the first place. Removing the second while believing you were only removing
the first left every dispatched read-only agent with nothing granted — not Read,
not Grep, not even the tool it uses to report its own findings.
The practical effect: an agent dispatched for a routine, read-only task could not run a single tool call, and could not tell anyone why, because reporting a blocker uses the same tool that was itself blocked. From outside, a blocked agent and an agent that quietly found nothing looked identical.
The grant list is restored, re-derived from what the agent's own command sandbox already permits rather than copied from history — the old list predated three commands the sandbox has permitted for some time, and restoring it unchanged would have started denying those. The two are now checked against each other directly, in both directions, so they cannot drift apart silently again.
Separately, and worth keeping regardless of the cause: an agent that cannot proceed is now told to say so, explicitly, the same way it would report a finding. This is not specific to the defect above — it is the general case: a blocked agent and a successful one that found nothing used to be indistinguishable from the outside, and now the blocked case names itself.
A fresh install could fail in four different ways, and each one only shows up on a machine that has never had Hivemind before [installer]¶
Every Hivemind instance we had access to for testing was an existing install with a git checkout already on disk. That population could not have found any of these, because none of the four defects below exist on an existing install — only on the first install a machine ever gets. If you are reading this as an upgrade, none of the next three items ever affected your install. The fourth might have, and is worth checking regardless of how you first installed.
- A fresh install could not complete at all.
docker-compose.ymlis written once, byinstall.sh, at install time — and on a machine that never had the repo, nothing ever fetched one. The install ran through every step, wrote.envand every secret, and then failed at the very last step withno configuration file provided: not found. It now fetches the compose file from the same release origin it already uses for the signed manifest, and only when one is not already present, so an existing install's compose file — yours, if you've edited it — is never touched. --install-dirwas accepted and silently ignored. The installer always operates on the directory it is sitting in, not the one you name — and if the script happened to be run from somewhere like/tmp, it would resolve that "directory it is sitting in" to/and proceed. It now refuses outright when the resolved directory is implausible, and shows both the path you asked for and the one it would actually use, side by side, so the mismatch is visible before anything is written rather than found on it.- An install with no bundle and no source silently configured a build that cannot
work. Without
--bundle, the installer used to print several green confirmations and configure a build from a localDockerfile.server— which does not exist on a customer machine, since customer installs only ever receive a bundle or an invite, never a source checkout. It now refuses at the second step, naming--bundleor your invite, rather than failing at the last one with a bare build error. (A real source checkout — ours, for development — is unaffected; that path still builds.) - Secret files could be provisioned so their own consumer could not read them. This is the one that could reach an existing install, so check it regardless of how long you've been running Hivemind. On a non-root install, the shared token the server and updater use to authenticate to each other could land owned in a way that satisfied one of the two and silently locked out the other — the updater could not read its own token, restarted continuously, and the server came up healthy the whole time, so the install looked successful. The same install path could also leave your database password and registry credentials world-readable on disk. Both are now checked against every principal that actually needs to read them, and the installer refuses — naming exactly which secret and which account — rather than writing something that looks provisioned and is not.
Check your own install, whether you set it up yesterday or a year ago:
db_password and registry_creds should read 600 (or 640, group-owned by the
updater) — never 644. If either shows 644, that credential has been readable by
every account on the host: treat it as a credential to rotate, not only a
permission to fix, since you cannot know who else on the host may have read it.
updater_token should read 640, owned by the server's account and grouped to the
updater's — if the updater has been restarting continuously, this is almost
certainly why. Re-running scripts/install.sh corrects all three of these on your
existing files without regenerating their values, so it is safe to run against a
live install.
A release's runner and updater images now get tracked, not just the server's [updater] [installer]¶
The mechanism that keeps your pinned image current after an update — the one the box above describes — only ever tracked one of the three images an install runs. The runner's pin, once written at install, had no maintainer after that: nothing kept it current as later releases shipped, which is the reason 0.1.83 needed a hand-edit at all and the reason this release still does, once, for the updater currently running on your install to hand off to one that can do better.
The tracking logic itself lives in the updater image, which is why it needs the
updater repin above before it does anything for you. The other half — where the pin
is stored — is an install.sh change and reaches you the moment the script is
republished, same as everything tagged [installer] below.
Two more things fall out of this that are each worth knowing on their own:
- We found a fourth image family with no maintainer at all: the agent provisioner. Nothing publishes a digest for it, nothing pins it, and an install falls back to a tag that exists in no registry. That is not fixed by this release — it is reported here so it is not mistaken for something this change already covers, and it is tracked as its own issue.
- The pin for the two images the updater used to hand-manage —
HIVEMIND_RUNNER_IMAGEandHIVEMIND_UPDATER_IMAGE— moves out of.envand into the compose override, which is the file a managed update can actually write. Nothing in your.envis deleted until the same value has been read back off the override it was just written into, so a failure partway through this one-time move leaves the pin in the old place rather than in neither.
The drift advisory that told you to fix the wrong file [updater] [installer]¶
A separate check — unrelated to the pin work above — watches for one specific old
defect: a compose file still declaring the server's optional settings in a form that
made an unset value look set. It fired on a genuinely fresh install using this
release's own, correct compose file, naming 44 variables and telling you to re-run
install.sh to rewrite docker-compose.yml.
That instruction could never have worked, on any install, and if you tried it and
saw nothing change, you were not doing anything wrong. A second run of install.sh
takes the upgrade path, and the upgrade path never touches the specific settings
the advisory was complaining about — so the advice was not merely wrong, it had no
mechanism that could ever satisfy it.
The real cause, once measured directly: only 11 of the 44 flagged variables were
actually affected, and the fault was in install.sh's own generated .env, not in
docker-compose.yml at all — 11 optional integrations (Plane, a knowledge-base
publish target, and similar) were being written as active, blank settings rather than
left out entirely, which is indistinguishable, from the check's point of view, from
the very compose defect it exists to catch. install.sh no longer writes them that
way on a new install — that half reaches you the moment the script is republished,
same as the fresh-install fixes above. The advisory's own message, which lives in the
updater image and needs the updater repin to reach you, now names .env first and
states the compose explanation only as a fallback, rather than asserting it as
settled fact. If you already have a .env carrying one of those 11 blank settings,
that advisory may still fire for you until you remove the line yourself — we do not
rewrite an existing .env on your behalf, on the same principle the secret-mode fix
above follows.
0.1.83¶
Before you read the rest: the runner half still does not reach you by upgrading¶
Same caveat as 0.1.82, and for the same reason. Some of the changes below live in the
agent runner image, and applying an update does not move your runner — your .env pins
it by digest, that pin is written once at install, and nothing rewrites it.
We are fixing that properly rather than repeating this note forever. The mechanism that makes a runner pin advance on a normal update is built and under review; it lands in a following release.
This release needs TWO pins moved by hand, and the second one is what ends this.
Both digests are in this release's signed manifest at
https://updates.seglamater.app/hivemind/releases/0.1.83/manifest.json
in your .env |
take it from |
|---|---|
HIVEMIND_RUNNER_IMAGE |
registry.runner_ref_with_digest |
HIVEMIND_UPDATER_IMAGE |
registry.updater_ref_with_digest |
Then pull and recreate each one. Both services sit behind a compose profile, so name the profile on every command. That is the form our installer and our compose file both use, and it is unambiguous across Compose versions:
docker compose --profile runner pull hivemind-runner
docker compose --profile runner up -d hivemind-runner
docker compose --profile updater pull hivemind-updater
docker compose --profile updater up -d hivemind-updater
Pulling by digest is content-addressed, so each pull verifies itself.
Why the updater one matters, and why it is the last. From the next release the updater moves your runner pin for you — but only a NEW updater can do that, and the container that performs an update is the one you already have installed. An update cannot deliver code that changes how updating works; that step has to be taken once, by hand. Take it now and this is the last time we will ask you to do this.
Each entry below is marked with where it lives, because that decides how it reaches you:
[server] arrives when you update; [runner] needs the step above; [compose]
needs a hand-edit to docker-compose.yml, because that file is written once at install
and no upgrade rewrites it. Each entry says which, and what to do about it.
RUST_LOG now controls the server's logging — it never did before [server]¶
If you have ever set RUST_LOG on the server service and seen no change, this
is why. The shipped docker-compose.yml has declared RUST_LOG on that service
since 0.1.78, but the server binary read a different variable —LOG_LEVEL—
which is not declared on that service. The log level was therefore fixed at
the built-in default and nothing you could set would change it.
The reason this was easy to miss is worth stating: setting RUST_LOG and then
seeing hivemind_server lines at the level you asked for looks like confirmation,
and it is not. Those lines appeared at that level anyway. Only a value whose
effect differs from the default — tower_http=debug, say — could have told the
two apart.
This one reaches you by upgrading. It is a change inside the server image, your compose already declares the variable, and nothing needs editing.
Precedence, highest first: the --log-level flag, then RUST_LOG, then
LOG_LEVEL, then info. LOG_LEVEL is legacy and is read only when RUST_LOG
is unset — which in a container never happens, because compose always supplies
RUST_LOG. If you have LOG_LEVEL set, the server now says at boot that it is
being ignored, rather than leaving you to wonder.
One thing to know before you set it. A filter that names only targets turns off every crate it does not name. So
gives you request logging and drops hivemind_server down to warnings and
errors. That is standard RUST_LOG behaviour, not a Hivemind quirk. If you want
your usual logs plus request tracing, include a global level:
To keep unmatched crates from going completely silent, the server appends a
warn floor when your filter names only targets — so a dependency's warnings and
errors still reach you. If you set a global level yourself, that is respected
exactly as written.
The switch is announced, not silent. Because compose always supplies
RUST_LOG, honouring it changes the effective filter on every install at once —
including yours, if you set the variable earlier and saw nothing happen. The
server therefore logs one line at startup naming the filter in force and where it
was read from. That line is emitted at warn on purpose: at info it would be
discarded by filters like RUST_LOG=tower_http=debug, which is to say it would
be invisible to precisely the people whose logging just changed.
A filter that fails to parse now says so and reports what it fell back to. It used to fall back silently, which looked identical to a filter that had worked.
tower_http stays at info by default: debug logs every request, which is the
wrong default for a running system.
The agent reaper can be turned on — it never could before [compose]¶
HIVEMIND_AGENT_REAPER_ENABLED has existed since the reaper merged, and no
install could ever set it. The server read the name, but docker-compose.yml
never declared it, and there is no env_file: directive — so a variable that is
not listed under the server service's environment: block is not passed into
the container at all. Setting it in .env succeeded at every visible step and
did nothing.
It is declared from this release, and still defaults to off.
This one does not arrive by upgrading. docker-compose.yml is written once,
when you install, and no upgrade rewrites it — so a file written before 0.1.83
still will not carry the line. Re-running scripts/install.sh does not rewrite
it either: that script regenerates .env, the override and the secrets, and
leaves compose as it was. To enable the reaper on an existing install, add
to the server service's environment: list yourself, then recreate that
service.
Before you enable the reaper, check your heartbeats [runner]¶
Leave it off unless last_heartbeat is being written. The reaper's only
input is heartbeat freshness, and an agent whose heartbeat has been missing
longer than the stale window is treated as a dead process and marked failed.
If your heartbeat writes are failing for some other reason, turning the reaper on
does not reveal that — it marks every agent failed shortly after it starts,
while the agent is still working. The reaper only updates the agent row; it never
stops a process, so this is a reporting error rather than lost work, but it is a
loud one.
The agent's MCP credential file is ignored, not merely untracked [runner]¶
When an agent starts, Hivemind writes a .mcp.json into its git worktree
containing an API key. That file was untracked but not ignored, so a blanket
git add -A in that repository could commit a live credential to your remote.
Hivemind now adds .mcp.json to the repository's .git/info/exclude before
writing it, so the file is ignored from the moment it exists. info/exclude is
local, untracked git state — your .gitignore is a tracked file and is left
alone.
This does NOT arrive by upgrading, and then only for worktrees created afterwards. The change is in the agent runner image, so it needs the repin step in the box above before a new worktree is written the new way.
Worktrees that already exist keep the file exactly as it is. If you have agent
worktrees from an earlier release, check whether .mcp.json was ever committed —
git log --all -S against the key value will tell you. If it was, treat it as a
credential to rotate rather than a file to delete.
An agent that finished without filing a report no longer reads as "nothing was recorded" [server]¶
This is the one a customer wrote to us about: an agent ran, finished, and the operator was told "nothing was recorded, so there is nothing for me to tell you" — while its output was sitting in a different table the whole time. The run was never lost. It was two tables away from the one this surface reads.
There are four outcomes now, and they read differently because they mean different things:
- the agent reported a result — it chose to file this, and it is what it filed
- content exists but the agent never confirmed filing it — what the process happened to be saying when it exited, not a report it stood behind
- no report was filed at all, but output was recovered from the execution log — shown, and labelled as recovered rather than as a report
- nothing was recorded — and this is now the only case that says so
The recovery path is the part worth knowing about: a run whose report failed to write is no longer a lost run, it is a retrievable one.
A 404 no longer logs at ERROR indistinguishably from a real fault [server]¶
A "not found" and a genuine database fault produced the same ERROR line, with no status in it, so the log could not tell you which had happened. The line now carries the status it resolved to.
If you have been reading our error logs and drawing conclusions from them, this is the release where those lines start meaning what they appear to mean — and it is worth re-reading any conclusion you drew from them before now.
A malformed updater URL disables updates instead of stopping the server [server]¶
A quoted or carriage-return-terminated HIVEMIND_UPDATER_API_URL used to boot fine
and then fail an update with builder error and nothing else — no status, no detail,
and the server was holding the exact string that failed to parse.
It is now validated at startup. A bad value disables the Updates page with a
specific reason rather than preventing the server from starting, because refusing
to boot would take down chat and agents over one wrong variable, and leave you no way
to reach the UI to fix it. The error prints the offending value so an invisible
character — a stray \r from a Windows editor — is visible rather than something you
have to guess at.
0.1.82¶
Before you read the rest: some of this does not reach you by upgrading¶
Most of what follows lives in the agent runner image, and applying an update does
not move your runner. Your .env pins the runner by digest, that pin is written
once at install, and nothing — not the Updates page, not re-running install.sh —
rewrites it. We found this because a customer measured their runner still on the
digest from four releases earlier.
That is our defect, not yours, and a supported path is being built. Until it lands, this is how to take the runner half of this release:
# in your .env, replace the HIVEMIND_RUNNER_IMAGE line with:
HIVEMIND_RUNNER_IMAGE=registry.seglamater.app/seglamater/hivemind-runner@sha256:5aa13fb94d1be0b88ac2d6cdbd07196500b7b2d244d5e982eacd848dd1ddfae7
docker compose --profile runner pull hivemind-runner && \
docker compose --profile runner up -d hivemind-runner
That digest is the 0.1.82 runner from this release's signed manifest. Pulling by digest is content-addressed, so the pull verifies itself.
Each entry below is marked [server] — arrives when you update — or [runner] — needs the step above.
Your read-only agents are no longer told to run a command they cannot run [runner]¶
Seven agent profiles were instructed to finish by running touch /tmp/hivemind-agent-done-…,
and the read-only profiles are denied exactly that by the agent sandbox. The denial
was correct and is unchanged; the instruction was the bug.
It was also pointless: reporting a result already writes that signal file, so an agent that reported had already signalled done, and an agent that had not reported had nothing to signal. Read-only profiles no longer mention it, and a test walks every profile so a new one cannot reintroduce it.
If you saw an agent end with a message like "the touch command is blocked by the
read-only sandbox", this is that. The hook's own line is
BLOCKED: read-only agent - command not permitted for a read-only agent.
A run that fails silently now says why [runner]¶
When an agent exited cleanly but produced no answer, the only account of what happened — anything it wrote to its error stream — was discarded, and the run was reported as "finished and printed nothing". That reads identically to a run that genuinely found nothing. The reason now survives into the record.
An agent run leaves a copy of its transcript on your machine [runner]¶
Agent transcripts lived only inside the runner container and died with it. They are now copied into your workspace, identified by session rather than by "most recent file", so a concurrent run cannot be picked up by mistake.
The copy never leaves your disk — that is enforced in code rather than intended, with a test that fails if it ever becomes possible.
The approval gate's CSRF protection is proven, not assumed [server]¶
The decide gate had a CSRF check whose firing was never demonstrated. It now has a test that fails if the protection stops working. No behaviour change if your install was healthy; this closes the gap between "the control exists" and "the control fires".
A fourth connection state: attached, authenticated, and not permitted [server]¶
Connections that were authenticated but not authorised were reported the same way as connections that had failed to authenticate. Those are different problems with different fixes, and collapsing them sent operators looking at credentials when the issue was permissions. They are now distinct.
Dependency and packaging¶
quick-xml 0.37 → 0.41 [server], with the two API breaks the bump hides handled
explicitly rather than papered over. Two Spawn environment variables that were
documented but never reached the container now do [server].
Upgrading¶
The server half is a normal update — see Upgrade. The runner half needs the digest step in the box at the top of this section.
If you run the agent runner and skip that step, your server will be on 0.1.82 while your agents keep the behaviour of whatever release you installed from. There is no warning today that tells you so; making that visible is the next thing we are building, at the suggestion of the customer who found it.
0.1.81¶
0.1.81 adds 1 database migration.
Your agent runner can register itself¶
If you enabled the agent runner and it never came up — the log line is
Worker role max is Steer, denied Spawn
— this is the release that fixes it. The runner's own startup registers the
machine it runs on, and that call was refused: the runner holds a runner
service credential, and machine registration required a full commander. So the
machine list stayed empty, the heartbeat loop that starts only after a
successful registration never started, and every dispatch reported that no
machine had heartbeated. Nothing you could set in .env would have changed it.
The runner now registers its own machine, and only that one. The claim is taken on first use and is never a takeover: a machine row your install already has, created before this release, stays unclaimable — "unclaimed means free" would permit exactly the hijack the old restriction existed to prevent. If you have a stale machine row for a runner host, remove it and let the runner re-register.
Everything else on the machines API — editing a machine, enrollment, decommission — stays commander-only, unchanged.
Your runner can now claim and drive agents, not just register¶
If your runner has ever looked healthy and still never started an agent, this is the other half. Registration was never sufficient on its own, so this one matters even though the entry above says registration is fixed.
The work that let the runner register widened the machine routes — register, heartbeat, restart acknowledgement. It did not touch the agent routes. The runner holds a runner-scoped service credential rather than a full commander's, and every write that starting an agent depends on was still gated on being a commander. So a runner that got as far as a registered, heartbeating machine — by whatever route it got there — met a refusal at the first spawn.
That refusal was close to invisible, which is why it is worth describing rather
than just fixing. It landed at the point where the runner claims the agent, and
it was fatal rather than something the runner recovered from: POST
/api/v1/spawn still returned 201, the machine still reported healthy, nothing
ran, and the only trace was a warning written inside the runner container.
Four surfaces now accept the runner's own credential — reporting machine readiness, claiming an agent, reporting an agent's lifecycle, and settling message delivery — and each one is bound to the machine the runner actually owns. Owning one machine does not license naming another: a runner asking to act on a machine it does not own is refused, and a runner whose machine cannot be resolved is refused rather than allowed to proceed unbound. What a runner may change on an agent is a fixed list of lifecycle fields, so the widening does not also hand it runtime limits or cost ceilings, and the timestamps it reports are held close to the present — a backdated one would otherwise steer the cleanup sweep into deleting the very records it just produced.
Two limits, stated rather than left for you to find:
- An unregistered runner can no longer claim an agent. A claim with no machine attached used to succeed unbound, which would have let any runner claim any pending agent in any project. That path is closed, so a runner that has not registered now gets a refusal instead of a silent success.
- Agent credential rotation and revocation are deliberately not converted.
Both sit behind
HIVEMIND_PER_AGENT_IDENTITY, which is off in the shipped configuration, so neither runs on a default install. Turning that flag on is not supported yet — a named limit, not an oversight.
One trap, now written up in Troubleshooting: never point
HIVEMIND_RUNNER_API_KEY at your admin key. It appears to work, and that is
the problem. Registering under an admin key creates the machine row without
the claim that normally accompanies it, and an unclaimed row is then permanently
refused to the correct credential — deliberately, because an unclaimed row means
"somebody else's", never "free to take". Switching the value back afterwards
does not recover it. It is a one-way door, not a shortcut.
The admin credential is no longer readable from the agent runner¶
This is the most important entry in this release, and half of it corrects something 0.1.80 only got part of the way.
0.1.80 moved ADMIN_API_KEY out of the server container's environment and
into a mounted file. That was correct, and it applied to a fresh install
only. The 0.1.80 entry below describes what the defect was; what matters here
is that if you installed before 0.1.80 and upgraded into it, your server
container still carried the admin key as a plain environment value — and it
booted cleanly while doing so, because the code deliberately still falls back to
the environment.
Two changes close that.
The managed updater migrates the credential on an existing install. On the
next managed update the value is moved out of the container environment and into
/run/secrets/admin_api_key. It is moved, never dropped — an install with no
active admin row depends on that value existing. If the updater cannot place the
file, it leaves the credential where it is and warns, naming the remediation: an
unremediated install is bad, an unadministrable one is worse, and only one of
those is something you can fix.
The runner's Docker proxy no longer grants a container-read surface. Moving
the credential to a file was not sufficient on its own. The filtered Docker
socket the runner uses was configured with CONTAINERS=1, and that single
permission covers more than the docker inspect it was reasoned about
through — the same permission serves the endpoint that returns the contents of
any file in any container on the host. So the credential had moved from one
readable location to another readable location, and the server-side check that
gates runner registration on the key being absent from the environment was
satisfied while the key stayed fully readable.
CONTAINERS is now 0 on the runner's socket proxy. The permission is removed
rather than the individual endpoints enumerated, because the endpoints nobody
enumerated are how this happened in the first place.
The scope of that, stated precisely, because it is narrower than the fix
sounds. This is about the runner. Two other socket proxies ship in the same
compose file — the managed updater's and the provisioner's — and both still hold
CONTAINERS: "1" and POST: "1", because both need container lifecycle
authority to do their work. What contains those two is not a narrower grant but
the network: each sits alone with its own client on an internal network the
runner never joins, and each serves our code rather than agent code. So the
property this release establishes is that the credential is out of reach of
code you did not write — not that no container on your host can read a file in
another one.
What that costs you, stated plainly: agent observability, and nothing else.
An agent can no longer run docker ps, docker logs or docker inspect
against containers on the host. Dispatch is untouched — agents run as processes
inside the runner container, and no container is created at any step of it.
The runner's Docker proxy no longer streams host-wide events¶
Closing CONTAINERS on its own would have left open the door it was meant to
shut. The proxy image leaves EVENTS, PING and VERSION enabled unless you
name them, and EVENTS is not the harmless one it looks like: the Docker daemon
event stream reports container names, images, labels and lifecycle transitions
for every container on the host — and the command lines of exec_create and
exec_start, which on some hosts carry secrets passed as arguments. A runner
that could no longer call /containers/json could have gone on enumerating your
whole host by subscribing there instead.
EVENTS is now 0 on the runner's socket proxy, and closing it costs nothing
measurable: nothing in the runner image subscribes to the Docker event stream.
PING and VERSION stay on, deliberately. The isolation check in
Install depends on /_ping answering — a 200 from it is what
proves the archive probe's 403 was a refusal rather than an unreachable
proxy. Turning PING off would leave that check unable to tell those two apart,
which costs more than the disclosure it would close.
This one does not reach you by upgrading. The managed updater does not
rewrite docker-compose.yml, so on an existing installation EVENTS: "0" has
to arrive the same way CONTAINERS: "0" does — see the checklist below.
Assistant tool calls leave a durable record¶
Until this release a tool call made by the assistant left nothing behind. The tool-call table stayed empty on that path, so "which tools were called, in which conversation, by which principal" had no answer. It has one now.
Three properties, each a deliberate choice rather than an implementation detail:
- The owning account is resolved per call, from the conversation the call happened in. It is never supplied by the caller and never taken from the model's input.
- If the conversation cannot be resolved, nothing is written and the tool does not run. A trail that attributes a call to the wrong account is worse than one that is honestly incomplete, because the first is believed.
- The row joins the append-only, hash-chained ledger the audit integrity card reports on, and is scrubbed by the same writer the agent harness uses, so credentials appearing in tool input are not persisted. Rows in that table cannot be updated or deleted, so a secret written there could not have been removed by the application at all.
Still empty on this path, so that a partial trail is not read as a complete one: the agent audit log, knowledge-retrieval records, and estate retrieval audit. Those answer "what was retrieved", and they are not wired yet.
Your installation is checked against what the release requires¶
docker-compose.yml is written once, by install.sh, at install time. No
upgrade path rewrites it — the managed updater changes the image digest in the
override file and deliberately preserves the rest of your bytes. That is the
correct behavior, and it has a consequence that was never written down for you:
a fix that lands in docker-compose.yml or .env.example does not reach your
installation until you re-run install.sh.
That is precisely how the admin-key exposure described above survived two
releases on a real installation whose managed updates had every one reported
success. The updater could not report what it had never applied.
The release now carries a table of properties it requires of an installation, evaluated against your running container rather than against your compose file — the compose file is the means, the container is the fact. You get:
- a distinct apply outcome,
delivery_incomplete, which deliberately does not contain the word "success"; - the remediation carried on the apply record itself, not only in a log line;
GET /v1/delivery, which answers the same question without applying anything;- a periodic sweep, on by default, for the case where the updater is never
invoked at all —
docker compose up -d serverre-reads the same old compose file, re-applies the old configuration, and exits 0.
It reports; it never refuses. A gate keyed on "your compose file is current" would refuse every upgrade on every installation predating any compose change — which is all of them — and strand those instances on the older, more vulnerable image.
"I could not look" is counted separately from "I looked and it is missing". Those are different facts, and only one of them is about your installation.
Captured agent transcripts are redacted before they are stored¶
Agent auto-capture posted raw terminal scrollback into the message store, and the only scrubbing happened at the render layer — so the API, the database, its backups and any support bundle held the captured pane verbatim. Redaction now happens at the write boundary, and if a known credential survives it, nothing is persisted at all rather than a partially-scrubbed transcript.
Two passes were added that a pattern rule cannot do on its own: a literal sweep for the credential values the process actually holds — a forge token is 40 hex characters and so is a git commit sha, so no shape rule separates them, only the value does — and a sweep for a token split across a line break by the terminal capture itself.
Separately, the shared forge token is no longer placed in every agent's environment. Only the one capability that consumes it receives it.
This fixes what gets stored from now on. Transcripts already in your database are not rewritten. If you have been running agent capture, treat the rows you already have as potentially holding unredacted output.
Do you need to do anything?¶
Yes — more than upgrading, and this applies to every installation that predates this release.
- Update your
docker-compose.yml— and note thatinstall.shis not what does it. Your compose file has never been rewritten by an upgrade, so several releases' worth of fixes reach you only by changing that file: the admin-key secret mount, the runner bind mount, the environment-variable declarations from 0.1.76, and the flag-absence corrections from 0.1.80.
Re-running install.sh does not update docker-compose.yml. It never
has. It generates your secrets, merges your .env, edits your override file
and starts the stack — but it neither fetches nor rewrites the compose file.
It does update HIVEMIND_VERSION in your .env, so an installation that
re-runs it ends up reporting a newer version with an unchanged compose file.
If you have ever seen your server report one version while your compose file
plainly predates it, this is why.
Two ways to actually get the changes:
-
Apply them by hand to your existing
docker-compose.yml. This is the dependable path and it preserves everything else you have customized. The settings this release needs are in step 3 below, andinstall.mdcarries the current file to compare against. -
Re-run the bootstrap installer — the script you originally fetched from your invite link and saved as
hivemind-install.sh, not theinstall.shsitting in your project directory. That is the path that delivers a newdocker-compose.yml. It replaces that file rather than merging into it, so copy your currentdocker-compose.ymlsomewhere safe first and re-apply your local edits afterwards — including the two proxy settings in step 3, which are not yet in any released compose file. The bootstrap also needs a live invite URL; if yours has expired, ask support for a new one rather than working around it.
Your .env is preserved on both paths, and your override file is edited
surgically rather than regenerated — see 0.1.79, which is the release that
stopped truncating it.
- Confirm the admin key is no longer a value in the container environment:
docker inspect hivemind-server | grep ADMIN_API_KEY
You want ADMIN_API_KEY_FILE=/run/secrets/admin_api_key — a path. If you
still see ADMIN_API_KEY= followed by a hex string, the migration has not
run on your installation yet.
- If you run the agent runner, check its Docker proxy — two settings, not
one. On the
hivemind-runner-socket-proxyservice in yourdocker-compose.yml, both of these must be present:CONTAINERS: "0" EVENTS: "0"
Set them by hand. Neither value reaches an existing installation on its
own, and nothing you can run locally will place them for you: the managed
updater does not rewrite docker-compose.yml, and neither does install.sh
(see step 1). The only alternative to editing the file yourself is re-running
the bootstrap installer and then re-applying your local edits, as step 1
describes. install.md carries the probe and the refusal you should expect
from it; troubleshooting.md covers what to do if the probe succeeds
instead, including rotation. If the credential was readable on your install,
rotate it after remediating.
-
If you run the agent runner, confirm
HIVEMIND_RUNNER_API_KEYholds a runner key and not your admin key. Registering under an admin key creates a machine row that the correct credential is afterwards permanently refused on, and putting the right value back does not recover it.troubleshooting.mdcarries the query that identifies affected rows. -
Check delivery completeness with
GET /v1/deliveryafter upgrading. It tells you whether anything this release requires of your installation failed to arrive.
0.1.80¶
Upgrade: agent code in the runner could become your Hivemind administrator¶
If you run the agent runner, this is the reason to upgrade.
ADMIN_API_KEY — your instance's full administrative credential — was present
as a plain value in the server container's environment. On its own that is
a disclosure. What made it an escalation is the runner:
- agent code runs in the runner container, which this codebase's own comments call the least trustworthy component;
- the runner is given a filtered Docker socket so an agent can inspect its own container, and that filter works by API endpoint, not by container identity — it cannot express "only your own container";
- so agent code could read the
servercontainer's environment; - and the runner is allowed to reach the server over the network by design, because the runner is a Hivemind client.
Read the key, present it as X-API-Key, and the agent is an administrator.
This had previously been recorded as mitigated on the grounds that network isolation closed the path to the database. That reasoning does not cover it: the agent never needed the database. It used the admin key against the server it is deliberately allowed to reach.
The fix. The credential is sourced from a mounted file. The container
environment carries ADMIN_API_KEY_FILE — a path, in a mount namespace the
agent's container does not share. This is the same shape the updater already
used for the database password, so it is an existing pattern rather than a new
mechanism.
A second instance, which fixing only the server would have left open. The
legacy web service carried the same credential under a different name. It sits
behind an opt-in profile and on a different network — but docker inspect is
not scoped by network, so an agent could read that container's environment just
as easily. It was converted too. Opt-in is a precondition, not a control.
What this release does NOT fix, said plainly, because a partial fix presented as a complete one is worse than an acknowledged partial one. These remain readable in the server's container environment by the same mechanism: the database URL (which embeds the database password), the runner key, your Plane and knowledge-base API keys, your LLM API keys, the CSRF secret and the metrics token. They are disclosure, not escalation-to-admin, and their severity differs — the database password is bounded by the database being unreachable from the runner, while LLM and integration keys are exfiltratable wherever the agent has network egress. Extending the same file-secret treatment to them is tracked separately.
And the limit that turned out to matter most: everything above reached a
fresh install only. An installation made before this release kept the
credential in its container environment after upgrading, and booted cleanly
while doing it. That was not corrected until the release above — see its entry,
and run the docker inspect check there.
If you hand-edited ADMIN_API_KEY in your .env, quotes now matter¶
Before this release the key reached the server through Docker Compose's variable
interpolation, and Compose strips surrounding quotes when it reads .env.
So an operator who hand-wrote
ADMIN_API_KEY="abc123"
had a working install, and nothing ever told them the quotes were decorative.
Reading that same line to write a secret file would have passed the quotes
through as literal characters of the credential — turning a working instance
unadministrable at upgrade time, with a .env that still looked correct, at the
exact moment you would reach for that credential to investigate the upgrade.
One layer of matching quotes, single or double, is stripped. A quote inside a credential is deliberately left alone: it is a legal character, and stripping it would corrupt a key that works today.
install.sh cannot produce this itself — it writes an unquoted hex string. Only
a hand-edited .env reaches this path, which is exactly the file most worth
surviving, because nobody generated it and nobody validated it.
Not fixed here, and worth knowing if you hand-edited that file too:
DB_PASSWORD already has the same split shape. Postgres receives the file while
the server receives a database URL built by interpolation, so a quoted value
gives the two sides different values.
An unset flag is now genuinely absent, rather than present and empty¶
0.1.79 fixed the reading side of this. This release fixes the writing side, so the distinction exists before anything has to interpret it.
The compose file declared 44 of the server's environment variables in a form that renders an unset variable as an empty string. So "the operator configured nothing" arrived at the server as a value that exists and carries no information. Anything asking whether a flag had been configured got a wrong answer, and an existence check was satisfied by a value that meant nothing.
Measured by rendering the old and new files and diffing every key: 44 variables
go from "" to genuinely absent when unset, and zero other differences. The
25 entries that carry real defaults keep them, and other services' blocks are
untouched. Setting a value in .env works exactly as before.
Nothing becomes enabled by this. An empty value was never truthy and still is not. Only the reporting becomes honest.
.env.example also gains a "dark-by-default feature flags" section carrying
HIVEMIND_CONFIG_STORE_ENABLED commented out. That variable had zero
occurrences in the file while install.sh and the self-hosting docs both told
you to set it — so a customer following our own instructions could not succeed,
for a store whose routes return 404 without it. A commented key is genuinely
absent; an uncommented empty one is present-and-empty; and those are now
different states. Uncommenting it with an empty value is a third state again,
meaning "explicitly set to nothing" rather than "not configured".
The signature-verification command in our documentation never worked¶
If you tried to verify a release manifest and could not: it was our command, not your network.
docs/quick-start.md documented a cosign verify-blob invocation that fails
for every customer, not only for those on egress-restricted networks. We
sign without a transparency-log entry, so the default verification path looks
for a log entry that does not exist, finds zero, and fails. On a restricted
network it fails a second and different way first, by trying to reach a
third-party Sigstore CDN.
The documented command now passes the flag that skips the log lookup only,
and verifies against the public key install.sh already stored and pinned on
your own machine rather than fetching a fresh copy — which also removes the
second network dependency from the procedure. The curl form is kept only as a
marked pre-install fallback, now with the checksum comparison the old text
omitted.
Signature verification is not weakened by this, and that was proven rather
than asserted: with the wrong key the verify is refused, and with a tampered
payload the verify is refused. cosign prints an alarming warning about
skipping log verification on every success, so the documentation now
pre-empts it and tells you the line to look for is Verified OK.
The runner's 403 told you the wrong thing, with total confidence¶
If your runner was refused, the message said your HIVEMIND_RUNNER_API_KEY was
wrong, in six lines ending "This will NOT fix itself."
It printed that for any 401 or 403. The actual refusal was usually positional — the request came from a source the forward-auth trust rules did not accept — and the credential had never been evaluated at all. What made it expensive is that the sentences are true and were attached to the wrong cause: an operator edits a correct credential, restarts both services as instructed, sees the identical 403, and has no reason to doubt the message.
Eleven of the twelve refusal codes are decided before or without reading the credential, and all eleven printed the credential message. One of them is a transient database failure, so the old text was not merely wrong about the cause but backwards about the remedy.
The runner now prints what the server actually said, and blames the credential only on positive evidence that it was evaluated. An unparseable response makes no claim at all: a vague message costs you a search, a confident wrong one costs you a day.
Alongside it, a trust decision that had collapsed two questions into one is separated. Refusing a request that asserts an identity header from an untrusted source is the entire point of that gate; refusing one that asserts nothing was never necessary, and that is what was blocking the runner. This was deliberately not fixed by widening the trusted-network list, which would be the wrong repair — the runner executes agent code, and admitting it to the identity-header boundary would let agent code claim to be an administrator and be believed.
The runner had no volumes and could not start as configured¶
The shipped runner service declared no volumes: at all, while
HIVEMIND_REPO_PATH defaulted to /workspace — which nothing creates. Every
installation that enabled the runner profile met this first. The bind mount now
exists.
Host and container paths are mounted as the same string, and that is a correctness requirement rather than tidiness: a project row stores its repository path as a host path and the runner resolves it inside the container, so two spellings for one directory means whichever half resolves the other's is wrong.
Do you need to do anything?¶
Yes, and the important one was never communicated at the time. The admin-key
fix above applies to a fresh install. If you installed before this release and
upgraded into it, re-run install.sh — or upgrade to the release above, which
migrates the credential for you. Either way, confirm it:
docker inspect hivemind-server | grep ADMIN_API_KEY
If that still shows a hex value rather than a path ending
/run/secrets/admin_api_key, you are still exposed. Remediate first, then
rotate the key.
0.1.79¶
This release is almost entirely about the path a customer actually walks: installing on a clean machine, upgrading an existing instance, and getting the assistant to answer. A walk of the documented clean-VPS install found eight distinct defects, six of which reported success while failing. Those are the ones worth reading about, because a failure that announces itself costs you an afternoon and a failure that congratulates you costs you a week.
Upgrading no longer destroys your compose override file¶
This is a data-loss fix. If you ever edited
docker-compose.override.yml, read this one.
The installer regenerated that file with a truncating redirect on every
upgrade. It is the one file the installer's own output tells you to edit — for
your runner bind mount, external networks, or a CA bundle — and it was destroyed
each time, with no backup, while .env had had one all along. The file's own
header said "merge into it"; the writer did not follow its own advice.
Whole-file regeneration cannot be made safe here even with markers, because your
content interleaves with generated content under the same services: key. So
the file is now edited surgically: the image pin is updated in place,
declared networks are added, nothing is ever removed, and a .before-<ts>
backup is written.
A root install left the server unable to read its own secrets¶
If you installed as root, the permission fix-up named the wrong set of files. It
was written for the updater's consumers, so two secrets the server reads
were never touched at all and stayed 0600 root:root, and one was readable by
the updater but not by the server.
Permissions are now derived from the secrets directory rather than a hand-kept list, at the tightest mode that works for each consumer, with a warning — not a silent pass — for any secret whose consumer is not recognized.
And the failure was being logged as a success. The server caught every
error reading those files, permission-denied included, and logged it at INFO as
"not present … expected when the updater profile is inactive". The file was
present, and install.sh always activates that profile, so the reassuring
explanation was guaranteed to be false. Not-found, permission-denied and other
failures are now logged distinctly, and the middle one at ERROR.
A root install wrote a value another shipped file refuses¶
install.sh wrote RUNNER_UID=0 when run as root and printed a green tick over
it; the runner's own entrypoint exits 1 on exactly that value. The documented
path produced it. The installer now writes nothing as root, lets the compose
default apply, and tells you what to set and how to find it.
The prechecks the documentation promised now exist¶
The installer's welcome text listed 2 GB RAM, 10 GB disk and time sync and said the installer checks them. It did not — none of those checks existed. They do now, and they fail hard, beside the existing binary checks. Disk is checked on both the install directory and Docker's data root, which are commonly on different filesystems. The result is three-valued: measurably short refuses, cannot-measure warns.
A misspelled LLM provider no longer produces a healthy, silent instance¶
An unrecognized HIVEMIND_ASSISTANT_PROVIDER was logged and then ignored, so
the server came up green with no LLM at all and an assistant that did nothing. A
typo produced a successful install and a dead chat box. The one message that
named the valid values was structurally unreachable for this error.
It is now refused at boot, with the valid values enumerated, and validated before the database connection — so you can hit the refusal without having provisioned a database first.
A stale trust anchor blamed the wrong artifact¶
If you had a secrets/cosign_pub from an earlier install, it was reused on
faith and never re-checked against the pinned checksum. A superseded key then
failed manifest verification with "the manifest is not the one the publisher
signed" — sending you to audit a file that was byte-for-byte correct. The anchor
is re-verified on reuse, and the failure now names whichever artifact is
actually wrong.
The documentation URL the installer printed was a 404¶
The only docs URL install.sh printed sent every customer to a missing page. It
is corrected, and a gate now covers the file the customer actually executes —
which is what the previous version of that check had missed.
The installer also now accepts the release bundle where the bootstrap actually puts it, rather than where it expected to find it.
SSO configuration reaches an upgraded install, not only a new one¶
The forward-auth block install.sh writes was only reaching greenfield
installs. If you upgraded, the SSO variables the setup guide tells you to
configure never appeared in your .env. Fixed, along with a missing SSO block
and a disk precheck that was measuring the wrong filesystem.
Separately, a guard meant to warn about a missing runner bind mount ran before the installer changed into the project directory, so it could never fire.
The assistant stopped being rejected by Anthropic¶
The tool list sent to the provider was assembled by concatenating three role-derived sets that overlap by construction, so shared tools were sent two or three times each. The provider rejected the whole request:
400 tools: Tool names must be unique.
The list is now assembled as a union inside the one function that builds it, so a future caller cannot omit the de-duplication. Where two definitions share a name, the one offered to the least privileged caller wins — if two spellings ever diverged, keeping the narrower one is the safe direction to fail.
Alongside it, the model picker now seeds and follows the configured provider rather than a hard-coded vendor default, and a stored model belonging to a different provider is corrected rather than sent as-is.
An empty environment value is read as absent, not as unrecognized¶
Reported by a customer, in the best description anyone has written of the defect: the flag looks set and is not.
Setting HIVEMIND_CONFIG_STORE_ENABLED produced a message saying your variable
"is set to "", which is not a recognized truthy value" — about a variable you
had never touched. The compose file rendered unset variables as empty strings,
which collapsed "you set nothing" and "you set something I do not understand"
into one signal.
This release fixes the reading side, in the shared predicate rather than in 47 individual compose entries. 0.1.80 fixes the writing side. Nothing becomes enabled: an empty value was never truthy and still is not. Only the reporting becomes honest.
The runner's state is visible in the product¶
A customer installed successfully and could not run agents. The installer had told them correctly, in terminal output that had scrolled past hours earlier, and the running product knew nothing about it.
The dashboard now shows whether a runner is actually attached, derived from the live connection a runner holds for its whole life. It counts runners, not subscribers — a browser tab left open on the dashboard is a subscriber too, and counting those would have rendered as an attached runner, which is a false all-clear on the surface built to prevent one.
It accepts no credentials. The surface is read-only; there is nowhere in it to type a key.
Do you need to do anything?¶
If you are upgrading, no — beyond the upgrade itself. If you have been running
the agent runner or using SSO, re-run install.sh afterwards so the corrected
.env and compose content actually reach your installation; an image upgrade
alone does not rewrite those files. If you previously edited
docker-compose.override.yml and found your changes missing after an upgrade,
that was the truncation defect above, and the .before-<ts> backups begin from
this release.
0.1.78¶
A note on the boundary of this section. 0.1.77 was released without a git tag, so the exact commit it was cut from is an inference rather than a record — see the 0.1.77 entry below. This section describes what landed between that inferred point and the 0.1.78 tag. A small number of the items below may have already been present in 0.1.77; we cannot tell you which, and we would rather say so than guess.
One database migration was added between 0.1.76 and 0.1.78. Because 0.1.77 was released without a tag, we cannot tell whether it first shipped in 0.1.77 or in 0.1.78. Treat both as adding it.
A pulled image can now say what built it¶
The published images carried empty org.opencontainers.image.version and
.revision labels — not wrong values, absent ones. No Dockerfile carried a
label at all, so an image you had pulled could not tell you what produced it.
All images are now labelled at build time with title, version, revision, source, licence and vendor. The revision is the field that matters: a version string is a claim someone typed, while the revision is the commit the bytes came from. When you report a bug, the version tells us what you think you have; the revision tells us what you have.
If the build cannot determine the commit, it refuses rather than emitting an empty label — an empty label is the defect, and "could not determine" must not share an outcome with "determined".
A denial-of-service fix on the HTTP path you actually use¶
h2, the HTTP/2 implementation underneath the server's request path, is moved
off RUSTSEC-2026-0258 — unbounded empty DATA frames, a remote
denial-of-service against the live serving stack. This is reachable in the
shipped server and sits directly on the path a customer traverses.
Two further advisories are cleared in the same lockfile change, and they are not three of a kind:
crossbeam-epoch(RUSTSEC-2026-0204) is present in the server binary, but the vulnerable path is not exercised.quinn-proto(RUSTSEC-2026-0185) is not reachable at all — it is an optional dependency of a feature this workspace never enables, so it is never compiled and never shipped.
Both are bumped because the cost was a few lockfile lines, not because they were urgent.
Commander chat remembers your conversation again¶
The commander page wrote to the native assistant and read its history
from somewhere else entirely — a legacy sidecar endpoint that is empty on every
customer install and returned 200 [] permanently. So you could send a message,
reload the page, and the conversation was gone. 0.1.55's note promised "reopen it
tomorrow and it is still there", which was true of the store and false of the
page in front of it.
The page now reads the assistant's own history, and a reply joins the restored thread instead of silently starting a new one beside it.
The old endpoint also answered a bare [], so the page could not distinguish
"no history yet" from "the read failed" and would render "no chat yet" over a
failure. It now keys on the server's own success flag, and shows a read-failed
banner when the read actually failed.
Non-admin SSO users are sent to a chat mount that will serve them¶
The chat page rendered for every SSO principal, and its send button was admin-only. A non-admin user got the page fully rendered and then a 403 on the first message. A user with no groups got the same.
A mount built for exactly that caller already existed, was already authorized correctly, and was referenced by no template anywhere — so this was wiring, not capability. The page now chooses the mount the request will actually be admitted to, using the same predicate the gate itself uses, so the page and the gate cannot disagree about who is an admin.
The release process can no longer forget to tag¶
Two gates were added to our own release tooling, and they are the direct reason the 0.1.77 entry below exists as an honest gap rather than a wrong answer: publishing now refuses a version with no tag naming the commit that was cut, and refuses a tag whose commit does not carry that version. Building separately refuses a cut whose version the source tree does not carry.
These do not change anything on your installation. They are recorded here because they are why this class of gap should not recur, and because the correction they enforce is one you can verify yourself from the image labels described above.
0.1.77¶
This release shipped, and we cannot tell you in full what was in it. That is our failure to record, not a gap in the software.
0.1.77 was built, published, signed, promoted and installed with no version tag ever placed. Two other releases — 0.1.74 and 0.1.76 — have the same gap, but 0.1.77's is the one with a customer-visible consequence.
If you are running 0.1.77, your instance reports the wrong version¶
/api/v1/health on a 0.1.77 install answers 0.1.76. So does
--version. This is confirmed on a real customer installation running the
correct image digest with the signature verified.
The cut was made against a source tree whose manifest still said 0.1.76, and the gate that now refuses exactly that did not exist until 0.1.78. So the binary was compiled believing it was 0.1.76, and it says so honestly.
How to tell what you are actually running. Do not trust the version string
on this release. Compare the image digest you are running against the digest
in the release manifest you installed from. From 0.1.78 onward, the images also
carry org.opencontainers.image.version and .revision labels, which give you
a second, independent answer — but those labels are absent on 0.1.77 images, for
the same reason everything else here is.
What we can and cannot tell you about its contents¶
We cannot enumerate this release's changes with confidence, and we are not going to publish a guess:
- Nothing published points back at a commit. The images of that era carry no revision label, and the release manifest carries no commit field.
- No commit ever carried the version 0.1.77. The source line runs from a commit declaring 0.1.76 straight to one declaring 0.1.78. There is no "the 0.1.77 commit" to look at.
- Container images are built from a working directory rather than from a recorded commit, so even the best-supported candidate cannot be proven to describe the bytes that shipped.
We have recorded internally which commit is the best-supported inference, with
its full evidence chain and labelled explicitly as an inference. We have
deliberately not placed a v0.1.77 tag on it. A tag asserts a faithful
record of which bytes became a release, and we do not have one; a false record
reads as authority, while a gap at least reads as a gap.
The changes in that window are the ones between 0.1.76 and 0.1.78 and, if a specific fix matters to you, we can answer for it individually — ask, and we will answer from the published artifact rather than from a tag we do not have.
Do you need to do anything?¶
If you are on 0.1.77, upgrade. Beyond the reporting defect above, 0.1.78 carries the denial-of-service fix described in its entry, and 0.1.79 and 0.1.80 carry the installer and authorization fixes in theirs.
If you have asked us which version you are on and been told 0.1.76 from the health endpoint, that answer may have been wrong. We got this wrong ourselves before we found it.
0.1.76¶
Eleven more documented switches actually reach the server¶
0.1.75 fixed two variables that the documentation described and the container
never received. This release closes the rest of that class. Eleven more are now
declared in docker-compose.yml's server service:
HIVEMIND_GATE_WEB_UI, HIVEMIND_GATE_WS_LIVE |
HIVE-1102 |
PLANE_PROJECT_MAP |
HIVE-1103 |
HIVEMIND_OPENAI_API_KEY, OPENAI_API_KEY, HIVEMIND_OPENAI_ENDPOINT |
HIVE-1101 |
HIVEMIND_METRICS_TOKEN, METRICS_ENABLED, PROMETHEUS_URL |
HIVE-1104 |
HIVEMIND_CSRF_SECRET, HIVEMIND_CORS_ALLOWED_ORIGINS |
HIVE-1105 |
Nothing changes if you have not set them. Every one is added with an empty
default (:-), and each was checked against the code that reads it to confirm
empty behaves identically to unset — not assumed. The two gates default to
closed whether the variable is absent or blank; PLANE_PROJECT_MAP parses an
empty string to an empty map; the OpenAI variables are filtered on non-empty
before use. So this release only makes these switches possible to set. It
does not change what happens to an install that leaves them alone.
The reason the empty default matters rather than being a detail: a required-with-no-default variable on a profile-gated service breaks the compose parse for everyone, including customers who never enable that profile. That is the defect that broke fresh installs of 0.1.72. Every variable here uses the form that cannot do that.
/metrics can be given a token for the first time¶
HIVEMIND_METRICS_TOKEN is the only application-layer protection /metrics
has, and until this release it could not be configured at all — the variable
never reached the container, so setting it in .env did nothing and said
nothing. That endpoint exposes your LLM spend in USD, your configured cost
caps, per-profile labels you chose the names of, the exact running version, and
a live pass/fail signal from the security self-check. Set the token if you
expose Hivemind at all.
One honest limit, because the fix does not cover it. /metrics binds to
loopback by default and is unreachable on a stock install. If you follow the
documented SSO setup, Caddy's forward-auth covers it. But if you expose
Hivemind without SSO — which nothing forbids and nothing documents — there is
still no guidance telling you to exclude /metrics in your own reverse proxy.
This release gives that population a token they can actually set; it does not
give them the documentation they should have. That gap is tracked separately.
0.1.75¶
The chat box works on a fresh install¶
HIVEMIND_ASSISTANT_PROVIDER — the switch docs/self-hosting/api-reference.md
has documented since 0.1.55 — never reached the server container: nothing in
docker-compose.yml declared it, and neither did the OpenAI key its code
default needs. The chat box rendered and the first message 503'd, on every
fresh install, every time, for nineteen releases. Fixed by defaulting the
switch to anthropic in the container's environment — not in the code, whose
own default stays openai (a deliberate choice from 2026-09-01, for anyone
setting the variable directly) — so a fresh install now talks to the
Anthropic key you already provided during setup. No new credential required.
SSO can actually be turned on¶
docs/self-hosting/sso-setup.md has told customers to set
HIVEMIND_FORWARD_AUTH and ten companion variables in .env since that guide
was written. None of them ever reached the container — docker-compose.yml's
server service never declared any of the eleven. Following the guide
exactly produced no error and no effect: forward-auth silently stayed off.
Fixed; every default added preserves exactly what happens today with none of
them set.
This was a broken feature, not an authentication bypass. With
HIVEMIND_FORWARD_AUTH unreachable, the request-source trust decision took
its Passthrough branch before any X-Authentik-* header was ever read —
never read from an untrusted source, not read-and-ignored. The resulting empty
trusted-nets list opened nothing either: trust is any-over-an-empty-set, false
for every peer. And because that check was always false, the strip-headers
branch was always taken too — a reverse proxy already injecting identity
headers had them actively stripped the whole time. Verified against a
freshly-migrated, dedicated test database: 5 tests, 0 skipped, 0 failed.
The agent runner's key reaches the server, and the profile is asked for¶
Two more pieces of the runner story 0.1.74 didn't finish (see the correction
on that entry, below). HIVEMIND_RUNNER_API_KEY — minted by install.sh,
delivered to the runner container — now also reaches the server, which is
what actually turns it into a usable credential; without this, no amount of
minting could have worked. And install.sh now asks, right after you provide
(or skip) a Claude credential, whether to enable the runner now — defaulting
to yes only when you have a key to use, and telling you about the repository
bind-mount it still cannot set up for you, either way you answer. Skip the
credential and nothing changes: the profile stays opt-in exactly as before.
One behavior note: re-running install.sh to upgrade can now enable the
runner profile, if you answer yes to that prompt and have a key on file. The
automatic Updates-page apply path never touches profile activation at all —
only a manual install.sh re-run can.
Known issues, not yet fixed¶
Found by the same audit that found the assistant and SSO gaps above — every
environment variable the server reads, checked against what
docker-compose.yml actually declares. Filed, not silently worked around,
and not fixed in this release:
- The OpenAI assistant backend has no way to receive its own key (HIVE-1101) — independent of which provider you select, only the Anthropic path is reachable today.
HIVEMIND_GATE_WEB_UI/HIVEMIND_GATE_WS_LIVEcannot be turned off (HIVE-1102) — both default to their gated-closed state and there is currently no supported way to opt out to the pre-gate, fully-public behavior even if you want it.PLANE_PROJECT_MAPnever reaches the server (HIVE-1103) — Plane integration credentials work, but the project-sync mapping does not.- No supported way to set a CSRF secret, a metrics bearer token, or opt in to Prometheus (HIVE-1104).
HIVEMIND_CORS_ALLOWED_ORIGINScannot be set (HIVE-1105) — embedding the public chat widget on a different origin than your Hivemind instance does not work.
Each of these is the identical shape: the server code is correct and
reachable, but nothing in the shipped compose file lets you configure it.
None require a new credential or a design decision — each is a one-line
addition to docker-compose.yml's server service, tracked separately.
Do you need to do anything?¶
No, beyond the usual upgrade, to get the assistant and SSO fixes. If you want
the agent runner and were not asked about it during upgrade (the automatic
Updates-page path does not ask), re-run install.sh once you have a Claude
credential on file.
0.1.74¶
0.1.74 adds 1 database migration.
The agent runner ships, and it works¶
The agent runner is the component that lets Hivemind run work on your own machines. It has existed in the codebase for a while; what was missing was everything needed to actually deliver it. This release closes that.
It gets its own identity. The runner authenticates as a dedicated service
principal — a runner account with only the authority it needs — rather than
reusing your instance's admin key. If you had wired a runner before this
release, it would have had to hold admin credentials to work at all. It no
longer does, and it cannot escalate to admin.
Your installer creates its credential for you. install.sh now mints a
HIVEMIND_RUNNER_API_KEY during install and writes it to your .env. It
refuses to write an empty value, and refuses to write one equal to your
ADMIN_API_KEY — either of those would have produced a runner that silently
could not work, or one holding more authority than intended. If you already
have a key in .env, it is reused and never regenerated.
The runner image is built and published. Previous releases built the runner
image on every cut to prove it compiled, but never pushed it, because nothing
server-side could turn its credential into a login — so a published image would
have guaranteed a 401 that looked like your own misconfiguration. That gap is
closed, and this is the first release where the image is available to pull:
verified at the registry, hivemind-runner:0.1.73 does not exist, so if you
looked for a runner image before now, there genuinely was not one.
Upgrading does not turn the runner on. It remains an opt-in compose profile.
Correction, added after this entry was first published: the header above
was incomplete. Everything it describes was true, and the runner still could
not authenticate — the minted key reached the runner container but never
reached the server container, because the server's own compose declaration
for HIVEMIND_RUNNER_API_KEY was missing (HIVE-1100). "Generated, delivered,
and ignored," the exact failure this whole feature exists to prevent,
recurring one layer further out than this entry noticed at the time. Fixed in
0.1.75 — see that entry, above.
Commander chat answers again¶
Commander chat was pointed at a sidecar service that is not part of a self-hosted install, so it could not answer. It now talks to the native assistant that ships with your server. If commander chat was silent or error-ing for you, this is why, and it is fixed.
No live internal URL ships to you any more¶
The manager sidecar's MANAGER_URL defaulted to a live internal address in
four separate places; a previous fix corrected only one of them. All four now
default to blank. This continues the fresh-install work from 0.1.73: a default
that points somewhere real is a default that fails confusingly on a machine
that is not ours.
Hardening¶
A permanent regression guard now proves that no model-originated request can
reach confirm_proposal. Confirming a proposal stays an action only a human can
take — this pins that property so it cannot regress silently.
Do you need to do anything?¶
No, beyond the usual upgrade. If you want to run the agent runner, see the
runner section of the self-hosting docs; you will need the
HIVEMIND_RUNNER_API_KEY that install.sh created, and it is already in your
.env if you installed or re-ran the installer on this release.
0.1.73¶
Install this one if you are installing for the first time¶
0.1.72 could not be installed from scratch. A fresh install failed at the last step with:
error while interpolating services.hivemind-runner.environment.HIVEMIND_API_KEY:
required variable HIVEMIND_RUNNER_API_KEY is missing a value
The compose file declared a required variable, with no default, on the opt-in agent-runner service. Docker Compose resolves every service's environment when it reads the file — before it decides which services your chosen profiles actually start — so a variable belonging to a service you never asked for still stopped the install. It failed after you had already generated your secrets, and named a variable the installer never told you to set.
If you are already running Hivemind, you were never affected, and you do not need to do anything differently. Updates replace container images; they do not rewrite the compose file on your host, so an existing installation never saw the broken declaration. This is also why it took a first-time install to find it.
What changed: the variable now defaults to empty instead of being required when the file is read. The agent runner is unaffected — it was already opt-in and is still not started by a standard install.
Nothing became less strict. An absent or empty runner key still cannot do anything: it reaches the runner, which presents it to the server, and the server rejects it. What changed is when the absence is noticed — at the point the value is used, rather than when the file is read.
Alongside the fix, a regression test now asserts the general rule rather than this one line: the shipped compose file may only require a variable that the installer unconditionally generates. It reads both the compose file and the installer to decide, so it cannot drift out of agreement with either.
0.1.72¶
Upgrade promptly: two authorization fixes, one of them a privilege escalation¶
If you run 0.1.71 or earlier, upgrade. Both issues below are authorization defects — authentication worked correctly throughout; what was missing was the check on what an authenticated caller is allowed to do.
Instance settings were writable by any authenticated account, including a
read-only one. /api/v1/settings and its web mirror /api/settings carried no
admin gate, so any principal with a valid credential could overwrite instance
configuration — including the vault password hash and the configured LLM API key —
and create arbitrary new settings keys. There was no allowlist bounding which keys
could be written.
The guard now sits on the settings router rather than on each mount, so both existing mounts inherit it and any mount added later inherits it too. It uses the non-overridable admin check, so no configuration flag can weaken it.
Self-registration no longer accepts an unauthenticated caller. The bootstrap registration path now requires the installer's admin key. First-run registration still works exactly as documented — you present the key you were given — and a second attempt is refused once an admin exists.
Do you need to do anything? Beyond upgrading, no. If you have non-admin accounts with API keys, review your instance settings after upgrading to confirm they hold the values you expect.
Cryptography dependency updated¶
The Chatalot cryptography dependency moves to a pinned build carrying four fail-closed corrections. No configuration change and no format change; the package set is otherwise unchanged from 0.1.71.
An assistant surface for non-admin users¶
Users who are not administrators get an assistant on its own mount, with a tool set derived from their role and no ability to reach approval actions. Proposing work remains an administrator capability for now — a non-admin proposal could not be actioned yet, and offering it would create requests nothing could fulfil.
0.1.71¶
A containerized agent runner ships — opt-in, and not yet turnkey¶
The component that starts an agent on a machine is now in docker-compose.yml,
as two services behind an opt-in runner profile: hivemind-runner and its
scoped hivemind-runner-socket-proxy. Until this release the runner was not in
the stack at all, which is why earlier documentation said a documented install
could not dispatch an agent.
Read this before planning around it. It is not part of a standard install and it is not turnkey:
install.shdoes not start therunnerprofile. A standard install still brings up four services, none of them the runner.install.shdoes not provision the runner's Anthropic credential. The service expectsANTHROPIC_API_KEY_FILE=/run/secrets/claude_api_key, and creating that secret is currently a manual step.- Because credential provisioning is still being settled, this page does not yet give a step-by-step for bringing the runner up. When there is a supported path, Install will carry it.
What changed is that the capability is now present and reachable rather than absent. What has not changed is that a fresh install does not have it running.
The reasoner now reads memory as its principal, not as the instance¶
reasoning::run previously read every memory in the instance
(MemoryFilter::default()). It now resolves what it may read through
memory_scope::reachable_projects — the same function the memories API uses, so
there is one answer to "who may read what" rather than a second one free to
drift from the first.
What this closed, stated precisely. No memory content ever reached a
persisted step: the one vault-derived line discards the content half of each
(key, content) pair before emitting. So this was not secrets in a transcript.
What did leak was key names of matching entries into agent_logs — and
because the hit filter matches on the value, a key appearing in that list
asserts that memory's body contains the searched substring. That is a substring
oracle over every memory body on the instance, one query per spawn, plus
enumeration of the matching key names, which are often customer-identifying on
their own.
Action required: none beyond upgrading. The scoping costs nothing in what the reasoner can report, because it reports no content either way — only its ability to resolve a subject it does not own.
Documentation corrected against the source¶
The published documentation was audited end to end against the code and
corrected. The substantive changes: the site no longer describes an attestation
component the product does not contain; the update path now names the signature
that is actually verified (the release manifest, which binds the image digest)
rather than one that is not; the unauthenticated-endpoint list is now the real
set; local accounts are documented as capping at one human, so SSO is presented
as the only multi-user path rather than a preference; and the role-capability
documentation was corrected where it understated what the ReadOnly role
actually refuses.
If you read these pages before this release and formed a plan from them, the two worth re-reading are Install and Upgrade.
0.1.70¶
Authorization fixes on the memories surface¶
Upgrade if your instance has more than one principal. Before this release the
six handlers behind /api/*/memories did not scope results to the caller. Three
of them took no caller identity at all; the by-id write pair checked only whether
the caller's role was readonly, which answers "may this caller write at all"
and never "may this caller write here."
Four concrete defects were measured against two authenticated tenants before the fix, and all four are closed in 0.1.70:
| Defect | Now |
|---|---|
One caller could read another project's memories by naming that project_id |
Refused — the caller's reachable project set is applied to the query |
The same rows were readable by id, with no project_id needed at all |
Refused; a by-id refusal returns 404, not 403, so it cannot confirm the id names a real memory |
| A caller could overwrite a memory belonging to another project | Refused |
| A caller could delete a memory belonging to another project | Refused |
A list request that omitted project_id also returned every memory on the
instance, because the project filter was applied only when one was supplied. That
is closed by the same change.
What decides access now. One function, memory_scope::reachable_projects,
returns the set of projects a principal may touch, and all six handlers consult it
and nothing else. Its current behavior preserves what the code already did
deliberately rather than inventing new policy:
- an agent reaches its own project plus the project named
infrastructure; - a human reaches the projects they own — ownership is the only relation this schema has, as there is no project-members table;
- an admin reaches everything.
Action required: none beyond upgrading. There is no configuration to set and no migration step for this fix. If your instance has ever issued an API key to anyone who should not see every project's memories, upgrading is the fix.
A read-only role is now reachable from SSO¶
Role::ReadOnly existed in Hivemind before this release but no SSO login could
ever land in it — it was reachable only from a role string already stored on a
user row. 0.1.70 adds a third group mapping, HIVEMIND_GROUP_READONLY, alongside
the existing HIVEMIND_GROUP_ADMIN and HIVEMIND_GROUP_USER.
Action required: none, unless you want the new role. An instance that never
sets HIVEMIND_GROUP_READONLY resolves roles exactly as it did before the
variable existed — the default is empty, and an empty group name can never match
because empty tokens are filtered out when the group header is split.
Read this if you run a multi-person instance. An authenticated identity that
matches none of your mapped groups still resolves to User, which can write.
So the read-only tier is not something you inherit by upgrading: it exists only
once you set HIVEMIND_GROUP_READONLY and put people in that group. See
Which of the three to set.
One piece of older advice is worth un-learning: pointing HIVEMIND_GROUP_USER at
a group nobody belongs to does not demote anyone. It routes those identities
to the default-allow rule, which is User — the most permissive outcome, not
the most restrictive.