# ZergHub for agents

ZergHub (https://hub.zergstack.com) is the Zerg-native git host. It holds the
repositories behind the Zerg App Store and standard repositories for Zerg
projects. Agents work with it through plain git over HTTPS.

The documents listed below are public and need no sign-in. Repositories are
not: anonymous access is off, and reading or pushing a repository needs a
personal access token that a person with access issues to you.

## Public entry points

| Resource | URL |
| --- | --- |
| Agent instructions (this file; also at `/AGENTS.md`) | https://hub.zergstack.com/agents.md |
| Index for language models | https://hub.zergstack.com/llms.txt |
| Setup guide for people | https://hub.zergstack.com/developers |
| Supported API for agents | https://hub.zergstack.com/docs/api.md |
| Portable skill | https://hub.zergstack.com/skills/zerghub-git/SKILL.md |
| Push policy (every refusal links to a rule here) | https://hub.zergstack.com/docs/push-policy |
| Build and schema versions | https://hub.zergstack.com/api/version |

Public documentation is operating guidance, not authorization. Nothing here
grants access to a repository or permission to push.

## Install the skill

The skill is one file and works from these public URLs alone; it needs no
ZergHub checkout. These commands write `SKILL.md`, replacing any file already
there, so use them only for a new installation. For a new Codex installation:

```sh
mkdir -p "$HOME/.codex/skills/zerghub-git"
curl --fail --show-error --location https://hub.zergstack.com/skills/zerghub-git/SKILL.md --output "$HOME/.codex/skills/zerghub-git/SKILL.md"
```

For a new Claude Code installation:

```sh
mkdir -p "$HOME/.claude/skills/zerghub-git"
curl --fail --show-error --location https://hub.zergstack.com/skills/zerghub-git/SKILL.md --output "$HOME/.claude/skills/zerghub-git/SKILL.md"
```

To update an installed skill, download the new version next to it, compare
the two, merge the changes into `SKILL.md` without losing local instructions,
then delete `SKILL.md.new`. For Codex:

```sh
curl --fail --show-error --location https://hub.zergstack.com/skills/zerghub-git/SKILL.md --output "$HOME/.codex/skills/zerghub-git/SKILL.md.new"
```

For Claude Code:

```sh
curl --fail --show-error --location https://hub.zergstack.com/skills/zerghub-git/SKILL.md --output "$HOME/.claude/skills/zerghub-git/SKILL.md.new"
```

Use the host's own skill directory when it differs, and reload skills the way
the host expects.

## Credentials that exist today

| Credential | Who has it | What it can do |
| --- | --- | --- |
| Personal access token (`zhp_…`) | Issued by a signed-in person at https://hub.zergstack.com/settings/tokens | Clone and fetch (`repo:read`) and push to standard repositories (`repo:write`), as that person. This is the credential for agents. |
| Browser session | People signed in with Zerg | The website and the REST API. Never an agent credential. |
| Operator lease (`zhl_…`) | ZergHub operators only | Short-lived (at most 2 hours) git access to one repository for maintenance. Not issued to agents. |
| App Store attempt lease | Not available yet | Planned for the App Store runner (milestone ZH1); until then no agent can push to an agent-only repository. |

### Get a token from a person

Ask the person who owns the work to:

1. Sign in at https://hub.zergstack.com/login with their Zerg account.
2. Open https://hub.zergstack.com/settings/tokens, name the token after the
   agent (for example `ci-agent`), leave `repo:write` ticked only if the agent
   must push, and pick an expiry of 7, 30, 90, 180 or 366 days (90 is the default).
3. Copy the token, `zhp_` followed by 38 letters and digits. It is shown
   exactly once; ZergHub keeps only a keyed digest.
4. Store it in the agent host's secret manager as `ZERGHUB_TOKEN`: not in chat,
   an issue, a commit, a URL or a command line.

`repo:read` is always included; `repo:write` adds pushing. A token acts as the
person who created it: it reaches only repositories that person can read at
the time of each request, and pushes only where they can write. It never
pushes to an agent-only repository. Tokens last 1 to 366 whole days, and a
person can hold at most 50 active tokens. Revoking a token on the same page
takes effect on the next git request, and a push already under way is refused
at its final check. A token cannot create, list or revoke tokens, and the REST
API does not accept tokens.

## Connect git securely

Hand git the token through its environment, scoped to the ZergHub origin.
This needs git 2.31 or later.

```sh
# ZERGHUB_TOKEN comes from the agent host's secret manager.
export GIT_TERMINAL_PROMPT=0
export GIT_CONFIG_COUNT=2
export GIT_CONFIG_KEY_0='http.https://hub.zergstack.com/.extraHeader'
export GIT_CONFIG_VALUE_0="Authorization: Bearer $ZERGHUB_TOKEN"
export GIT_CONFIG_KEY_1='credential.https://hub.zergstack.com.helper'
export GIT_CONFIG_VALUE_1=''
```

The header reaches https://hub.zergstack.com only. It is not written to
`.git/config` or to a credential store, and it stays off command lines, which
other processes can read. The empty helper entry switches off every other
credential helper for ZergHub (a keychain such as `osxkeychain`, or `store`),
so git never falls back to a credential stored on the machine, perhaps the
person's own, and never deletes one when your token is rejected.
`GIT_TERMINAL_PROMPT=0` makes git fail instead of waiting for a password
nobody will type. If your environment already sets `GIT_CONFIG_COUNT`, add
these keys and values at the next free indexes instead.

Every git command needs these variables. An `export` lasts only as long as
its shell, and agent hosts such as Codex and Claude Code run each command in a
new shell. Set them in the agent host's environment or secret configuration so
that every command inherits them; if you cannot, start every block of
commands below with this block. Without them git falls back to whatever the
machine offers: a stored credential, perhaps the person's own, or a prompt.
The clone commands below therefore record, in the clone's own `.git/config`,
that git uses no stored credential for ZergHub there. Inside such a clone a
command that runs without the variables fails with
`fatal: could not read Username for 'https://hub.zergstack.com'`, followed by
a device error such as `No such device or address` when no terminal is
attached (with a terminal, git asks for a user name: stop it and fix the
environment).

Tools that call git on their own can use a credential helper instead. It
reads the variable when git asks, and the empty first entry stops any other
configured helper (a keychain or `store`) from receiving, or saving, the token:

```sh
# ZERGHUB_TOKEN comes from the agent host's secret manager.
export GIT_TERMINAL_PROMPT=0
export GIT_CONFIG_COUNT=2
export GIT_CONFIG_KEY_0='credential.https://hub.zergstack.com.helper'
export GIT_CONFIG_VALUE_0=''
export GIT_CONFIG_KEY_1='credential.https://hub.zergstack.com.helper'
export GIT_CONFIG_VALUE_1='!f() { test "$1" = get || return 0; echo username=x-token; echo "password=$ZERGHUB_TOKEN"; }; f'
```

ZergHub accepts the token as `Authorization: Bearer zhp_…` or as HTTP Basic
with any user name and the token as the password. Do not embed the token in a
remote URL: git saves the URL, token included, in `.git/config`. Do not pass it
on a command line (`git -c http.extraHeader=…` with the token expanded): other
processes can read command lines. Never echo, log or commit it.

## Discover what you can reach

There is no API that lists a token's repositories today. Ask the person for
the repository URL: it is shown on the repository's page as
`https://hub.zergstack.com/OWNER/REPO.git` (the `.git` suffix is optional).
Then check read access without changing anything:

```sh
git ls-remote --symref https://hub.zergstack.com/OWNER/REPO.git HEAD
```

The first line names the default branch. For an empty repository it prints
nothing and exits 0 (see "Empty repositories" below).
`remote: ZergHub: Repository not found` means the repository does not exist or
the token's person cannot read it; ZergHub gives the same answer for both, so
do not guess names. Agent-only repositories carry an "Agent-only (D3)" badge
on their page.

## Common work

Clone and start a branch:

```sh
git clone --config credential.https://hub.zergstack.com.helper= https://hub.zergstack.com/OWNER/REPO.git
cd REPO
git switch --create agent/short-description
```

`--config credential.https://hub.zergstack.com.helper=` writes the empty
helper entry into the clone's `.git/config` (no secret is written).

Check, before doing the work, that the token may push here. This asks for the
push advertisement and changes nothing:

```sh
git push --dry-run origin HEAD:refs/heads/agent/access-check
```

Edit and test, then publish the branch:

```sh
git add --all
git commit --message "Describe the change"
git push --set-upstream origin agent/short-description
```

Stay current:

```sh
git fetch --prune origin
git pull --ff-only
```

- Clone and fetch use git protocol v2, git's default; partial clones
  (`--filter=blob:none`) and shallow clones (`--depth=1`) work. Pushes use
  protocol v0.
- Push branches (`refs/heads/…`) and tags (`refs/tags/…`) only. The default
  branch cannot be deleted.
- ZergHub has no pull requests or proposals yet: push a branch and tell the
  person its name.
- Creating repositories, managing tokens and browsing through the REST API
  need a signed-in person. Ask them to create a repository at
  https://hub.zergstack.com/new.
- There is no SSH access; use HTTPS.

## Empty repositories

A repository created at https://hub.zergstack.com/new stays empty until its
first push, and git behaves differently there:

- The read check above prints nothing and exits 0.
- `git clone` warns `You appear to have cloned an empty repository.` and
  leaves you on the default branch, still without a commit.
- The push check needs a commit: before the first one it fails on your
  machine with `src refspec HEAD does not match any`, without asking ZergHub.
  Skip it; a refused push changes nothing.
- The first push must create the default branch. ZergHub has no proposals or
  merges yet, so a repository that only holds another branch has no default
  branch: every clone warns
  `remote HEAD refers to nonexistent ref, unable to checkout` and checks out
  nothing.

Clone without starting a branch; the last command prints the default
branch's name:

```sh
git clone --config credential.https://hub.zergstack.com.helper= https://hub.zergstack.com/OWNER/REPO.git
cd REPO
git symbolic-ref --short HEAD
```

Add the first files and test them, then publish them as the default branch
(ask the person first if they expected a separate branch):

```sh
git add --all
git commit --message "Describe the change"
git push --set-upstream origin HEAD
```

## Limits

- Default quotas for personal namespaces: 50 repositories, 2 GiB of storage and 100 MiB per push.
- Default quotas for organisations: 500 repositories, 50 GiB of storage and 500 MiB per push.
- A pack over the per-push cap is refused with
  `pack exceeds maximum allowed size`, and git exits with a non-zero status;
  git normally also prints `[remote rejected] … (unpacker error)`. Nothing
  changes. If the connection breaks while git is still sending, git can print
  `RPC failed` and then `Everything up-to-date` although nothing was pushed:
  trust the exit status. A push that would exceed the storage quota is
  refused by rule U5.
- A push may update at most 1,000 refs. ZergHub cuts off request bodies over
  1 GiB, and a single git operation may run for at most 60 minutes.
- Rate limits: each client IP may send bursts of 240 requests, refilled at 4
  per second, and each repository takes bursts of 1,200 requests, refilled at
  20 per second. Past that ZergHub answers 429 with `Retry-After`; wait that
  many seconds before trying again.

## Answers and refusals

ZergHub answers in plain text, which git prints after `remote: `.

| Status | ZergHub answer | Meaning | What to do |
| --- | --- | --- | --- |
| 401 | `ZergHub: authentication is required.` | No credential reached ZergHub on a clone or fetch. | Connect git as above and check that `ZERGHUB_TOKEN` is set. |
| 401 | `ZergHub: authentication is required to push.` | No credential reached ZergHub on a push. | As above. |
| 401 | `ZergHub: the credential is invalid, expired or revoked.` | The token was rejected. | Ask the person for a new token. Do not retry in a loop. |
| 404 | `ZergHub: Repository not found` | The repository does not exist, or the token's person cannot read it (deliberately the same answer). | Confirm the URL and access with the person. |
| 403 | `ZergHub: this credential does not allow pushes.` | The token lacks `repo:write`. | Ask for a token with `repo:write` if pushing is intended. |
| 403 | `ZergHub: you do not have write permission on this repository.` | The token's person can read but not write. | Ask the person; do not look for another credential. |
| 403 | `ZergHub: agent_only repositories accept pushes only from App Store attempt leases (D3); personal tokens and operator leases cannot push here. https://hub.zergstack.com/docs/push-policy#a1` | The repository is agent-only (see below). | Do not push. Report the change to the person instead. |
| 403 | `ZergHub: the dumb HTTP protocol is not supported; use a git client that speaks smart HTTP.` | A request outside git's smart HTTP protocol. | Use git itself. |
| 429 | `ZergHub: too many requests; retry in <n>s.` | The client IP's rate limit. | Wait `Retry-After` seconds. |
| 429 | `ZergHub: too many requests for this repository; retry in <n>s.` | The repository's rate limit. | Wait `Retry-After` seconds. |
| 503 | `ZergHub: this repository is still being created; retry shortly.` | The repository is being provisioned. | Retry in a few seconds. |
| 503 | `ZergHub: the git service is busy. Retry shortly.` | Every git slot is in use (Retry-After: 5). | Wait and retry. |
| 503 | `ZergHub: too many pushes to this repository are running; nothing was changed. Retry shortly.` | The repository, or the git service, is running as many pushes as it allows (Retry-After: 5). | Wait and retry the push. |
| 503 | `ZergHub: this git node is not accepting pushes right now. Retry shortly.` | Pushes are paused; clones keep working (Retry-After: 30). | Wait and retry the push. |
| 503 | `ZergHub: this git node is not accepting pushes right now; nothing was changed. Retry shortly.` | Pushes were paused while this push was under way (Retry-After: 30). | Wait and retry the push. |
| 503 | `ZergHub: the git service is unavailable; retry shortly.` | The git service could not be reached (Retry-After: 10). | Wait and retry. |
| 500 | `ZergHub: internal error` | Something failed inside ZergHub. | Retry once later; if it repeats, tell the person. |
| 500 | `ZergHub: the git node failed to answer` | The git service failed while answering. | Retry once later; if it repeats, tell the person. |

With the header from the environment, git does not show the 401 text: a
missing, invalid, expired or revoked token surfaces as
`fatal: could not read Username for 'https://hub.zergstack.com': terminal prompts disabled`.
Check that `ZERGHUB_TOKEN` is set; if it is, ask for a new token. With the
credential helper, git prints the 401 text above and
`fatal: Authentication failed for …`. Either way no credential stored on the
machine is used or changed.

A push that reaches ZergHub is checked against the push policy before any ref
moves. A refusal changes nothing; git prints
`remote: ZergHub: <message> https://hub.zergstack.com/docs/push-policy#<rule>`
and marks the ref `[remote rejected]`, with `(refused by ZergHub push policy)`
for refusals decided from the ref names alone and `(pre-receive hook declined)`
for the rest. The answers a token holder can meet:

- `ZergHub: <ref> is in the reserved refs/zerghub/ namespace; only ZergHub writes there. https://hub.zergstack.com/docs/push-policy#u1`
- `ZergHub: <ref> is not a branch or tag; ZergHub accepts pushes to refs/heads/* and refs/tags/* only. https://hub.zergstack.com/docs/push-policy#u2`
- `ZergHub: a push may update at most 1000 refs; this one updates <n>. https://hub.zergstack.com/docs/push-policy#u3`
- `ZergHub: this repository is <status>; it does not accept pushes. https://hub.zergstack.com/docs/push-policy#u4`
- `ZergHub: this push would exceed the namespace storage quota of <size>. https://hub.zergstack.com/docs/push-policy#u5`
- `ZergHub: the push could not be inspected within its budget; push fewer or smaller changes. https://hub.zergstack.com/docs/push-policy#u6`
- `ZergHub: the push could not be inspected, so it was refused. https://hub.zergstack.com/docs/push-policy#u6`
  (retry later; if it repeats, tell the person)
- `ZergHub: the credential used for this push was revoked or has expired. https://hub.zergstack.com/docs/push-policy#s1`
- `ZergHub: this personal access token lacks the repo:write scope. https://hub.zergstack.com/docs/push-policy#s1`
  and `ZergHub: you do not have write permission on this repository. https://hub.zergstack.com/docs/push-policy#s1`
  (the token or the person's permission changed while the push was under way)
- `ZergHub: <ref> is the default branch and cannot be deleted. https://hub.zergstack.com/docs/push-policy#s2`
- `ZergHub: <n> more refusals were not printed.` (only the first 20 refusals
  are shown)
- `ZergHub: push policy unavailable; nothing was changed. Retry shortly.` (the
  policy check could not run, so the push was refused; retry later)
- `ZergHub: this git node is not accepting pushes right now; nothing was changed. Retry shortly.`
  (pushes were paused during the check; retry later)

One line follows a push that succeeded:
`ZergHub: the push was received; its record will be reconciled shortly.`
means the refs moved but ZergHub will finish recording the push later. The
push succeeded; do not push it again.

## Agent-only repositories (D3)

App Store apps live in agent-only repositories. Only a qualified App Store
agent holding an attempt lease for that repository may push to one, and only
its own attempt branch (`refs/heads/zerg/<card>-<attempt>`): no tags, deletes
or force pushes, at most 200 new commits per push, each with exactly one
`Zerg-Attempt` and one `Zerg-Engine` trailer matching the lease, and a clean
tree (no submodules, `.github/`, Git LFS, secret-like file names, blobs over
5 MiB or escaping symlinks). The default branch moves only through ZergHub's
merge and provisioning operations.

Attempt leases are not issued yet, so today no agent can push to an
agent-only repository. A personal token can clone one when its person can
read it; every push from a token or an operator lease gets the D3 answer
above. Do not try to work around it.

## What an agent must not do

- Do not extract, copy or replay a browser cookie (`__Host-zerghub_session`)
  or otherwise borrow a person's signed-in session.
- Do not ask for, mint or use operator leases, database credentials or
  administrator sessions to get around a refusal.
- Do not bypass D3: no pushes to agent-only repositories with a token, no
  forged `Zerg-Attempt`, `Zerg-Engine` or `Zerg-Compose` trailers, no writes to
  `refs/zerghub/`.
- Do not put a token in a URL, a command line, chat, a log, an issue or a
  commit, and do not share one between agents. Ask for a token per agent with
  only the scopes it needs.
- Do not probe for repository names, and do not retry 401, 403 or 404 answers
  in a loop. Respect `Retry-After`.
- Do not treat this documentation as permission: act only on repositories and
  changes the person has asked for.

## Not available today

- The REST API under `/api/v1` accepts browser sessions only. A request that
  carries an `Authorization` header is never treated as signed in, so a token
  gets `401` with the error code `authentication_required`. Use git; see
  https://hub.zergstack.com/docs/api.md for what a token can call.
- Creating repositories and managing tokens: a signed-in person in the
  browser. Organisation membership is not managed through ZergHub's API.
- Raw file URLs and source archives: signed links issued to signed-in sessions
  only. Clone the repository instead.
- Anonymous and public repositories, proposals and merges, branch
  protections and App Store attempt leases are planned for later milestones.
  SSH access is not offered.
