Plow Seeds
seeds/plow-pbc/seed-hermes-gbrain

Hermes + gbrain (Compose)

Give your Hermes AI agent a persistent, searchable knowledge graph it can read and write without any image rebuild.

  • knowledge-management
  • ai-memory
  • docker-compose
  • hermes
  • agent-tooling
by plow-pbc·branch main·commit 9114d40·updated Jun 1, 2026

Setup & installation

Install in your terminal with command:
curl -fsSL https://raw.githubusercontent.com/plow-pbc/seed/main/install.sh | bash -s -- https://github.com/plow-pbc/seed-hermes-gbrain/blob/main/SEED.md

What this seed does

This seed adds a long-term memory layer to a Hermes AI agent running in Docker Compose. Once installed, Hermes can store anything you ask it to learn — articles, notes, PDFs — in a self-improving knowledge graph, and later search or query that brain through ordinary chat. Nothing leaves your machine: the entire brain lives in a folder on your host that survives container restarts.

The install is done by running one script against your already-running Hermes container. The script handles everything inside the container — no need to rebuild the Docker image, install anything on your host beyond Docker, or touch the Hermes codebase. The brain persists on your disk; only two small symbolic links live inside the container and are automatically restored on every restart by an entrypoint hook the installer places.

Semantic (meaning-based) search is on by default and requires an embedding API key from OpenAI, Voyage, or ZeroEntropy; if you prefer not to use an external API, pass --no-embedding and the brain still works with fast lexical (keyword) search. An optional --with-sidecar flag enables a background sync process for legacy workflows that write plain markdown files to the brain folder.

When to use it

  • You want Hermes to remember articles and notes you share with it so you can ask questions about them later.
  • You're running Hermes in Docker and want to add a persistent knowledge base without rebuilding or modifying the container image.
  • You want to send a URL to Hermes via iMessage (with seed-hermes-plow-chat) and have it automatically ingested into a searchable brain.
  • You want to switch your Hermes brain from the default local database to a hosted Postgres instance safely, without losing your embedding API key.
  • You need to set up a knowledge graph for an air-gapped or offline environment where no embedding API is available, using lexical-only search.
View raw SEED.md
# Purpose

