# GitHub accounts: how the agent authenticates How the agent's `gh` and `git push` authenticate, and how the GitHub notification poller works. What an operator does to give an agent an account is in [GitHub accounts](../../2-handbook/external-accounts/github.md). ## How the agent uses it When enabled, the container gets: - **A `gh` wrapper** on `PATH` (shadowing the raw `gh`) that exports `GH_TOKEN` from the token file at invocation, then execs real `gh`. So `gh pr create`, `gh api …`, etc. just work — `gh` derives the identity from the token. - **A git credential helper** (`git-credential-hive-github`), wired via a host-scoped `/etc/gitconfig` entry for `https://github.com`, so `git push https://github.com//` authenticates as `x-access-token` + the PAT (GitHub ignores the username for PAT auth). Host-scoped, so it never touches the forge (`localhost:3000`) or any other remote. Both scripts read the token from `/github-token` **at invocation time**, so a PAT written (or rotated) mid-session takes effect immediately — no container rebuild or restart. Until the file exists, `gh` / `git push` fail unauthenticated. The build bakes the token path into the scripts (rather than reading it from an env var), because claude's Bash tool runs in a minimal environment that wouldn't carry one. ## Notifications `hive-github-notify` polls github.com for the agent, turning each unread notification thread into a todo. it's a **separate binary and a separate systemd unit** from the internal forge's poller (`hive-forge-notify`, see [the forge notification poller](forge-notify.md)), installed by `nix/agent-modules/github.nix` under `services.hyperhive.agent.github.enable`. Both binaries ship from the one `hive-forge-notify` derivation, so the unit is a second `ExecStart` path, not a new package. Two units rather than one daemon with two loops, because it puts the decision in nix: a hive built without this module has **no github poller in its closure at all**, which is what makes GitHub access separable rather than merely switched off. it's also why this isn't a cargo feature — a feature would unify across the workspace and cost every crate its build cache. At runtime the poller needs the PAT above. No PAT, no polling: the unit logs why and exits 0, which is why it's `Restart = on-failure` and never `always` — a clean exit on a PAT-less agent must not become a restart loop. The poller waits about ten minutes for the PAT to appear before it gives up. A path unit (`hive-github-notify.path`) starts the poller again when `/github-token` changes, so a PAT that lands later still enables polling. Once polling, it re-reads the file on every tick, so it picks up a rotated PAT on the next tick without a restart. Forgejo models its notifications API on GitHub's, so one tolerant parse serves both: `id`, `repository.full_name`, `subject {title,url,latest_comment_url}` and `updated_at` line up field for field. Lenient deserializers absorb the two real differences — GitHub sends the thread id as a _string_ where Forgejo sends a number, and says `PullRequest` where Forgejo says `Pull`. `hive-github-notify` prefixes todo keys `gh:` so a github thread id can't collide with a forge one. Two host differences worth knowing before touching this code: - **Auth scheme, not just value.** Forgejo takes `Authorization: token `; GitHub wants `Bearer ` plus `Accept: application/vnd.github+json`, `X-GitHub-Api-Version` and a `User-Agent`. Sending Forgejo's form to GitHub doesn't error — it authenticates as _nobody_ and silently drops to the unauthenticated rate limit. The cheap way to tell the two apart is the rate-limit header: `x-ratelimit-remaining` near 5000 is an authenticated user, near 60 is anonymous. - **GitHub sets the cadence.** It returns `X-Poll-Interval` (60s in practice, slower than the poller's own tick) and rate-limits callers who ignore it, so the loop re-arms to the server's interval whenever that's _slower_ than the poller's. A hint faster than the poller's own tick isn't a reason to poll harder. ⚠️ **This needs the `notifications` scope on the PAT.** A token minted for `gh` + `git push` typically carries `repo` only, which is enough to push and open PRs but **not** to read the notification stream (nor to mark a thread read, which is the same scope). A PAT without it doesn't break anything: the poller logs the refusal and stays quiet, and the agent never gets GitHub wakes. If an agent's GitHub notifications never arrive, check the token's scopes first — the symptom is silence, not an error.