hivemind login. There are two ways to authenticate one, and the choice determines who the resulting sessions belong to.
Identity: PAT or service account?
Mint a PAT under Settings > Personal Access Tokens and inject it as
HIVEMIND_TOKEN, the same env var the daemon uses for any static token. Your org admin can disable PAT authentication org-wide or restrict it to write scope only, so check that policy before depending on one.
Service accounts have their own sa_* identity and scoped tokens. With OIDC federation, the workload swaps a short-lived ID token from a trusted provider for a HiveMind token at runtime, so there’s no static secret to rotate. Everything else on this page is about service accounts. Pick a PAT if you want the session under your own name, or an SA if you want it under a dedicated bot identity.
Pick an auth method
Prefer OIDC when it’s available. Policy is checked against verified claims on every exchange, and there’s nothing for you to rotate.
Identity providers
Configure these per service account in Admin > Service Accounts.GitHub Actions
Put branch, environment, and
pull_request constraints in the CEL policy, not the selector. Your workflow needs id-token: write so the runner will mint an ID token (see the Pattern A example below for the full job structure).
Kubernetes
Mount a projected service account token whose audience matches your HiveMind server:
path: token resolves to /var/run/secrets/hivemind/token, which is where the daemon reads from by default. Override with HIVEMIND_OIDC_TOKEN_FILE if you need a different path.
Modal
Modal injects
MODAL_IDENTITY_TOKEN once at container start and never rotates it. Modal mints ~48h ID tokens, so our handler clamps the resulting JWT to the 24h ceiling, long enough to cover a full-timeout Modal Function without a mid-run refresh. See Token lifecycle below.
Environment, app, and function names go in the CEL policy.
Generic OIDC
Any provider with an OIDC discovery document, including in-house IdPs, build systems, and anything that mints JWTs.
To feed an ID token to the daemon, write it to a file and point
HIVEMIND_OIDC_TOKEN_FILE at the path:
HIVEMIND_OIDC_TOKEN_FILE is unset, the daemon falls back to /var/run/secrets/hivemind/token, the same code path Kubernetes projected tokens use. The daemon reads the file and posts the contents to the backend, which routes by the token’s iss claim.
For tokens your provider rotates, rewrite the file in place. The daemon re-reads it on every refresh, so an updated file picks up automatically the next time the JWT nears expiry.
Static token
When OIDC isn’t an option, mint a service-account token in the dashboard and inject it asHIVEMIND_TOKEN. No exchange, no policy, so treat it like any other secret.
Pick an expiration when you create the token:
Scopes
There are two scopes,read and write, and the same set applies whether you authenticate via OIDC or a static token.
Most bots only need
write. Add read for analytics jobs or agents that look at past sessions to inform new runs. A read-only token fits dashboards and exporters that should never push.
Set one up
- Open Admin > Service Accounts > New service account.
- Pick a provider. Issuer is pre-filled for the built-in ones. For Generic OIDC, paste your provider’s issuer URL.
- Set the selector and scopes, and save.
(issuer, selector) lookup to find the SA, then verifies the token’s signature and audience against the configured provider. See Token lifecycle for how the resulting JWT is sized and rotated.
Advanced: CEL policies
A CEL policy further restricts which tokens get exchanged, on top of the issuer and selector match. It’s most useful for shared platforms like GitHub Actions:https://token.actions.githubusercontent.com is the issuer for every workflow on GitHub, so the policy is where you pin to specific repos, branches, or workflows. The same applies to Modal, where one issuer serves every workspace.
A GitHub Actions policy pinned to one workflow on main:
Token lifecycle
What happens after the daemon makes its first exchange.JWT lifetime
The HiveMind JWT the daemon receives tracks the ID token’sexp claim, clamped to [1h, 24h]:
The floor keeps short runner tokens (GitHub Actions hands out ~10min ID tokens) from causing constant refresh churn. The ceiling caps the blast radius of a leaked JWT. In between, we honor what the issuer signed for. A 4-hour Modal Function with a 4-hour identity token gets a 4-hour JWT, so the daemon doesn’t have to refresh mid-run.
Rotation
Roughly five minutes before the JWT expires, the daemon refreshes by re-reading the ID token source and re-running the exchange. Whether refresh succeeds depends on the source:
If a refresh attempt sees the same ID token it already exchanged (same
jti or the same synthesized replay key), the daemon’s client-side guard short-circuits the call and falls back to the cached JWT until it actually expires. After that point sync pauses until a fresh ID token is available.
JTI replay protection
Every ID token is exchanged exactly once. The backend stores the(issuer, jti) pair in Redis with a 24h TTL and rejects any subsequent exchange that matches.
A few edge cases worth knowing:
- Tokens without
jti(notably vanilla Kubernetes projected service account tokens, which carryiss/sub/aud/exp/iatbut nojticlaim) are handled automatically. The server derives a stable replay key by hashing the canonical full set of verified claims, keyed by the matched issuer and audience. Kubelet’s in-place rotation always produces a freshiat/exp, so legitimate rotations get a fresh key and succeed while replays of the same physical token still collide and are blocked. - Generic OIDC providers that follow OIDC Core typically include
jti. If yours doesn’t, the synthesis path catches it as long as the token carriesiat,sub, andexp. Tokens missing all three are rejected. - The replay window is per-issuer: two different issuers can mint tokens with the same
jtistring and they won’t collide.
Pattern A: CI agents you launch yourself
When you control the runner loop (you install the agent, you call it, you tear it down), run the daemon alongside the agent withhivemind start and hivemind stop. Sessions stream live, and the daemon does a final catch-up pass on shutdown.
A complete GitHub Actions job. The id-token: write permission is what lets the runner mint an OIDC token. The daemon exchanges it for a HiveMind JWT on its own, so you don’t need a static HIVEMIND_TOKEN secret:
hivemind start launches the daemon in the background. hivemind stop sends SIGTERM and waits up to 10s for a final catch-up pass so the agent’s last message makes it to the dashboard. The if: always() guard makes sure stop runs even when the agent step fails, so failed runs still get uploaded.
For agents that write to a non-default home dir (Codex, custom Cursor paths), set the home env var on the Start hivemind daemon step. The daemon inherits it and watches the right place:
Lighter alternative: import after the run
If you don’t want a background daemon,hivemind import --all uploads everything in one shot at the end of the job:
--allow-empty), so the step itself is the assertion.
The same pattern works outside CI. Drop
hivemind import --since 1h into a shell wrapper or a cron entry. It’s idempotent, so re-running is safe.
Pattern B: Long-running container (Docker sidecar)
When the agent runs continuously and you want sessions to stream live, run hivemind as a sidecar. It watches the agent’s session directory read-only and uploads on a timer. The agent writes JSONL into a shared named volume. The sidecar reads the same volume and uploads:
The 30-second
stop_grace_period matters: it gives the sidecar time to flush the final upload batch when Compose tears the stack down.
Pattern C: GitHub Copilot cloud agent (MCP server)
For runners where you don’t control the loop, like the GitHub Copilot cloud agent, register hivemind as an MCP server. Copilot’s runtime spawns it at session start and tears it down at session end, so the daemon’s lifecycle matches the agent’s.1. Add the MCP server
In repo Settings > Copilot > MCP configuration:hivemindhas to be onPATH, because Copilot resolves thecommandvalue againstPATH.- The OIDC request env vars have to be re-exported under the
COPILOT_MCP_prefix. Copilot only forwards env vars with that prefix into MCP subprocesses, so the daemon won’t see GitHub’sACTIONS_ID_TOKEN_REQUEST_*env vars unless you copy them over.
.github/workflows/copilot-setup-steps.yml, which Copilot runs automatically before launching the agent:
COPILOT_MCP_HIVEMIND_TOKEN under Settings > Environments > copilot, and reference it from the MCP config (Copilot requires the COPILOT_MCP_ prefix on every secret):
2. Flush on shutdown
Copilot kills the MCP subprocess when the agent finishes, so the daemon may have entries it hasn’t uploaded yet. Add a session-end hook that callshivemind flush to drain those before the runner is torn down. Drop this at .github/hooks/hooks.json:
hivemind flush blocks until in-flight uploads finish, or until the 60s timeout. The || true keeps a flush error from failing the agent.