> See [[README#Purpose]].

## Normative Language

The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, RECOMMENDED, MAY, and OPTIONAL in this document are to be interpreted as described in RFC 2119.

`Implementation-defined` means the behavior is part of the implementation contract; this specification does not prescribe a single policy.

Sub-folder SEEDs in this tree inherit the RFC 2119 declaration. They MUST NOT re-declare it.

## Dependencies

### Runtime

- Hermes Agent MUST run in the Docker-backed `seed-hermes` shape: a host `compose.yaml`, a whole `./data:/opt/data` bind mount, and `HERMES_HOME=/opt/data` inside the container. ^dep-hermes-docker
- The container's hermes user (uid 501, gid 20 by default) MUST have its passwd-level HOME at `/opt/data`. The official `nousresearch/hermes-agent` image declares `hermes:x:501:20::/opt/data:/bin/sh` in `/etc/passwd`. ^dep-hermes-home
- The Hermes runtime MUST inject a subprocess HOME at `${HERMES_HOME}/home/` (i.e. `/opt/data/home/`) for every command spawned by the agent's terminal tool. This is implemented by `get_subprocess_home()` in Hermes' `hermes_constants.py`: when that directory exists, every subprocess gets `HOME=/opt/data/home` regardless of the user's passwd HOME. gbrain looks for its store at `$HOME/.gbrain/`, so the install MUST live under `/opt/data/home/`, not under `/opt/data/`. This is the single load-bearing fact of this SEED; getting it wrong produces "No brain configured" errors that look like an install bug. ^dep-subprocess-home
- The container's login-shell PATH (used by `bash -lc`, which is how Hermes' terminal tool invokes commands) MUST include `/usr/local/bin`. The official Hermes image sets this. The installer MUST place gbrain on the login-shell PATH by symlinking `/usr/local/bin/gbrain` and `/usr/local/bin/bun` as root; the non-login-shell-only `/opt/data/.local/bin` is NOT sufficient. ^dep-path-login
- The container MUST have outbound network access to `https://bun.sh/install` and to the gbrain source repo while `install_gbrain_into_compose.sh` runs. After install, no network is required from the host setup path. ^dep-install-net
- The container MUST have `bash`, `curl`, and `git` available. The official Hermes image (Debian 13 trixie) supplies these. ^dep-container-tools
- The container MUST be able to acquire `unzip` (Bun's installer requires it). The official Hermes image does NOT ship unzip. The installer script MUST install it via `apt-get install -y unzip` running as uid 0 inside the container. This writes to the container's writable layer, not the bind-mount, so if the container is later recreated the installer script MUST be re-run (which is idempotent). ^dep-unzip

### Host

- The host MUST have Docker with Compose support and a `seed-hermes` scaffold prepared with `./scripts/prepare.sh`. ^dep-host-docker
- The host MUST be able to bring up at least one Compose service (`hermes`) declared in `<scaffold>/compose.yaml`. v0.2.0+ default installs run hermes-only. When `--with-sidecar` is passed, the host MUST also be able to bring up a second `gbrain-sync` service declared in `compose.gbrain.yaml` (file-then-sync flow, retained for v0.1.x downstream agents — NOT host-crontab). ^dep-host-sidecar
- The host setup path MUST NOT require host `bun`, host `gbrain`, host `node`, host Python beyond `python3`, or host writes outside the scaffold directory. ^dep-host-minimal

### Optional adjacent seeds

- `https://github.com/plow-pbc/seed-hermes-plow-chat` is OPTIONAL. When installed in the same scaffold, the user MAY drive ingestion by texting an iMessage like "ingest [URL]" and the Plow Chat plugin routes the message into Hermes' normal chat surface; the Hermes prompt at `ref/hermes-prompts/ingest-instructions.md` takes care of writing markdown to `/opt/data/home/brain/`. ^dep-plow-chat

## Objects

The named entities that exist on the Hermes side. Knowledge-graph entities (brain pages, embeddings, edges) are owned by gbrain and are not redefined here.

### Container-resident gbrain install

All install artifacts live under `/opt/data/home/`, which is the HOME Hermes' terminal tool injects for subprocesses. Because `/opt/data` is bind-mounted from the host's `<scaffold>/data/`, every artifact survives `docker compose down/up` without an image rebuild.

- `/opt/data/home/.bun/` MUST hold the Bun runtime install. ^obj-bun-install
- `/opt/data/home/.bun/bin/bun` MUST be the bun binary. The install script MUST symlink it as `/usr/local/bin/bun` (root-owned, in the container's writable layer) so it is reachable from the login-shell PATH Hermes uses. ^obj-bun-symlink
- `/opt/data/home/.bun/bin/gbrain` (or equivalent post-`bun link`) MUST be the gbrain CLI entrypoint. The install script MUST symlink it as `/usr/local/bin/gbrain` (root-owned). The `/usr/local/bin/` symlinks live in the container's writable layer, not the bind mount; if the container is recreated, the installer MUST be re-run. The install is idempotent so re-running is safe. ^obj-gbrain-symlink
- `/opt/data/home/.gbrain/` is the gbrain PGLite store and config dir; gbrain locates it via `$HOME/.gbrain`. ^obj-gbrain-store
- `/opt/data/home/.gbrain-src/` is the cloned gbrain source the install runs `bun install && bun link` in. ^obj-gbrain-src
- `/opt/data/home/brain/` MUST be a git-initialized markdown repository (still relevant for v0.1.x file-then-sync flow + downstream agents that author plain markdown). v0.2.0+ direct-put agents (e.g. seed-hostex-history-ingest memory layer) write pages via `gbrain put` and do not require the brain repo to contain their pages. The install script MUST `git init` the brain repo and create a single empty seed commit so `gbrain sync --repo /opt/data/home/brain` does not fail on an unborn HEAD (only relevant when `--with-sidecar`). ^obj-brain-repo
- `<scaffold>/data/bin/hermes-gbrain-entrypoint.sh` MUST exist and be executable. It is the hermes service entrypoint hook: on container boot it (as root) re-applies the `/usr/local/bin/{bun,gbrain}` symlinks to the bind-mounted bun + gbrain binaries, then exec's the canonical Hermes entrypoint. This is what makes the gbrain install survive `docker compose down`/`up` cycles — the symlinks live in the container's writable layer (reset on recreate) but the binaries persist in the bind mount. The install script MUST copy `ref/scripts/hermes-gbrain-entrypoint.sh` to this location. ^obj-hermes-entrypoint-hook
- `<scaffold>/data/bin/gbrain-init-url-safe.sh` MUST exist and be executable. It is the operator-facing safe wrapper around `gbrain init --url <db_url>`. `gbrain init --url` rewrites `~/.gbrain/config.json` without merging existing fields, which silently drops the embedding-provider API key written by step 4b of the canonical install — gbrain then downgrades search to "conservative" mode with no fail-loud signal. This wrapper captures the keys from the existing config, delegates to `gbrain init --url`, and re-merges the captured keys into the new config (mode 600). The install script MUST copy `ref/scripts/gbrain-init-url-safe.sh` to this location. (Substrate clean-install defect #9, see `/tmp/cross-repo-substrate-defects.md`.) ^obj-init-url-safe-wrapper

### Host orchestration scripts

- `ref/scripts/install_gbrain_into_compose.sh` is the canonical installer. It targets a running scaffold with `--scaffold <dir>` (default `./hermes-agent`) or `--container <name>`. It MUST execute every install step as the hermes user (uid 501, gid 20 by default — overridable via `--uid` / `--gid`) with `HOME=/opt/data/home` pinned, so files land with the correct ownership AND in the directory Hermes' terminal tool will actually look in. The installer MUST write `<scaffold>/compose.gbrain.yaml` (the hermes entrypoint override; v0.2.0+ default contains hermes entrypoint only, no sidecar — see `^act-hermes-entrypoint-override`). When `--with-sidecar` is passed it MUST additionally emit the `gbrain-sync` service block into the same file and write `<scaffold>/data/.gbrain-sync.env` (mode `600`, sidecar-only). It MUST ensure `<scaffold>/.env` contains `COMPOSE_FILE=compose.yaml:compose.gbrain.yaml` so `docker compose up -d` picks up the override without `-f` flags. ^obj-install-script
- `ref/scripts/uninstall.sh` MUST stop and remove the `gbrain-sync` sidecar IF PRESENT, delete `<scaffold>/compose.gbrain.yaml`, delete `<scaffold>/data/.gbrain-sync.env` IF PRESENT, delete `<scaffold>/data/bin/hermes-gbrain-entrypoint.sh`, delete `<scaffold>/data/bin/gbrain-init-url-safe.sh`, and strip the seed-managed `COMPOSE_FILE=compose.yaml:compose.gbrain.yaml` line from `<scaffold>/.env`. It MAY remove `/opt/data/home/.bun`, `/opt/data/home/.gbrain`, `/opt/data/home/.gbrain-src`, `/opt/data/home/brain` from the bind-mounted host data dir when invoked with `--purge`. The default invocation MUST NOT delete the brain content. With `--purge`, the script MUST also remove the root-owned `/usr/local/bin/{bun,gbrain}` symlinks inside the running container. ^obj-uninstall-script
- `ref/hermes-prompts/ingest-instructions.md` MUST describe the ingest contract Hermes is expected to follow: write markdown to `/opt/data/home/brain/`, git-commit it, slug-matches-path, no manual files from the human. ^obj-ingest-prompt

## Actions

### gbrain is installed by docker exec into the running container

- A host agent MUST run `ref/scripts/install_gbrain_into_compose.sh --scaffold <seed-hermes-scaffold>` against a scaffold whose `docker compose up` is already running; it MUST NOT call `hermes plugins install`, modify the Hermes image, or run `bun`/`gbrain` on the host. ^act-install-via-exec
- The install script MUST be idempotent: re-running it against an already-installed scaffold MUST exit zero without reinstalling bun or re-cloning gbrain, and MUST NOT clobber an existing `/opt/data/home/brain` git repo. ^act-install-idempotent
- The install script MUST verify the bind-mount assumption by reading the hermes user's natural HOME (`docker compose exec -u <uid>:<gid> -T hermes bash -c 'echo $HOME'`) and refusing to proceed if it is not `/opt/data`. ^act-install-home-guard
- The install script MUST create `/opt/data/home/` so Hermes' `get_subprocess_home()` activates and every Hermes-spawned subprocess from that point on sees `HOME=/opt/data/home`. ^act-install-create-subprocess-home
- The install script MUST run every install step (bun install, git clone, bun install, bun link, gbrain init, git init of the brain repo) with `HOME=/opt/data/home` pinned, so the install exercises the same environment Hermes' terminal tool will use. This way HOME-related bugs surface at install time, not when the user texts the agent. ^act-install-pin-home
- The install script MAY accept `GBRAIN_REPO_URL` (default `https://github.com/garrytan/gbrain`) and `GBRAIN_REF` (default: remote's default branch) for pinning. ^act-install-pin-source
- The install script MUST detect the actual `hermes` user uid/gid by probing `/etc/passwd` of the running container BEFORE falling back to `<scaffold>/.env` `HERMES_UID`/`HERMES_GID`. The image-side `hermes` user (`awk -F: '$1=="hermes" {print $3":"$4}' /etc/passwd`) is the truth source; the host-side `.env` may carry host uids that don't exist in the image. If both are populated and disagree, the installer MUST emit a `WARNING:` to stderr and use the container-probed value (`--uid`/`--gid` flags retain override priority). ^act-install-detect-uid
- The install script MUST auto-detect provider-native API key environment variables when `--embedding-api-key` / `--openai-api-key` flags and `GBRAIN_EMBEDDING_API_KEY` are all unset. Auto-detection priority is provider-bound: `openai` → `$OPENAI_API_KEY`; `voyage` → `$VOYAGE_API_KEY`; `zeroentropy` → `$ZEROENTROPY_API_KEY`. The installer MUST log which env var it used (without echoing the value). ^act-install-autodetect-key
- The install script MUST recognize the `openai-codex/<model>` and `openai-codex:<model>` embedding-model prefixes as operator intent to reuse the existing Codex OAuth credential rather than acquire a separate paid api.openai.com key. Because gbrain has no Codex-OAuth-backed embedding provider implementation (verified empirically — `grep -rE 'openai-codex|codex.*embed' src/` returns zero matches), the installer MUST NOT attempt to authenticate the Codex OAuth token against api.openai.com (it returns 401). Instead, the installer MUST emit a banner to stderr explaining: (1) why this isn't supported; (2) that the install is gracefully degrading to `--no-embedding` (lexical-only search continues to work); (3) three self-serve paths to enable semantic search later (api.openai.com key + `gbrain config set`; voyage/zeroentropy alternatives; or re-run installer with canonical `openai:<model>` syntax). After emitting the banner the installer MUST set `NO_EMBEDDING=1` and complete a valid lexical-only install. ^act-install-codex-degrade

### brain repo is initialized

- The install script MUST `git init` `/opt/data/home/brain` if it is not already a git repo. ^act-brain-init
- The install script MUST create a single empty seed commit (`git commit --allow-empty -m "init brain repo"`) so `gbrain sync` works. ^act-brain-seed-commit
- gbrain syncs from the brain repo's git HEAD, not the working tree. Untracked markdown files are NOT picked up by `gbrain sync`. The ingest contract therefore REQUIRES Hermes (or any other writer) to `git add` and `git commit` newly written pages before they become reachable through `gbrain search` / `gbrain list`. ^act-brain-commit-required
- The install script MUST run `gbrain init` so the gbrain database (PGLite by default, Postgres when `--postgres-url <URL>` is passed) is materialized before first sync. With `--postgres-url`, the install delegates engine setup to `gbrain init --url <URL>` from the start, avoiding the unsafe PGLite→Postgres re-init sequence that drops the embedding API key (substrate defect #9). The embedding key is still persisted into `~/.gbrain/config.json` deterministically by step 4b regardless of which engine path runs. ^act-gbrain-init
- The install script MUST accept `--postgres-url <URL>` (env: `GBRAIN_POSTGRES_URL`) for first-class Postgres-backend initialization. The URL is forwarded verbatim to `gbrain init --url`. The URL value MUST NOT be echoed to stdout in full form; the install script masks the credential portion in any echoed banner (e.g. `postgres://<masked>@host/...`). ^act-gbrain-init-postgres-flag
- The install script MUST default to embeddings-on. It MUST default the model to `openai:text-embedding-3-large` and MUST require an embedding-provider API key obtained from one of (in priority order) `--embedding-api-key`, `$GBRAIN_EMBEDDING_API_KEY`, or an interactive prompt (silent, no echo) when stdin is a TTY. The install MUST fail fast if no key can be obtained. The operator MAY pick any provider gbrain supports via `--embedding-model <provider>:<model>` (e.g. `openai:text-embedding-3-small`, `voyage:voyage-3-large`, `zeroentropyai:zembed-1`). ^act-gbrain-init-embeddings-on
- The install script MUST validate the API key with a single pre-flight `curl` to the provider's `models` endpoint (bearer auth) before doing any other work, so an invalid key fails fast and not 30 seconds into the install. The operator MAY skip validation with `--skip-key-validation` for networks that block the provider. ^act-gbrain-key-preflight
- The install script MUST persist the API key into `/opt/data/home/.gbrain/config.json` under the provider-specific field (`openai_api_key`, `voyage_api_key`, or `zeroentropy_api_key`), with file mode `600`, never echoed to stdout/stderr, never written to any other location read by the `hermes` service, never set as a Hermes process env var. The key MUST be passed to `gbrain init` via an inline env-var assignment scoped to that single command so it does not appear in `ps`, `env`, or any traced output. **When `--with-sidecar` is passed**, the install script MUST also write the key into `<scaffold>/data/.gbrain-sync.env` under the same provider-specific field name in `KEY=value` form, mode `600`. That env file is loaded ONLY by the `gbrain-sync` sidecar via `env_file:` in `compose.gbrain.yaml` — the `hermes` service in `compose.yaml` MUST NOT reference it. This dual-write is required because gbrain v0.36.x reads the OpenAI key from `$OPENAI_API_KEY` during sync, not from `config.json`. v0.2.0+ default installs (no sidecar) MUST NOT write the sidecar env file. ^act-gbrain-key-storage
- The install script MUST accept `--no-embedding` as an explicit opt-out for offline / air-gapped / no-API installs. In that mode it falls back to `gbrain init --pglite --no-embedding`. When `--with-sidecar` is also passed, the sidecar env file sets `SYNC_FLAGS=--no-embed`, and verify's V7 sync uses `--no-embed`. ^act-gbrain-no-embedding-opt-out

### hermes entrypoint hook + (optional) sync sidecar in Compose

- The install script MUST write `<scaffold>/compose.gbrain.yaml` containing a `hermes` service override that sets BOTH `user: "0:0"` AND `entrypoint:` to `[/usr/bin/tini, -g, --, /opt/data/bin/hermes-gbrain-entrypoint.sh]`. `user: "0:0"` is required so the entrypoint hook runs as root and can write `/usr/local/bin/`. The hook then exec's the canonical Hermes entrypoint, which detects root and drops to the `hermes` user via `gosu` (the upstream image's pre-existing pattern). The override does NOT change the steady-state runtime user — only the boot-time privilege moment so the symlink re-apply works. Without `user: "0:0"`, the hook inherits the base compose's `user:` value (often non-root, e.g. uid 10000 on substrate v2 clean installs), `ln -sf /usr/local/bin/...` silently no-ops, and `gbrain` becomes unreachable from the login-shell PATH after the next container recreate. ^act-hermes-entrypoint-override
- The install script MUST copy `ref/scripts/hermes-gbrain-entrypoint.sh` to `<scaffold>/data/bin/hermes-gbrain-entrypoint.sh` (mode `755`). The script MUST be idempotent: it MAY run on container boot, `ln -sf` the symlinks if the binaries exist, and exec the canonical Hermes entrypoint with all args forwarded. ^act-hermes-entrypoint-script
- The install script MUST copy `ref/scripts/gbrain-init-url-safe.sh` to `<scaffold>/data/bin/gbrain-init-url-safe.sh` (mode `755`). The script: reads `$HOME/.gbrain/config.json`, captures every `*_api_key` field, calls `gbrain init --url "$URL" "$@"`, then re-merges the captured keys into the new config (mode `600`). It MUST verify the round-trip (keys present post-merge) and exit non-zero if any captured key is missing after the merge. It MUST NOT echo any key value to stdout/stderr; it MAY log the field names that were preserved. ^act-init-url-safe-script
- When `--with-sidecar` is passed, the install script MUST append a `gbrain-sync` service block to `<scaffold>/compose.gbrain.yaml`. The sidecar uses the same `nousresearch/hermes-agent` image, runs as `${HERMES_UID}:${HERMES_GID}`, sets `HOME=/opt/data/home` declaratively in `environment:`, mounts `./data:/opt/data`, loads `./data/.gbrain-sync.env` via `env_file:`, and exec's `gbrain sync --repo /opt/data/home/brain --watch --interval 300 ${SYNC_FLAGS:-}` as PID 1. The sidecar MUST `depends_on: hermes` and MUST have `restart: unless-stopped`. ^act-sync-sidecar
- The install script MUST add `COMPOSE_FILE=compose.yaml:compose.gbrain.yaml` to `<scaffold>/.env` so a plain `docker compose up -d` (no `-f` flags) picks up the override. If a `COMPOSE_FILE=` line already exists and does not already reference `compose.gbrain.yaml`, the installer MUST update it to the seed default. ^act-sync-compose-file
- The install script MUST NOT install a host crontab line. The host crontab path (previously `install_host_sync_cron.sh`) is removed because it failed on macOS hosts where cron's default `PATH` does not include `/usr/local/bin` and where `docker compose exec -u 501:20` defaults to `HOME=/opt/data` instead of `/opt/data/home`. ^act-sync-no-host-cron

### Hermes is taught the ingest contract

- A host agent SHOULD copy `ref/hermes-prompts/ingest-instructions.md` into `<scaffold>/data/skills/gbrain-ingest/SKILL.md` (or paste it into the Hermes system prompt for the chat surface in use) so Hermes, when asked to "ingest [URL]" or "ingest this PDF", writes structured markdown into `/opt/data/home/brain/` AND git-commits it, and never asks the human to do either. ^act-ingest-prompt-install
- The ingest contract MUST require Hermes to either omit the `slug:` frontmatter or set it to match the file path under `/opt/data/home/brain/`, because mismatched slugs are the primary cause of gbrain sync failures. ^act-slug-rule

### Hermes' terminal tool can invoke gbrain

- After install, `docker compose exec -T -u <uid>:<gid> hermes bash -lc 'which gbrain'` MUST resolve to `/usr/local/bin/gbrain` — i.e. the login-shell PATH (which is what Hermes' terminal tool actually uses) finds gbrain. ^act-path-login
- `docker compose exec -T -u <uid>:<gid> hermes bash -lc 'gbrain --version'` MUST exit zero. ^act-gbrain-version
- `docker compose exec -T -u <uid>:<gid> -e HOME=/opt/data/home hermes bash -c 'gbrain doctor'` MUST run all DB checks without `[FAIL]`-level errors on the DB layer. Config-level `[FAIL]`s (e.g. missing `RESOLVER.md`, missing embedding key) are advisory follow-up tasks and do not block this action. ^act-gbrain-doctor

### Per-profile model wiring (only if Hermes profiles are used)

- If a host agent creates a per-profile gbrain wiring (e.g. a `gbrain-memory` profile under `<scaffold>/data/profiles/<name>/config.yaml`), it MUST mirror the scaffold-level `model.provider` and `model.default` keys into the profile's `config.yaml`, otherwise `hermes chat -p <profile>` exits to the model setup wizard. ^act-profile-model-mirror
- This SEED does not create profiles by default; the wiring is documented but optional. ^act-profile-optional

## Verify

1. **Bun-present check.** Run `docker compose exec -T -u 501:20 hermes test -x /opt/data/home/.bun/bin/bun`. Does it exit zero? Expected: yes. ^v-bun-present

2. **gbrain-on-PATH (login shell) check.** Run `docker compose exec -T -u 501:20 hermes bash -lc 'which gbrain'`. Does it print a path ending in `/gbrain` (e.g. `/usr/local/bin/gbrain`) and exit zero? Expected: yes. This proves the PATH symlink works for the same shell mode Hermes' terminal tool uses. ^v-gbrain-path

3. **gbrain doctor check.** Run `docker compose exec -T -u 501:20 -e HOME=/opt/data/home hermes bash -c 'gbrain doctor'`. Does it run, produce output, and contain no DB-layer `fatal:` / `schema mismatch` / `No brain configured` lines? Expected: yes. A fresh install MAY produce config-level `[FAIL]`s (e.g. missing `RESOLVER.md`); those do not fail this check. ^v-gbrain-doctor

4. **Brain repo check.** Run `docker compose exec -T -u 501:20 hermes git -C /opt/data/home/brain rev-parse --is-inside-work-tree`. Does it print `true`? Expected: yes. ^v-brain-repo

5. **Host bind-mount survival check.** Inspect the host: `ls <scaffold>/data/home/.bun/bin/bun <scaffold>/data/home/.gbrain <scaffold>/data/home/brain/.git`. Do they all exist on the host filesystem? Expected: yes. ^v-host-bindmount

6. **(Conditional) Sync sidecar check.** Run `docker compose --project-directory <scaffold> ps --services --status running`. For `--with-sidecar` installs, the output MUST include a line equal to `gbrain-sync`. For v0.2.0+ default installs (sidecar opt-out), this check is skipped; verify.sh detects mode by inspecting `<scaffold>/compose.gbrain.yaml` for the `gbrain-sync:` service block. `--skip-sidecar-check` MAY be passed to override. ^v-sync-sidecar

6b. **hermes entrypoint hook check.** Run `test -x <scaffold>/data/bin/hermes-gbrain-entrypoint.sh` AND `docker compose exec -T hermes bash -c 'readlink -f /usr/local/bin/gbrain'`. The entrypoint script MUST exist + be executable on host, AND the in-container symlink MUST resolve to `/opt/data/home/.bun/bin/gbrain`. This proves the hook re-applied symlinks on container boot. ^v-entrypoint-hook

7. **End-to-end ingest check.** Write a deterministic markdown file under `/opt/data/home/brain/`, `git add` + `git commit` it (gbrain sync reads from git HEAD), run `docker compose exec -T -u 501:20 -e HOME=/opt/data/home hermes bash -c 'gbrain sync --repo /opt/data/home/brain'`, then run `gbrain search '<title>'` the same way. Does the search return the page? Expected: yes. For `--no-embedding` installs, the sync MUST include `--no-embed`; verify.sh auto-detects this by inspecting `config.json` for any `*_api_key` field. ^v-end-to-end

## Open

- This SEED does not ship a derived Hermes image. The `bun` + `gbrain` install lives entirely under the bind-mounted `/opt/data/home/` and persists via the host volume; only the `/usr/local/bin/` symlinks live in the container's writable layer. A future variant `Dockerfile.gbrain` (FROM `nousresearch/hermes-agent:latest`) would let `docker compose up` on a fresh host work without an install step, at the cost of image-rebuild overhead. ^o-derived-image
- (v0.1.x file-then-sync flow) The sync ran in a Compose sidecar (`gbrain-sync` in `compose.gbrain.yaml`) using `gbrain sync --watch`. The host-crontab path was removed because of `PATH` and `HOME` defects on macOS hosts (cron `PATH` does not include `/usr/local/bin`; `docker compose exec -u 501:20` defaults `HOME` to `/opt/data` instead of `/opt/data/home`). In v0.2.0+ the sidecar is OPT-IN via `--with-sidecar` — direct-put downstream agents (seed-hostex-history-ingest memory layer, str-manager-approval v12.4.0+) write pages via `gbrain put` and don't need a mirror. The sidecar is dead weight for them. ^o-sync-sidecar-shipped
- This SEED does not configure gbrain's secrets store (e.g. `X_BEARER_TOKEN`) or any external collectors. The blog tutorial's X-to-brain pipeline, OAuth 2.0 PKCE flow, and port-on-public-IP trick assume a VPS with a public IP and are out of scope for a Mac/Linux localhost compose target. ^o-x-collectors
- This SEED defaults to gbrain's PGLite backend. Switching to Supabase requires extra `gbrain config` keys and is not documented here. ^o-supabase
- Bridging the existing `openai-codex` OAuth credential at `data/auth.json` into a usable `OPENAI_API_KEY` for gbrain is out of scope. The operator supplies an embedding-provider API key at install time (default model `openai:text-embedding-3-large`, override via `--embedding-model`). For installs without API access, `--no-embedding` falls back to lexical-only search. ^o-embedding-providers
- This SEED does not author a `/opt/data/skills/RESOLVER.md` (gbrain's skill-routing table). A fresh install reports `resolver_health: MISSING_FILE` from `gbrain doctor`; that is a follow-up configuration task for the operator, not an install failure, and `ref/verify.sh` treats it as advisory. ^o-resolver
- PGLite is single-writer. **When `--with-sidecar` is installed**, the `gbrain-sync` sidecar and a manual `gbrain sync` (or `verify.sh`) can collide on the lock; the verify harness retries up to five times with a 3-second backoff. Operators who run heavy ad-hoc syncs should consider pausing the sidecar (`docker compose stop gbrain-sync`) for the duration. v0.2.0+ default installs (no sidecar) don't hit this contention. ^o-pglite-single-writer
- The canonical gbrain install command is `bun install + bun link` from the upstream repo. If a higher-level seed packages an opinionated `gstack-gbrain-install` step, it MAY be used in place of the inline install, but this SEED does not depend on it. ^o-gstack-install

## Non-Goals

- This SEED does not document gbrain's CLI or knowledge-graph semantics; see the upstream gbrain repo. ^ng-gbrain-cli
- This SEED does not modify the Hermes container image (no derivative Dockerfile). ^ng-image-rebuild
- This SEED does not configure or run X (Twitter), Slack, WhatsApp, or any other external collectors. ^ng-collectors
- This SEED does not require host-side gbrain, host-side bun, host-side Python beyond `python3`, or host-side network access after install completes. ^ng-host-minimal
- This SEED does not commit, log, or print any gbrain secrets, OAuth tokens, or bearer tokens. ^ng-secrets
View raw README.md
# Hermes + gbrain (Compose) SEED

## Purpose

This SEED installs [gbrain](https://github.com/garrytan/gbrain) — a self-improving
markdown knowledge graph that compounds with every source you feed it — into a
running `seed-hermes` Docker Compose scaffold, so Hermes' built-in terminal tool
can read and write the brain directly. It is the Docker-Compose-shaped sibling
of the host/bare-metal hermes-with-gbrain seed.

The install path is exec-only: no derived image, no Hermes plugin install, no
host Python or host Bun. `bun` and the gbrain CLI are installed *inside* the
running container, but into `/opt/data/home/` — which is bind-mounted from the
host's `<scaffold>/data/` directory, so the install survives
`docker compose down`/`up` without a rebuild.

Three things this SEED is *not*:

- It is not a derived `Dockerfile.gbrain` image (see `## Open` in `SEED.md`).
- It is not the blog-tutorial X-collector pipeline (the OAuth-callback-on-public-IP
  trick assumes a VPS, not a Mac).
- It is not a Hermes plugin in the seed-hermes-plow-chat sense; gbrain is reached
  through Hermes' shell tool, not through a gateway adapter.

## The load-bearing fact

Hermes' terminal tool does **not** spawn subprocesses with the hermes user's
natural HOME. It calls `get_subprocess_home()` in `hermes_constants.py`, which
rewrites HOME to `${HERMES_HOME}/home/` (i.e. `/opt/data/home/`) whenever that
directory exists. gbrain looks for its store at `$HOME/.gbrain/`, so the
install MUST live under `/opt/data/home/`, not under `/opt/data/`. Get this
wrong and `gbrain doctor` reports "No brain configured" even though everything
*looks* right when you `docker compose exec` it manually — because the manual
exec bypasses the HOME rewrite.

This SEED bakes the rewrite into every install step: the installer pins
`HOME=/opt/data/home` for the entire run, so the install path exercises the
exact same environment Hermes' shell tool will see, and HOME-related bugs
surface at install time rather than the first iMessage.

## Why exec-into-running-container works

The official `nousresearch/hermes-agent` image already does three things that
make the exec-only install easy:

1. The `/opt/data` bind mount maps `<scaffold>/data/` from host to container,
   so anything installed under `/opt/data/home/` persists.
2. `get_subprocess_home()` activates as soon as `/opt/data/home/` exists on
   disk, so Hermes' terminal tool sees the gbrain install without any
   `.profile` editing or `HERMES_MEM_HOME` env-var bake.
3. The container's login-shell PATH includes `/usr/local/bin`, where the
   installer drops root-owned symlinks for `bun` and `gbrain`. These two
   symlinks are the only thing that doesn't live on the bind mount — they
   live in the container's writable layer and get recreated on every
   (idempotent) install run.

## Install

From the parent folder that contains the seed-hermes scaffold at
`./hermes-agent/` and with `docker compose up -d` already running:

```bash
# Interactive — the installer prompts for your OpenAI API key (silently)
ref/scripts/install_gbrain_into_compose.sh --scaffold ./hermes-agent

# Non-interactive — pass the key via flag or env var
ref/scripts/install_gbrain_into_compose.sh \
  --scaffold ./hermes-agent \
  --embedding-api-key sk-…

# Different provider/model
ref/scripts/install_gbrain_into_compose.sh \
  --scaffold ./hermes-agent \
  --embedding-model voyage:voyage-3-large \
  --embedding-api-key pa-…

# Lexical only (no embeddings, no API key needed)
ref/scripts/install_gbrain_into_compose.sh \
  --scaffold ./hermes-agent --no-embedding
```

The installer:

1. **Asks for an embedding API key** (via flag, `$GBRAIN_EMBEDDING_API_KEY`,
   or interactive silent prompt), then validates it against the provider's
   `models` endpoint with a single pre-flight `curl`. If the key is bad,
   the install aborts before doing anything. Default model is
   `openai:text-embedding-3-large`; override with `--embedding-model`.
2. Verifies the hermes user's natural HOME is `/opt/data` (the image
   assumption).
3. Creates `/opt/data/home/` so Hermes activates the subprocess-HOME rewrite.
4. Installs `unzip` as root (bun's installer needs it; image ships without it).
5. Downloads and installs Bun into `/opt/data/home/.bun/`.
6. Clones the gbrain repo into `/opt/data/home/.gbrain-src/`, runs
   `bun install` and `bun link`.
7. Symlinks `/opt/data/home/.bun/bin/{bun,gbrain}` into `/usr/local/bin/`
   (as root, so login-shell PATH finds them).
8. Runs `gbrain init --pglite --embedding-model <model>`, passing the API key
   inline so it never enters the parent shell's env.
9. **Persists the API key** into `/opt/data/home/.gbrain/config.json` with
   mode `600` under the right provider field (`openai_api_key`,
   `voyage_api_key`, or `zeroentropy_api_key`). The key is never written
   anywhere else, never echoed, never logged.
10. `git init`s `/opt/data/home/brain/` and creates an empty seed commit.
11. Runs smoke checks under a login shell with HOME pinned to
    `/opt/data/home` — the same environment Hermes will use.

Re-running the script is a no-op when everything is in place.

### Key handling and Hermes isolation

The API key is written to exactly two places, both mode `600`, both never
read by Hermes' service:

- `/opt/data/home/.gbrain/config.json` — read by interactive `gbrain` CLI
  invocations from Hermes' shell tool.
- `<scaffold>/data/.gbrain-sync.env` — read only by the `gbrain-sync`
  sidecar (`env_file:` in `compose.gbrain.yaml`). The sidecar needs the
  key in process env at sync time because gbrain's embedding pipeline
  reads `$OPENAI_API_KEY` from the environment, not from `config.json`.

The key is never:

- placed in `data/.env` (which gets loaded into the **hermes** service's
  container env — that file is shared, the sidecar env file is not),
- placed in the `hermes` service's `environment:` block,
- set as `$OPENAI_API_KEY` for Hermes' main process,
- echoed to stdout/stderr at any point.

Hermes' provider is locked to `openai-codex` (which reads the OAuth token at
`data/auth.json`, not `$OPENAI_API_KEY`). Hermes' subprocess wrapper in
`tools/environments/local.py` actively strips provider env vars before
spawning commands. So even if your shell exported `OPENAI_API_KEY` for some
other reason, Hermes would not pass it through to gbrain. gbrain reads the
key off disk from its own config file (for CLI invocations) or from the
sidecar-only env file (for the sync loop), completely independent of Hermes.

## Sync sidecar (OPT-IN as of v0.2.0)

v0.2.0+ default installs run **hermes-only** — no sync sidecar. Direct-put
downstream agents (seed-hostex-history-ingest memory layer v0.2.0+,
str-manager-approval boss skill v12.4.0+) write pages directly via
`gbrain put` from inside Hermes' shell tool. A `gbrain sync --watch` sidecar
that mirrors `/opt/data/home/brain/` flat-files into gbrain is dead weight
for them.

For legacy v0.1.x flows that author markdown files to `/opt/data/home/brain/`
and expect a sync loop to ingest them, pass `--with-sidecar` to the
installer. That re-enables the historical behavior: the installer writes the
`gbrain-sync` service block into `compose.gbrain.yaml` and the
`data/.gbrain-sync.env` env file (mode `600`, sidecar-only, holds the
embedding provider's API key — `OPENAI_API_KEY`, `VOYAGE_API_KEY`, or
`ZEROENTROPY_API_KEY` — plus a `SYNC_FLAGS` line).

When the sidecar is active, the installer adds
`COMPOSE_FILE=compose.yaml:compose.gbrain.yaml` to `<scaffold>/.env` so a
plain `docker compose up -d` (no `-f` flags) brings hermes and gbrain-sync
up together. `docker compose down` stops both.

To tail the sync logs (only relevant with `--with-sidecar`):

```bash
docker compose --project-directory ./hermes-agent logs -f gbrain-sync
```

## Postgres backend + safe `gbrain init --url` (v0.2.1+)

Default install uses PGLite. For first-class Postgres:

```bash
./ref/scripts/install_gbrain_into_compose.sh --scaffold ./hermes-agent \
  --postgres-url 'postgres://gbrain:password@<host>:5432/gbrain' \
  --embedding-api-key sk-...
```

This skips the unsafe PGLite→Postgres re-init sequence entirely.

**Migrating an existing PGLite install to Postgres safely** (do NOT run
`gbrain init --url` directly — it drops your embedding API key):

```bash
(cd ./hermes-agent && docker compose exec -T -u 501:20 \
  -e HOME=/opt/data/home hermes \
  bash -lc '/opt/data/bin/gbrain-init-url-safe.sh \
    "postgres://gbrain:password@<host>:5432/gbrain"')
```

The safe wrapper captures `openai_api_key` / `voyage_api_key` /
`zeroentropy_api_key` / `anthropic_api_key` from your current config,
delegates to `gbrain init --url`, re-merges the captured keys into the
new config (mode 600), and verifies the round-trip. Never echoes key
values.

Why this exists: `gbrain init --url` rewrites `~/.gbrain/config.json`
without merging existing fields. Without the wrapper, your embedding
provider key disappears silently — gbrain downgrades search to
"conservative" mode (no LLM expansion, no embedding semantic cache) and
nothing fails loudly. Substrate clean-install defect #9. The wrapper
ships at `<scaffold>/data/bin/gbrain-init-url-safe.sh` and is operator-
runnable any time.

## Hermes entrypoint hook (always installed)

The installer ALWAYS writes a `hermes` service entrypoint override into
`compose.gbrain.yaml` and copies `ref/scripts/hermes-gbrain-entrypoint.sh`
to `<scaffold>/data/bin/hermes-gbrain-entrypoint.sh`. The hook runs on every
container boot (as root, before exec'ing the canonical Hermes entrypoint)
and re-applies the `/usr/local/bin/{bun,gbrain}` symlinks to the
bind-mounted bun + gbrain binaries.

Why this is mandatory: the `/usr/local/bin/` symlinks live in the
container's writable layer, which is reset on `docker compose down`/`up`.
The actual bun + gbrain binaries live in `/opt/data/home/.bun/bin/` (bind
mount, persists). Without the entrypoint hook, operators would have to
re-run the installer after every container recreate. With it, the install
is durable across recreate cycles.

Why a sidecar and not a host crontab (when `--with-sidecar`)?

1. **Portability** — works on any host that runs Docker (Linux, macOS,
   Windows). A host crontab is Unix-only and macOS has further wrinkles
   (cron's default `PATH` does not include `/usr/local/bin`, so `docker`
   itself resolves as `command not found`).
2. **Lifecycle** — `docker compose up`/`down` starts and stops the sync
   atomically. No orphan ticks against a torn-down container.
3. **No `docker compose exec` HOME gotcha** — `exec -u 501:20` defaults
   to `HOME=/opt/data` (the hermes user's `/etc/passwd` entry), not
   `/opt/data/home` where gbrain's config lives. The sidecar sets
   `HOME=/opt/data/home` declaratively in its `environment:` block.

If you installed with `--no-embedding` + `--with-sidecar`, the installer
writes `SYNC_FLAGS=--no-embed` into `data/.gbrain-sync.env` so the
sidecar's watch loop skips the embed step.

## Teach Hermes the ingest contract

`ref/hermes-prompts/ingest-instructions.md` is the contract Hermes needs to
follow:

- When the user says "ingest [URL]" or "ingest this PDF", Hermes writes
  structured markdown directly into `/opt/data/home/brain/`.
- Hermes then `git add`s and `git commit`s the file — gbrain syncs from the
  brain repo's git HEAD, not the working tree, so an uncommitted file is
  invisible.
- The frontmatter either omits the `slug:` field or sets it to match the file
  path, because mismatched slugs are the most common cause of sync failure.
- Hermes does not ask the human to write markdown by hand.

Copy this file into a Hermes skill or paste it into the system prompt for the
chat surface you use. When `seed-hermes-plow-chat` is also installed, you can
just text "ingest https://…" from iMessage and Hermes handles the rest.

## Uninstall

```bash
ref/scripts/uninstall.sh --scaffold ./hermes-agent
```

This stops and removes the `gbrain-sync` sidecar, the `compose.gbrain.yaml`
override file, the `data/.gbrain-sync.env` env file, and the seed-managed
`COMPOSE_FILE=` line in `<scaffold>/.env`. To also delete the brain content,
the PGLite store, the Bun runtime from the host volume, and the
`/usr/local/bin/{bun,gbrain}` symlinks inside the container, pass `--purge`:

```bash
ref/scripts/uninstall.sh --scaffold ./hermes-agent --purge
```

## Verify

```bash
ref/verify.sh --scaffold ./hermes-agent
```

This runs the seven checks listed in `SEED.md#Verify`, all under
`HOME=/opt/data/home` (the same HOME Hermes' terminal tool injects for
subprocesses), plus an end-to-end ingest round-trip. Exit code is zero on
success.

## License

MIT.

Version history

1 release
v1.0.0Jun 1, 2026

Initial Seed Release.

Dependencies

5 required · 3 optional
Hermes + gbrain (Compose)
  • Seedseed-hermes Docker Compose scaffoldrequired· seed ›· SEED.md: 'Hermes Agent MUST run in the Docker-backed seed-hermes shape'
  • SWDocker + Composerequired· link ›· SEED.md: 'The host MUST have Docker with Compose support'
  • SWgbrain CLI (garrytan/gbrain)required· link ›· Installed automatically inside container from https://github.com/garrytan/gbrain via bun link
  • SWBun runtimerequired· link ›· Installed automatically inside container at /opt/data/home/.bun/ by the installer script
  • APIEmbedding provider API key (OpenAI / Voyage / ZeroEntropy)optional· Required for semantic search (default: openai:text-embedding-3-large); omit with --no-embedding for lexical-only mode
  • SWPostgres (optional backend)optional· Optional via --postgres-url flag; installer defaults to PGLite (embedded)
  • Seedseed-hermes-plow-chatoptional· seed ›· SEED.md: 'OPTIONAL. When installed in the same scaffold, the user MAY drive ingestion by texting an iMessage'
  • StateHERMES_HOME bind-mount and subprocess HOME rewriterequired· SEED.md: container must have ./data:/opt/data bind mount and HERMES_HOME=/opt/data; get_subprocess_home() must activate /opt/data/home/

Contributors

1 contributor
Daniel DelattreContributor@delattre1

Activity

0 comments

You need to be signed in with GitHub to comment.

No comments yet — be the first to share how this seed worked for you.

Similar seeds