Git
Git hosting for your org: create repos, clone, push, and see what they cost.
Also for this capability: API · CLI · MCP · SDKs
Git hosting for your org: create repos, clone, push, and see what they cost.
| Base URL | https://api.hanzo.ai |
| Operations | 44 |
| Auth | Authorization: Bearer $HANZO_API_KEY |
Specification
HIP-1232 · Git — Repository Hosting — Draft · read the specification →
git is Git hosting for an org: create repos, clone, push, browse, and see
what they cost. It mounts the /v1/git surface of the cloud binary — bare
repositories on disk, the smart-HTTP and SSH transports git clients speak
natively, imports, pulls, mirroring and the browse pages
(apps/git/git.go:1-45). It is implemented in hanzoai/cloud at apps/git.
This HIP states the target surface: everything under /v1/git, with the
host-gated root spellings leaving the binary for the standalone forge that
serves that host in production.
Motivation
The capability claimed four top-level prefixes for one product: its own
/v1/git, an un-host-gated browse tree at /git, and root routes — /,
/explore, /:org/:repo — answered only when the request's Host is the
dedicated git host. That host is served in production by the standalone
forge, a separate process, so the root claims published addresses this
binary never deliverably answers; the manifest walk already refuses a
parameter-first prefix.
Specification
The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted as in RFC 2119.
The stores
The capability owns per-org stores, not one shared file: each org's repo
metadata is its own git.db opened through the org-store seam
(apps/git/git.go:222), beside the repositories themselves — bare git
repos under {DataDir}/git, with an S3-backed storage seam
(apps/git/storage.go) — and one system ssh_keys registry
(apps/git/keystore.go:68), global because an SSH connection presents a
key before any org is known, so auth is a single fingerprint lookup.
The addresses
Every route is under /v1/git: repos CRUD and usage; the smart-HTTP
protocol at /v1/git/:org/:repo/... and, for project-scoped repos,
/v1/git/:org/:project/:repo/... — the path is the only channel a git
client can carry a project in, since it sends no headers; and the browse
pages, folded from /git to /v1/git/explore and
/v1/git/:org/:repo{,/tree,/blob,/commits}, where literal segments outrank
the :org parameter so nothing collides. The router today still carries the
/git tree and the three host-gated root spellings; each pair is a line in
cloud's openapi/misfiled.txt. The browse tree closes by fold; the root
spellings close by deletion — git.hanzo.ai is served in production by the
standalone forge, so removing them changes nothing live and only retires
the single-binary dev forge origin, whose clone address remains
/v1/git/:org/:repo.
Repos, usage, pulls and control operations are typed. The smart-HTTP
operations are declared prose, and cannot be otherwise: their wire is git's
own protocol — pkt-line framing, side-band multiplexing, gzip request
bodies — not JSON, and the document says so where it matters, telling the
consumer this is not an API call to make by hand
(apps/git/smart_http.go:38-45). The browse pages answer server-rendered
HTML and are declared the same way.
Tenancy
The org is the gateway-minted, IAM-validated claim (HIP-0026), with an
optional project sub-scope; every query is scoped by it, so one org can
never read, clone, push to or delete another's repos. On the pack routes the
path also names an org, and it MUST equal the authenticated one
(apps/git/smart_http.go:284). A push is never anonymous. The one read
concession is a repository marked public: fetch-side operations answer
without an authenticated org, push-side never
(apps/git/smart_http.go:292). Over SSH, the presented public key's
fingerprint resolves through the global registry to the org whose scope the
pack machinery then runs under — the same boundary the HTTP path derives
from the header (apps/git/ssh.go:27-33).
Free, in those words
The capability is free: the plugin declares Price: cloud.Free
(plugin/git/main.go:21), and no handler gates or meters spend. Usage is
measured, not charged: every repo's size is re-measured on create and after
each push, exposed per-repo and in total at /v1/git/usage, and each
measurement emits a git.usage log line a metering consumer can bill on
(apps/git/git.go:696).
Events, observability, stage
The capability publishes no events on the bus, so a customer's webhooks
receive nothing from it. A landed receive-pack emits push.landed on the
in-process lifecycle stream, and the notification reactor delivers
push.landed, deploy.live and deploy.failed to the channels an org has
subscribed, durably, deduplicated per event (apps/git/notify.go:48-88).
Beyond the request span it emits the git.usage line and structured logs
only. Its stage is ga: the manifest row carries no stage field, and
absent means ga (HIP-0139 §8).
The push-to-build door is not here. The deploy trigger's one registrant is the platform process, so the forge delivers pushes to platform's hook (HIP-1230); a receiver in this process signed, accepted, answered 204 and built nothing, which is why the address moved.
Upstreams
go-git/go-gitv5 (Apache-2.0) — repository init and the read path, confined to one file so nothing go-git-shaped escapes it (apps/git/gitbackend.go:3-12).go-git/go-billyv5 (Apache-2.0) — the filesystem abstraction under it.golang.org/x/crypto/ssh(BSD-3-Clause) — the SSH transport.- the system
gitbinary (GPL-2.0) — executed as a subprocess for stateless-RPC pack streaming (apps/git/gitexec.go), so multi-GB packs stream through stdin/stdout and never land in this process's memory; invoked, never linked. - the smart-HTTP framing patterns are ported from the upstream forge's
routers/web/repo/githttp.go(MIT) (apps/git/smart_http.go:30).
Rationale
The alternative to deleting the root spellings is keeping a second clone address alive in a binary that does not serve the host it is gated on — an address the manifest walk refuses and production traffic never reaches. The alternative to declaring the pack operations is typing them, which would publish JSON schemas for a wire that is not JSON and hand every generated SDK twelve methods that corrupt a clone when called.
Security Considerations
The wrong implementation leaks source code. A pack route that trusts the path org instead of comparing it to the authenticated one serves another tenant's repository to anyone who can spell its name; the path-vs-identity guard is the boundary. The SSH key registry is the authentication for the SSH transport, so writing to it is minting access: key registration is org-scoped and the fingerprint lookup is global only because it must run before identity exists. Pack streaming through the CLI is also the memory bound — buffering a push in-process would let one crafted pack exhaust the binary serving every org.
Four surfaces
| Surface | Reaches this capability as | Coverage |
|---|---|---|
| REST | git at its own prefix | 44 operations |
| CLI | hanzo git … | 44 of 44 |
| SDK | GitApi in every published client | 40 of 44 — the clients are generated at their own release |
| MCP | tool git on https://api.hanzo.ai/v1/mcp | 56 operations, 11 under the document's own id — ask describe for the rest |
Quickstart
export HANZO_API_KEY=sk-... # console.hanzo.ai → API keysThen the first call — a read that needs nothing but the key. GET /v1/git, operation get_git:
hanzo git listimport { Configuration, GitApi } from 'hanzoai';
const api = new GitApi(new Configuration({ accessToken: process.env.HANZO_API_KEY }));
const { data } = await api.getGit();from hanzoai.cloud import ApiClient, Configuration
from hanzoai.cloud.api import GitApi
client = ApiClient(Configuration(access_token=os.environ["HANZO_API_KEY"]))
result = GitApi(client).get_git()cfg := cloud.NewConfiguration()
cfg.AddDefaultHeader("Authorization", "Bearer "+os.Getenv("HANZO_API_KEY"))
client := cloud.NewAPIClient(cfg)
resp, _, err := client.GitAPI.GetGit(context.Background()).Execute()
if err != nil {
return err
}use hanzo_cloud::apis::{configuration::Configuration, git_api};
let mut cfg = Configuration::new();
cfg.bearer_access_token = std::env::var("HANZO_API_KEY").ok();
let result = git_api::get_git(&cfg, Default::default()).await?;import ai.hanzo.cloud.ApiClient;
import ai.hanzo.cloud.api.GitApi;
ApiClient client = new ApiClient();
client.setRequestInterceptor(b -> b.header("Authorization", "Bearer " + System.getenv("HANZO_API_KEY")));
var result = new GitApi(client).getGit();curl https://api.hanzo.ai/v1/git \
-H "Authorization: Bearer $HANZO_API_KEY"Tool git, op get_git — POST the JSON-RPC envelope to https://api.hanzo.ai/v1/mcp.
curl -X POST https://api.hanzo.ai/v1/mcp \
-H "Authorization: Bearer $HANZO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "git",
"arguments": {
"op": "get_git",
"input": {}
}
}
}'Endpoints
| Endpoint | What it does |
|---|---|
POST /v1/git/{org}/{project}/{repo}/git-receive-pack | Accept a push, and turn it into a build |
POST /v1/git/{org}/{project}/{repo}/git-upload-pack | Serve a clone or fetch |
GET /v1/git/{org}/{project}/{repo}/info/refs | Advertise a repository's refs to a git client |
GET /v1/git/{org}/{repo}/commits | Read a repository's commit log |
POST /v1/git/{org}/{repo}/git-receive-pack | Accept a push, and turn it into a build |
POST /v1/git/{org}/{repo}/git-upload-pack | Serve a clone or fetch |
GET /v1/git/{org}/{repo}/info/refs | Advertise a repository's refs to a git client |
GET /v1/git/{org}/{repo} | Open a repository's home page |
GET /v1/git/explore | Discover public repositories across every org |
DELETE /v1/git/keys/{id} | Removes a registered SSH key, scoped to the caller's org: an org can only delete its own, and a key id it does not own is not found. |
GET /v1/git/keys | Returns the SSH public keys registered to the caller's org — the keys that authenticate git clone git@<host>:<org>/<repo>.git. |
POST /v1/git/keys | Registers an SSH public key so it can authenticate git clone git@<host>:<org>/<repo>.git for the caller's org. |
GET /v1/git/repos/{name}/blob | Returns one file's bytes at one revision. |
GET /v1/git/repos/{name}/commits | Walks a ref's history newest first, or one path's history when a path is given. |
GET /v1/git/repos/{name}/files | Returns every file a glob selects at one revision, WITH its bytes and the revision they came from. |
POST /v1/git/repos/{name}/gc | Repacks a repo into one bitmapped pack and rewrites its commit-graph, so the next clone reuses the bitmap instead of walking the whole object graph. |
POST /v1/git/repos/{name}/mirror | Imports an external git repository into the caller's repo, provisioning it on first use. |
DELETE /v1/git/repos/{name}/mirrors/{id} | Removes one outbound mirror target; later pushes stop being forwarded to it. |
GET /v1/git/repos/{name}/mirrors | Returns a repo's outbound mirror targets — the downstream remotes the mirror reactor pushes to whenever a push lands here. |
POST /v1/git/repos/{name}/mirrors | Registers a downstream remote the repo's advanced refs are pushed to whenever a push lands here. |
POST /v1/git/repos/{name}/pulls/{number}/merge | Merges an open pull request by FAST-FORWARDING base to head, and answers the proposal in its merged state with the revision base now points at. |
GET /v1/git/repos/{name}/pulls/{number} | Returns one pull request by its per-repo number. |
GET /v1/git/repos/{name}/pulls | Returns a repo's pull requests, newest number first — what is waiting to be reviewed, and what has already landed. |
POST /v1/git/repos/{name}/pulls | Proposes a branch for merging and returns it with its number. |
POST /v1/git/repos/{name}/push | Lands a set of files as one commit without a git client — the hanzo.app builder's push. |
GET /v1/git/repos/{name}/readme | Returns the README at the tree root as plain text — unrendered, so the caller decides how to present it. |
GET /v1/git/repos/{name}/refs | Lists a repo's branches, tags and default branch — what a branch picker needs in one call. |
DELETE /v1/git/repos/{name}/subscriptions/{id} | Removes one Slack subscription from a repo; the notifier stops posting that repo's events to that channel. |
GET /v1/git/repos/{name}/subscriptions | Returns a repo's Slack subscriptions — which channels the lifecycle notifier posts this repo's push and deploy events to. |
POST /v1/git/repos/{name}/subscriptions | Binds a Slack channel to a repo, so the lifecycle notifier posts that repo's push and deploy events there. |
GET /v1/git/repos/{name}/tree | Lists the immediate children of one directory at one revision, directories before files. |
GET /v1/git/repos/{name} | Returns one repo with its live ref state: every branch name and the resolved HEAD commit. |
PATCH /v1/git/repos/{name} | Flips a repo's public bit, the one mutable repo setting today. |
DELETE /v1/git/repos/{name} | Removes a repo's metadata and purges its storage. |
GET /v1/git/repos | Returns the repos in the caller's scope, most recently updated first. |
POST /v1/git/repos | Provisions an empty bare repository in the caller's scope and returns it with its clone URLs. |
GET /v1/git/usage | Returns per-repo and total storage bytes for the caller's org — the queryable, per-tenant number commerce and o11y meter on. |
POST /v1/git/webhook | Retired — forge pushes build via platform.hanzo.ai |
POST /v1/git/zap/createRepo | Create a repository over the ZAP transport |
POST /v1/git/zap/deleteRepo | Delete a repository over the ZAP transport |
POST /v1/git/zap/getRepo | Read one repository over the ZAP transport |
POST /v1/git/zap/listRepos | List your repositories over the ZAP transport |
POST /v1/git/zap/usage | Report your org's git storage footprint over the ZAP transport |
GET /v1/git | Browse your org's repositories |
How is this guide?