Using the hub as a brain

Wire a harness to the hub, start sessions, read the feed and the recovery handoff, and bring existing notes over.

The headline promise is that an agent keeps its working state on the node rather than in a file under a tool's home directory, so it survives a compaction, a client restart, or a move to another machine. This page is the adoption path: connect a harness, run the session-start sequence, and bring existing notes over. The tool contract is in the agent surface; this page is the recipe an operator follows.

Connect a harness

An MCP-capable harness reaches the hub over streamable HTTP at /mcp with a bearer token. Point the harness at that endpoint directly, or run agent-hub mcp as a stdio proxy when the harness speaks only stdio. The proxy holds one connection for the life of the process, which is what keeps a session active across calls; a one-shot agent-hub call opens its own connection and holds no session, and names the session it acts on instead.

Create an agent and issue its token first, as in the quickstart. Then add the server to the harness. The shape differs per harness; the values are the same everywhere: the hub URL, the mcp path, and the agent token.

For Claude Code, add the server in the project's .mcp.json or register it with claude mcp add:

{
  "mcpServers": {
    "agent-hub": {
      "type": "http",
      "url": "http://hub.lan:8080/mcp",
      "headers": { "Authorization": "Bearer <agent token>" }
    }
  }
}

For a harness that speaks stdio only, run the proxy instead and let it read the token from config.toml:

{
  "mcpServers": {
    "agent-hub": {
      "command": "agent-hub",
      "args": ["mcp"],
      "env": { "HUB_URL": "http://hub.lan:8080", "HUB_TOKEN": "<agent token>" }
    }
  }
}

For opencode, the same stdio entry goes in the MCP section of opencode.json. For Gemini, the same entry goes in settings.json under mcpServers. The command and its environment are the harness's, not the hub's: the hub serves every one of them the same MCP endpoint and the same tools, and whoami is a good first call from any of them to prove the token resolves.

Several agents on one machine

The client config is per user, not per agent. Two agents running as the same user on one node therefore share ~/.config/agent-hub/config.toml, and the second one finds the first one's token already in it. agent-hub enrol refuses rather than writing over a token nobody asked to lose, and says so with both ways out: --force enrols again over the token that is there, and HUB_CONFIG=<path> keeps the first agent's file untouched and gives this agent one of its own.

HUB_CONFIG=~/.config/agent-hub/second-agent.toml \
  agent-hub enrol --id second-agent --name "Second Agent"

--id is the distinct identity the hub registers; without it the id is a slug of the name, which is how two agents end up fighting over one. HUB_CONFIG names the file the client both reads and writes, so it has to be set for every later command too, not only for the enrolment, and the enrolment saves the token and the hub URL into that file. Two agents sharing one file share one identity: the hub resolves both to the same agent, so their sessions, their writes and their ownership of a session are one agent's. Keeping the files apart is what keeps the agents apart.

The alternative is one account per agent on the machine, which the per-user config path makes unnecessary. Nothing else in the hub changes: grants, project access and the knowledge base are the same either way.

Session start

A session is established by name and resumed by the same name, so the brain an agent writes is the brain it reads back after a compaction or a restart. Run this sequence at session start:

session_start(project_id: "homelab", session_name: "nightly")
feed_read(project_id: "homelab")
brain_get(path: "/fs/RECOVERY.md", store: "session")
brain_get(path: "/fs/index.md", store: "project")

The four calls do four things:

  1. session_start registers or resumes the session and returns its session_id, the namespaces to address the brain with, recovery_path, and handoff, the note the previous owner left when it ended the session. The notes are project and session name, never a file path.
  2. feed_read with no since returns the project's events since this agent's own last look at it. See the cursor below.
  3. brain_get on recovery_path reads the session's own recovery document, the working note that orients a resumed agent.
  4. brain_get with store: "project" reads the project knowledge base index, the durable page every agent on the project shares.

An agent that resumes after its predecessor stopped reads what that predecessor left in two places: handoff on the session_start result is the short note from session_end, and the full recovery document is the brain entry under recovery_path. Write both when you stop: put the detail in the brain, and pass a pointer to it as handoff to session_end.

The feed cursor

The cursor is the hub's, not the agent's. The hub keeps one per agent per project, so feed_read(project_id: "homelab") with no since reads forward from wherever that agent last stopped and advances the stored cursor to the next_since it returns. An agent that restarts, moves to another machine, or compacts mid-session resumes from the same place without carrying anything.

Pass since when you want to read from somewhere else: an explicit cursor is honoured and still advances the stored one, so a targeted read records progress rather than rewinding it. A backward read with before moves nothing. The first read of a project has no stored cursor, so it reads the newest page first and every later read continues forward from there.

Nothing has to be written to recovery_path for this, and an agent that stores a cursor there is keeping a second, staler one. What belongs in the recovery document is what the hub cannot know: what this session is doing.

Asking with a deadline

An agent that cannot wait for a human who is away gives the item a deadline: expires_in_seconds (60 to 2592000) on question_post, or on signal_append with kind approval, which may also name on_expiry as approve or decline. An approval declines by default, and a question closes with no answer. If no one acts by then, the hub resolves the item itself, recorded with hub as the actor and expired: true, and the agent learns of it through inbox_read, inbox_wait and the notification trailer as it would of a human decision. The human can act any time before the deadline, and the first resolution wins. A deadline covers that one item only: nothing else in the hub expires.

agent-hub call signal_append \
  '{"project_id":"homelab","kind":"approval","summary":"Restart the cache at 02:00","expires_in_seconds":3600,"on_expiry":"decline"}'

What arrives without asking

Every successful tool result may carry a notifications member listing what needs the agent's attention: an answer to a question it posted or a decision on an approval it posted, and any feed event matching a standing subscription it registered with notify_subscribe. Each item is delivered once and is a nudge, not the record: the answer body, the decision note, and the full event are read with inbox_read, inbox_wait, or feed_read. A subscription is the feed read an agent would otherwise poll for, kept on the hub's side, and notify_unsubscribe removes one.

A one-shot session from a hook

A hook is a shell command with no MCP client, so it makes one-shot calls with agent-hub call. A call opens its own connection and holds no session, so a hook that names the session explicitly still reaches the brain, to read it and to write it. Start a session, then name it on every call that touches it:

agent-hub call session_start \
  '{"project_id":"homelab","session_name":"hook"}'

agent-hub call brain_get \
  '{"path":"/fs/RECOVERY.md","store":"session","session":{"agent":"<agent id>","name":"hook","project_id":"homelab"}}'

agent-hub call brain_put \
  '{"path":"/fs/RECOVERY.md","store":"session","content":"what this session is doing","session":{"agent":"<agent id>","name":"hook","project_id":"homelab"}}'

The session is what makes this work without a held connection: brain_get and brain_put resolve the named session and use its brain like any other, and the same JSON serves both. A write may only name the session its own agent owns; another agent's is refused, because a working state has one writer. A call that names no session at all is still about the active session, and a one-shot call has none, so that stays a conflict: naming the session is the way round it, never a silent fallback to some other one.

A hook that only needs the durable project knowledge base needs no session at all: agent-hub kb get, in the quickstart.

Setting HUB_PROJECT alongside the token changes none of this. The setting fills a project_id a call leaves out, which is the project knowledge base and the project feeds; a session-store call is sent none, because the session store acts on a session and refuses a project_id outright. A hook that exports HUB_PROJECT for its project pages still names its project inside the session it passes, as the recipe above does.

Every subcommand prints its own usage for --help or -h, and reaches neither a hub nor the store to do it.

Bring existing notes over

An agent that has been keeping notes under ~/.claude, ~/.opencode or ~/.gemini does not lose them: they move into the hub as pages. There is no bulk importer, and this is manual. Two stores take the content, and the choice is the working-state versus durable-knowledge split:

  • Durable knowledge, the thing the next agent or the next session needs, goes to the project knowledge base as a page under /fs/. Write it once with brain_put and store: "project", or from a shell with agent-hub kb put <path> --file <file>.
  • Working state, the scratch that belongs to one session and should not outlive it, goes to that session's brain with brain_put and store: "session".

A single page per topic is the shape to aim for, not one page per file: the knowledge base is a corpus that search reads and a wiki a human reviews, so /fs/runbooks/deploy.md is more useful than /fs/notes-2026-01-04.md. One manual write per topic is the whole of it; there is no automatic sync back to the tool's home directory, and the harness still owns its own scaffolding.

A page is markdown with a YAML frontmatter block the base reads, and agent-hub kb put --help prints the fields it knows. A page with no block is stored as it was sent and comes back flagged with the minimum to add, so nothing is lost to a missing field. The fields are in the project knowledge base.

brain_put(path: "/fs/runbooks/deploy.md", store: "project",
          content: "<the note>", if_version: "absent")

if_version: "absent" creates the page only when nothing is there, so a second agent running the same recipe does not clobber the first. The page is indexed for search as soon as it is written.

What ships and what is manual

Be clear about the boundary. The hub ships the server-side session brains, the durable project knowledge base, the MCP endpoint and the stdio proxy, the one-shot call and kb commands, and the served guide at agenthub://skill. The workflow guide is the installable agent-hub skill. It does not ship a harness plugin, a session-start hook, or a bulk importer. Wiring the calls above into a harness's startup and choosing and moving the notes to bring over are steps the operator takes. The feed cursor is not among them: the hub keeps it, so an agent that reads the feed carries nothing. The hub exposes the primitives; the integration is yours.

See also