| 1 | --- |
| 2 | title: Git |
| 3 | description: Remotes, credentials, private repositories, deploy keys and limits. |
| 4 | --- |
| 5 | |
| 6 | g1t speaks git's smart HTTP protocol. Any git client works. |
| 7 | |
| 8 | ## Remotes |
| 9 | |
| 10 | ```text |
| 11 | https://g1t.sh/<workspace>/<repo>.git |
| 12 | ``` |
| 13 | |
| 14 | Public repositories can be cloned without signing in: |
| 15 | |
| 16 | ```sh |
| 17 | git clone https://g1t.sh/flagon-io/g1t.git |
| 18 | ``` |
| 19 | |
| 20 | If the workspace is [renamed](/guides/workspaces/#rename-a-workspace), the |
| 21 | old remote redirects to the new one for 90 days. Git follows the redirect |
| 22 | and warns about it; point the remote at the new address: |
| 23 | |
| 24 | ```sh |
| 25 | git remote set-url origin https://g1t.sh/<new-workspace>/<repo>.git |
| 26 | ``` |
| 27 | |
| 28 | ## Authentication |
| 29 | |
| 30 | Pushing, and reading private repositories, needs credentials. Use your |
| 31 | username, and as the password either your account password or an |
| 32 | [access token](https://g1t.sh/settings/tokens). Tokens are recommended: they can be revoked |
| 33 | individually and they also work for the API. With |
| 34 | [two-factor authentication](/guides/authentication/#two-factor-authentication) |
| 35 | on, your password is not accepted for git, since it would get past your |
| 36 | second factor: use a token, with Code at read and write to push. |
| 37 | |
| 38 | To avoid typing it each time, let git store it: |
| 39 | |
| 40 | ```sh |
| 41 | git config --global credential.helper store |
| 42 | ``` |
| 43 | |
| 44 | ## Creating a repository by pushing |
| 45 | |
| 46 | Pushing to a repository that does not exist, in a workspace you belong to, |
| 47 | creates it as a private repository, so nothing pushed by mistake is |
| 48 | published. To make it public, see |
| 49 | [change who can see a repository](/guides/managing-repositories/#change-who-can-see-a-repository). |
| 50 | |
| 51 | ```sh |
| 52 | git push https://g1t.sh/<workspace>/new-repo.git main |
| 53 | ``` |
| 54 | |
| 55 | ## Private repositories |
| 56 | |
| 57 | A private repository is visible only to people with a |
| 58 | [role](/guides/access-and-roles/) on it. Cloning and fetching need |
| 59 | Read, and pushing needs Write. To |
| 60 | everyone else it looks exactly like a repository that does not exist, both |
| 61 | on the site and to git. |
| 62 | |
| 63 | On the site, an address you cannot see gives the same page either way, with |
| 64 | status 404: |
| 65 | |
| 66 | | You are | The page says | |
| 67 | | --- | --- | |
| 68 | | Signed out | **Nothing here**: this page doesn't exist, or it's private; sign in if it's yours. **Sign in** brings you back to the same address. | |
| 69 | | Signed in | **Nothing here**: this page doesn't exist, or you don't have access to it, with which account you are signed in as and a link to switch account. If you should have access, ask someone with the Admin role on it to add you. | |
| 70 | |
| 71 | Issues, pull requests and workspace pages work the same way. The sidebar |
| 72 | does not open the project or workspace the address names, so nothing on |
| 73 | the page hints at whether it exists; a missing file, commit or issue in a |
| 74 | project you can see keeps that project's sidebar. A profile that does not |
| 75 | exist says **No one on g1t goes by that name**, since profiles are public. |
| 76 | |
| 77 | ## Download a ZIP |
| 78 | |
| 79 | On a repository's **Files** page, **Code** → **Download ZIP** downloads the |
| 80 | branch shown as one zip, its files in a folder named `<repo>-<branch>`. It |
| 81 | works for anyone who can see the repository, and for any branch, tag or |
| 82 | commit at `g1t.sh/<workspace>/<repo>/archive/<ref>.zip`, or as a gzipped |
| 83 | tar at `…/archive/<ref>.tar.gz`. Executable files and symlinks stay as |
| 84 | they are in git. An archive holds the files, not the history; clone for |
| 85 | that. A repository with more than 10,000 files or over 24 MB is too large |
| 86 | to download this way, so clone it instead. |
| 87 | |
| 88 | The API serves the same archives at GitHub's addresses, |
| 89 | [`GET /repos/{owner}/{repo}/tarball/{ref}`](/reference/api/repositories/download-tarball-archive/) |
| 90 | and [`/zipball/{ref}`](/reference/api/repositories/download-zipball-archive/): |
| 91 | each answers with a redirect to a link that needs no token, good for 10 |
| 92 | minutes, for the commit the ref names. Its files are in one folder named |
| 93 | as GitHub names it, `<workspace>-<repo>-<first 7 of the commit>`, which is |
| 94 | what `actions/checkout` expects when it downloads a repository in a job |
| 95 | container without git. With a token, a private repository's work too: |
| 96 | |
| 97 | ```sh |
| 98 | curl -L -H "Authorization: Bearer $G1T_TOKEN" -o hello.tar.gz \ |
| 99 | https://api.g1t.sh/repos/flagon-io/hello/tarball/main |
| 100 | ``` |
| 101 | |
| 102 | ## Raw files |
| 103 | |
| 104 | **Raw**, above a file on its page, opens the file as it is, with nothing |
| 105 | around it. Any file is at: |
| 106 | |
| 107 | ```text |
| 108 | https://g1t.sh/<workspace>/<repo>/raw/<branch, tag or commit>/<path> |
| 109 | ``` |
| 110 | |
| 111 | That address sends you on to the commit the branch or tag names now, on |
| 112 | g1t's file host: |
| 113 | |
| 114 | ```text |
| 115 | https://g1tusercontent.com/<workspace>/<repo>/raw/<commit>/<path> |
| 116 | ``` |
| 117 | |
| 118 | g1tusercontent.com is a site of its own so that nothing in a repository |
| 119 | can reach your g1t.sh session: it never receives g1t.sh's cookies, and a |
| 120 | file opened there cannot run script. Images, video, audio and PDFs are |
| 121 | served as themselves; any other text, HTML, SVG source, XML and |
| 122 | JavaScript included, as plain text; anything else as a download. A file |
| 123 | is served up to 10 MB; clone the repository for larger ones. |
| 124 | |
| 125 | - **Public repositories.** The address works for anyone, and an address |
| 126 | at a commit can be kept for good. |
| 127 | - **Private repositories.** The address carries a `token` that g1t.sh |
| 128 | makes for someone who can read the repository. It is good for that one |
| 129 | file for an hour or two; after that, open the file on g1t.sh again for a |
| 130 | new address. Revoking someone's access does not end an address they |
| 131 | already have before then. |
| 132 | |
| 133 | Images in a file's page show from its raw address, and so do pictures in a |
| 134 | README that name a file in the repository (``, |
| 135 | relative to the README's folder, or `/docs/diagram.png` from the |
| 136 | repository's root): they show the file at the commit the page shows. |
| 137 | Uploaded avatars are on the same host, at |
| 138 | `https://g1tusercontent.com/avatars/<sha256>`. |
| 139 | |
| 140 | ## Browsing without an account |
| 141 | |
| 142 | Public projects, Explore, Search and profiles are open to everyone, in the |
| 143 | same sidebar members use. Signed out, the sidebar has Explore and Search, |
| 144 | and in a project its Code, Issues, Pull requests, Agents, Workflows and |
| 145 | Deployments; pages only people with a role on the repository see, such as |
| 146 | Security and Settings, are left out. **Sign in** and **Sign up** sit at the bottom, and both bring you |
| 147 | back to the page you were on. |
| 148 | |
| 149 | ## Protected branches |
| 150 | |
| 151 | A repository protects its branches and tags with [rulesets](/guides/rules/), |
| 152 | under **Settings → Rules**. A push that breaks a rule is refused for |
| 153 | everyone, whatever their role, and for agents, unless a ruleset lists them |
| 154 | as able to bypass it. Git prints which ruleset and rule refused it, and how |
| 155 | to fix it: |
| 156 | |
| 157 | ```text |
| 158 | remote: error: rules for refs/heads/main declined this push: |
| 159 | remote: - Changes to main must be made through a pull request. [ruleset "Protect main", pull_request] |
| 160 | remote: Push a branch, open a pull request into main, and merge it. |
| 161 | ! [remote rejected] main -> main (declined by ruleset "Protect main" (pull_request)) |
| 162 | ``` |
| 163 | |
| 164 | With **Require a pull request before merging**, changes reach a branch only |
| 165 | by merging a pull request. Creating the branch, such as the first push to |
| 166 | an empty repository, is still allowed. Rulesets also block force pushes and |
| 167 | deletions, restrict who creates branches and tags, check commit messages, |
| 168 | signatures and the files a push changes, and set what a merge needs. See |
| 169 | [rules](/guides/rules/). |
| 170 | |
| 171 | ## Branches |
| 172 | |
| 173 | Push any branch to a repository you can write to, and open a |
| 174 | [pull request](/concepts/overview/#pull-requests) from it on the |
| 175 | repository's **Pull requests** tab. |
| 176 | |
| 177 | ```sh |
| 178 | git switch -c my-change |
| 179 | git push origin my-change |
| 180 | ``` |
| 181 | |
| 182 | ### The Branches page |
| 183 | |
| 184 | A repository's **Branches** tab, `g1t.sh/<workspace>/<repo>/branches`, lists |
| 185 | its branches under five tabs, kept in the address as `?tab=`: |
| 186 | |
| 187 | | Tab | What it lists | |
| 188 | | --- | --- | |
| 189 | | **Overview** | The default branch, then the first five of each of the others below that has any: yours, active and stale, each with a link to the rest. | |
| 190 | | **Yours** (`?tab=yours`) | Branches whose latest commit is yours. | |
| 191 | | **Active** (`?tab=active`) | Branches with a commit in the last 90 days. | |
| 192 | | **Stale** (`?tab=stale`) | Branches with no commit in the last 90 days. | |
| 193 | | **All** (`?tab=all`) | Every branch but the default, newest commit first. | |
| 194 | |
| 195 | **Search branches** narrows any tab by name (`?q=`). Each branch shows: |
| 196 | |
| 197 | | Column | What it says | |
| 198 | | --- | --- | |
| 199 | | **Branch** | Its name, linked to its files, with a button that copies it; a globe when it has a preview. | |
| 200 | | **Updated** | Who made its latest commit, and when, linked to the commit. | |
| 201 | | **Check status** | All of its latest commit's checks at once: a check when they passed, a cross when one failed, a dot while any runs. Choose it to see each. | |
| 202 | | **Behind / Ahead** | How many commits the default branch has that it lacks, and the other way round, with bars drawn against the largest count shown. The counts are exact, merges included. g1t reads up to 1,000 commits of each history to find where the two meet; a branch that left the default branch further back than that shows no counts. | |
| 203 | | **Pull request** | Its open pull request, else its newest merged or closed one, with its state and number. | |
| 204 | |
| 205 | On a phone, the columns fold under the name. Up to 60 branches are read for |
| 206 | a page, those with an open pull request first; search finds the rest. |
| 207 | |
| 208 | People with the Write role or higher can change branches from the page: |
| 209 | |
| 210 | - **New branch** makes a branch from another branch, or from a tag or a |
| 211 | commit, which you type in. |
| 212 | - The trash button deletes a branch. A branch with commits the default |
| 213 | branch lacks, or whose counts could not be read, asks first; a merged |
| 214 | one (nothing ahead, or its pull request merged) goes at once. It stays in |
| 215 | the list as **Deleted**, with **Restore**, which makes it again at the |
| 216 | same commit, until you leave the page. While it is gone, a pull request |
| 217 | from it can be neither updated nor merged. A deletion is listed in the |
| 218 | repository's **Activity** with the commit it pointed to, so it can still |
| 219 | be restored later from the API. |
| 220 | - The **⋯** menu opens a **New pull request** (the compare page against the |
| 221 | default branch), the branch's **Activity**, the **View rules** that hold |
| 222 | for it (Admin), and **Rename branch**. See |
| 223 | [rename a branch](/guides/managing-repositories/#rename-a-branch) for what |
| 224 | renaming does to pull requests, addresses and clones. |
| 225 | |
| 226 | The default branch is never deleted, and renaming it needs Admin. |
| 227 | [Rules](/guides/rules/) hold for every one of these changes: a ruleset that |
| 228 | restricts creating, deleting or renaming a branch refuses it here as it |
| 229 | would a push, and says why. When nothing protects the default branch, |
| 230 | people with the Admin role see a note saying so, with **Protect this |
| 231 | branch**, which starts a [ruleset](/guides/rules/), and **Dismiss**, which |
| 232 | this browser remembers for them and that repository. An archived |
| 233 | repository offers none of these changes. |
| 234 | |
| 235 | From the API, a person's token with `repo:write` can do the same: |
| 236 | |
| 237 | | Route | MCP | What it does | |
| 238 | | --- | --- | --- | |
| 239 | | [`POST /repos/{owner}/{name}/branches`](/reference/api/repositories/create-branch/) | `repository` `create_branch` | Makes branch `name` at `from` (a branch, tag or commit; the default branch when left out). Answers with its `name` and `hash`. | |
| 240 | | [`DELETE /repos/{owner}/{name}/branches/{branch}`](/reference/api/repositories/delete-branch/) | `repository` `delete_branch` | Deletes it and answers with the `hash` it pointed to. `?head=<sha>` leaves it alone (`409`) if it has moved since. | |
| 241 | | [`POST /repos/{owner}/{name}/branches/{branch}/rename`](/reference/api/repositories/rename-branch/) | `repository` `rename_branch` | Renames it to `new_name`. | |
| 242 | |
| 243 | To restore a deleted branch, make it again with the `hash` deleting it |
| 244 | answered with as `from`. Encode a branch name with slashes as one path |
| 245 | segment: `feature/login` is `feature%2Flogin`. |
| 246 | |
| 247 | ```sh |
| 248 | curl -X DELETE https://api.g1t.sh/repos/acme/rocket/branches/feature%2Flogin \ |
| 249 | -H "Authorization: Bearer $G1T_TOKEN" |
| 250 | # {"name": "feature/login", "hash": "9f2c4e1b…"} |
| 251 | |
| 252 | curl -X POST https://api.g1t.sh/repos/acme/rocket/branches \ |
| 253 | -H "Authorization: Bearer $G1T_TOKEN" \ |
| 254 | -H "Content-Type: application/json" \ |
| 255 | -d '{"name": "feature/login", "from": "9f2c4e1b…"}' |
| 256 | ``` |
| 257 | |
| 258 | Making a branch sends `git.push`, and deleting one sends `git.deleted`, to |
| 259 | [webhooks](/guides/webhooks/#events), as a push would; both |
| 260 | are recorded in the [audit log](/guides/audit-log/) as `branch.created` and |
| 261 | `branch.deleted`. g1t's token never deletes a branch. |
| 262 | |
| 263 | The **Tags** tab lists tags newest first, up to 100, each with its commit |
| 264 | and a ZIP of its files. |
| 265 | |
| 266 | **Compare**, `g1t.sh/<workspace>/<repo>/compare/<base>...<head>`, shows |
| 267 | what one branch has that another does not: its commits, then every change. |
| 268 | Pick the two branches at the top; **Open a pull request** starts one from |
| 269 | the compared branch. |
| 270 | |
| 271 | On **Files**, each file and folder shows the commit that last changed it and |
| 272 | when, from the branch's whole history. On a long history the first view |
| 273 | can show some of them blank while g1t finishes reading it; a later view |
| 274 | fills them in, and after a push only the new commits are read. The branch menu at the top switches branch and keeps the |
| 275 | folder or file you are on. |
| 276 | |
| 277 | ## Pull request forks |
| 278 | |
| 279 | A pull request that was not opened from a branch has its own remote: |
| 280 | |
| 281 | ```text |
| 282 | https://g1t.sh/pulls/<pull request id>.git |
| 283 | ``` |
| 284 | |
| 285 | Only whoever opened the pull request can push to it, or, for one g1t |
| 286 | made, whoever asked for it. Pushes to a fork |
| 287 | update the pull request's head commit on its page. |
| 288 | |
| 289 | ## Pull request refs |
| 290 | |
| 291 | g1t keeps `refs/pull/<number>/head` and `refs/pull/<number>/merge` in a |
| 292 | repository for each open pull request (see |
| 293 | [What a pull request's run builds](/guides/actions/#what-a-pull-requests-run-builds)). You can |
| 294 | fetch them like any branch, and a `git clone --mirror` gets them, as on |
| 295 | GitHub: |
| 296 | |
| 297 | ```sh |
| 298 | git fetch origin refs/pull/12/head |
| 299 | ``` |
| 300 | |
| 301 | They are hidden refs, as on GitHub: only g1t moves them. A push leaves |
| 302 | them out of what it reads from the repository, and a push that names one |
| 303 | is refused for that ref alone, with the rest of the push applied. So |
| 304 | `git push --mirror` of a repository you cloned with `--mirror` from |
| 305 | GitHub moves every branch and tag and reports only the pull request refs: |
| 306 | |
| 307 | ```text |
| 308 | ! [remote rejected] refs/pull/1/head -> refs/pull/1/head (deny updating a hidden ref) |
| 309 | ``` |
| 310 | |
| 311 | Git then says some refs failed to push; the branches and tags are there. |
| 312 | To leave them out, push branches and tags alone: |
| 313 | `git push origin --all && git push origin --tags`. A push with `--atomic` |
| 314 | that names one is refused whole. |
| 315 | |
| 316 | ## Limits |
| 317 | |
| 318 | ### Size limits |
| 319 | |
| 320 | Repositories are stored in Cloudflare Artifacts. g1t checks its limits |
| 321 | before a push is stored, and declines a push that would cross one. git |
| 322 | prints the reason beside each branch (`! [remote rejected] main (…)`), and |
| 323 | what to do as `remote:` lines. Nothing in a declined push is stored. |
| 324 | |
| 325 | | Limit | Size | What happens past it | |
| 326 | | --- | --- | --- | |
| 327 | | A file | 32 MB | The push is declined, naming the file's size. | |
| 328 | | A repository, with its pull requests' forks | 950 MB, as g1t counts what was pushed (the store holds 1 GB) | The push is declined; once full, pushes are refused with the reason before any data is sent. | |
| 329 | | A free workspace's repositories | 1 GB private and 5 GB public, together; none on the g1t plan | Pushes to the full kind are refused with the reason before any data is sent; see [usage and billing](/guides/usage-and-billing/). | |
| 330 | | A push that push protection can scan before it lands | Most pushes; very large ones are scanned after they land | A very large push goes through and is scanned after it lands; secrets found are open alerts. To have it checked first, push in parts, oldest commits first. | |
| 331 | | A push | 100 MB | Refused by the network with HTTP `413` before g1t sees it. | |
| 332 | |
| 333 | To push a large history in parts: |
| 334 | |
| 335 | ```sh |
| 336 | git rev-list --reverse HEAD | awk 'NR % 500 == 0' | xargs -I{} git push origin {}:refs/heads/main |
| 337 | git push origin main |
| 338 | ``` |
| 339 | |
| 340 | Each push sends only what the one before did not. |
| 341 | |
| 342 | ### Pushes of many branches or tags |
| 343 | |
| 344 | Every branch and tag in a push is stored. Each one is also announced as a |
| 345 | `git.push` event, which starts workflows and calls webhooks, except in a |
| 346 | push of many: |
| 347 | |
| 348 | | A push of | What is announced | |
| 349 | | --- | --- | |
| 350 | | Up to 3 tags | Each tag | |
| 351 | | More than 3 tags (`git push --tags`, say) | None of the tags | |
| 352 | | Up to 1,000 branches | Each branch | |
| 353 | | More than 1,000 branches | Only the default branch, if it moved | |
| 354 | |
| 355 | To have tags start workflows, push them 3 or fewer at a time. |
| 356 | |
| 357 | ### When the store is busy |
| 358 | |
| 359 | If Cloudflare Artifacts is rate limiting g1t or not answering, g1t tries |
| 360 | reads again for a moment, then answers git with HTTP `429` (rate limited) |
| 361 | or `503` (unavailable) and a `Retry-After` header saying how many seconds |
| 362 | to wait. Pushes are never tried again on your behalf: run `git push` |
| 363 | again. On g1t.sh the page says the git storage is busy instead of failing, |
| 364 | and [status.g1t.sh](https://status.g1t.sh) shows **Git storage**. |
| 365 | |
| 366 | ### Git operations |
| 367 | |
| 368 | Each clone, fetch and push is a git operation, your agents' included: |
| 369 | their sandboxes use the same git endpoints you do, and a pull request's |
| 370 | working copy counts for its repository's workspace. Every workspace has 50,000 |
| 371 | a month included. Past that, a workspace on the g1t plan pays $0.18 per |
| 372 | 1,000, and a free workspace is never charged: past 50,000 in a month, its |
| 373 | git requests past 60 in an hour are answered `429` with when to try again, |
| 374 | until the month turns. Counting starts on 2026-10-14. See |
| 375 | [git operations](/guides/usage-and-billing/#git-operations). |
| 376 | |
| 377 | ### Request limits |
| 378 | |
| 379 | Git requests without credentials are limited to 120 a minute from each IP |
| 380 | address, about 40 clones; with credentials, 1,200 a minute for each set of |
| 381 | credentials. Anonymous clones of one repository that g1t has not cached |
| 382 | are limited to 120 a minute, whoever makes them. Past a limit, git is |
| 383 | answered `429` with a message saying to wait a minute. Clone with |
| 384 | [credentials](#authentication) to count against your own limit. See |
| 385 | [rate limits](/reference/rate-limits/). |
| 386 | |
| 387 | What these limits mean in practice, and what to do instead, is on |
| 388 | [What g1t can't do yet](/about/limitations/#git). |
| 389 | |
| 390 | ## Where a slow request's time went |
| 391 | |
| 392 | Every answer g1t gives git carries a `Server-Timing` header: how many |
| 393 | milliseconds each step of the request took. To see it, run git with its |
| 394 | HTTP trace on: |
| 395 | |
| 396 | ```sh |
| 397 | GIT_TRACE_CURL=1 git ls-remote https://g1t.sh/<owner>/<repo>.git 2>&1 | grep -i server-timing |
| 398 | ``` |
| 399 | |
| 400 | | Step | What it is | |
| 401 | | --- | --- | |
| 402 | | `repo` | Finding the repository, and checking your credentials if you sent any | |
| 403 | | `moved` | Only for an address with no repository: looking for a renamed workspace or a transferred repository to send you to | |
| 404 | | `access` | Deciding whether you may fetch from or push to it | |
| 405 | | `kept` | A free workspace's limits, and looking for a ref listing and a store credential made a moment ago | |
| 406 | | `mint` | Only when no credential was kept: the git store making one for the request | |
| 407 | | `store` | The git store's answer to a clone or fetch | |
| 408 | | `recv` | Only for a push: receiving it from git | |
| 409 | | `checks` | Only for a push: checking it against the rules of the branches and tags it changes, for workflow files a token may not change, and for secrets and private email addresses, all at once | |
| 410 | | `upload` | Only for a push: handing it to the git store and its answer | |
| 411 | | `refs` | Only for a push: recording that the repository's refs changed | |
| 412 | | `total` | Everything g1t did | |
| 413 | | `repos` | The same, measured where your request arrived | |
| 414 | |
| 415 | A push's checks run side by side, so each also has its own entry, after |
| 416 | the steps and not counted in the total: `read` (reading the push's |
| 417 | objects), `rules`, `scan` (secrets and email addresses) and, for an access |
| 418 | token, `gate` (workflow files). |
| 419 | |
| 420 | g1t asks git to send each push whole: every object a delta in it builds on |
| 421 | is in the push too (the `no-thin` capability), so checking a push reads |
| 422 | nothing from the repository. A push that changes a large file a little |
| 423 | uploads more than it would otherwise, and stays within the |
| 424 | [size limits](#size-limits). If your git sends a push that builds on |
| 425 | objects outside it anyway, g1t reads up to 200 of those from the |
| 426 | repository to check it, and says `thin;desc=yes`. |
| 427 | |
| 428 | Some entries say how a step went rather than how long it took: |
| 429 | |
| 430 | | Entry | Values | |
| 431 | | --- | --- | |
| 432 | | `refs;desc=` | `hit-colo` or `hit-shared` when the ref listing came from g1t's cache, `miss` when the git store was asked | |
| 433 | | `pack;desc=` | Only for a fresh clone: `hit` when its pack came from g1t's cache, `miss` when the git store built it | |
| 434 | | `cred;desc=` | `isolate` or `shared` for a store credential made a moment ago, `mint` for a new one | |
| 435 | | `thin;desc=` | Only for a push: `no` when it came whole, `yes` when it built on objects outside it | |
| 436 | |
| 437 | The ref listing git asks for first on every clone and fetch is kept for up |
| 438 | to a minute, and only the same question about the same refs gets the same |
| 439 | answer: a push, a merge or any other change to a repository's branches and |
| 440 | tags makes the next fetch ask the git store again. A change can take up to |
| 441 | 5 seconds to reach every fetch. |
| 442 | |
| 443 | A fresh clone, one that has no objects yet (shallow clones such as |
| 444 | `git clone --depth=1` included), has its pack kept too, for up to 7 days |
| 445 | or until the repository's branches or tags next change. The next clone that |
| 446 | asks for the same commits in the same way gets the same pack without the |
| 447 | git store building it again. A fetch into a repository you already have, |
| 448 | and any pack over 200 MB, always goes to the git store. |
| 449 | |
| 450 | Include the header when you report a slow clone, fetch or push. |
| 451 | |
| 452 | ## SSH |
| 453 | |
| 454 | Git over SSH is not available yet. Use HTTPS, which works for clone, |
| 455 | fetch and push everywhere SSH would. |
| 456 | |
| 457 | Why: git over SSH needs raw TCP connections on port 22, and g1t runs |
| 458 | entirely on Cloudflare's network. Accepting inbound TCP traffic directly |
| 459 | into Workers is in a beta from Cloudflare that g1t has applied for and is |
| 460 | waiting on. SSH keys can already be added under |
| 461 | [Settings → SSH keys](https://g1t.sh/settings/keys), |
| 462 | and will be used once SSH is on. So can [deploy keys](#deploy-keys). |
| 463 | |
| 464 | ## Deploy keys |
| 465 | |
| 466 | A deploy key is an SSH key that reaches one repository and nothing else. |
| 467 | Give one to a server or a pipeline that needs to clone a repository, or |
| 468 | push to it, without a person's account behind it. A deploy key belongs to |
| 469 | the repository: it keeps working when the person who added it leaves the |
| 470 | workspace, and it is not tied to anyone's role. |
| 471 | |
| 472 | :::note |
| 473 | Deploy keys are used over SSH, which is [not on yet](#ssh). You can add |
| 474 | them now, and they will work as soon as SSH is. Until then, a machine can |
| 475 | clone and push over HTTPS with a |
| 476 | [workspace access token](/guides/workspaces/#workspace-access-tokens). |
| 477 | ::: |
| 478 | |
| 479 | ### Read-only or read and write |
| 480 | |
| 481 | A deploy key is read-only unless you choose **Allow write access** when |
| 482 | you add it: |
| 483 | |
| 484 | | Access | It can | |
| 485 | | --- | --- | |
| 486 | | **Read-only** (the default) | Clone and fetch the repository, private or not. | |
| 487 | | **Read and write** | Clone, fetch and push, workflow files under `.g1t/workflows/` and `.github/workflows/` included. | |
| 488 | |
| 489 | Either way it reaches only its own repository: any other address is |
| 490 | refused, in its workspace or anywhere else, and so is pushing to an |
| 491 | address with no repository, which would otherwise make one. |
| 492 | |
| 493 | A key with write access can change workflows, and workflows run with the |
| 494 | repository's secrets. Allow it only for a machine that must push. You |
| 495 | cannot change a key's access later: delete it and add it again. |
| 496 | |
| 497 | ### Add a deploy key |
| 498 | |
| 499 | You need the Admin role on the repository and a confirmed email address. |
| 500 | |
| 501 | 1. Make a key pair on the machine that will use it, without a passphrase |
| 502 | if it runs unattended: |
| 503 | |
| 504 | ```sh |
| 505 | ssh-keygen -t ed25519 -C "deploy@build-server" -f ~/.ssh/g1t_deploy -N "" |
| 506 | ``` |
| 507 | |
| 508 | 2. Open the repository's **Settings → Deploy keys**, |
| 509 | `g1t.sh/<workspace>/<repo>/settings/keys`. |
| 510 | 3. Under **Add a deploy key**, give it a **Title**, such as the machine that |
| 511 | uses it, and paste the public key (`~/.ssh/g1t_deploy.pub`) into **Key**. |
| 512 | Left without a title, it takes the key's comment. |
| 513 | 4. Choose **Allow write access** only if the machine must push. |
| 514 | 5. Choose **Add deploy key**. |
| 515 | |
| 516 | g1t takes `ssh-ed25519`, `ecdsa-sha2-nistp256`, `ecdsa-sha2-nistp384`, |
| 517 | `ecdsa-sha2-nistp521` and `ssh-rsa` keys. A public key can be registered |
| 518 | once on g1t: a key that is already someone's SSH key, or a deploy key on |
| 519 | any repository, is refused with **Key is already in use.** Give each |
| 520 | machine, and each repository, its own key. A repository can have up to |
| 521 | 100 deploy keys. |
| 522 | |
| 523 | ### Manage deploy keys |
| 524 | |
| 525 | **Settings → Deploy keys** lists every key on the repository, oldest |
| 526 | first, with its title, fingerprint, whether it is **Read-only** or **Read |
| 527 | and write**, who added it and when, and when it was last used. **Last |
| 528 | used** is when the key last signed in over SSH, to within 5 minutes; |
| 529 | **Never used** means it never has. Use it to find keys nothing uses any |
| 530 | more. |
| 531 | |
| 532 | To remove a key, choose **Delete** beside it and confirm. Anything using |
| 533 | it stops at once. |
| 534 | |
| 535 | Only people with the Admin role on the repository see the page and |
| 536 | manage its keys. An agent's token never can, and neither can a deploy key. |
| 537 | A workspace's own access token can only when an owner gave it Admin. See |
| 538 | [access and roles](/guides/access-and-roles/#deploy-keys). Adding and |
| 539 | deleting a key is recorded in the workspace's |
| 540 | [audit log](/guides/audit-log/) as `repo.deploy_key_added` and |
| 541 | `repo.deploy_key_removed`. |
| 542 | |
| 543 | Deploy keys are kept by repository, so renaming or transferring the |
| 544 | repository keeps them. While a repository is deleted its keys do not work, |
| 545 | and when it is removed for good they go with it. |
| 546 | |
| 547 | ### Use a deploy key with git |
| 548 | |
| 549 | Once SSH is on, point git at the key for the repository's remote: |
| 550 | |
| 551 | ```sh |
| 552 | GIT_SSH_COMMAND="ssh -i ~/.ssh/g1t_deploy -o IdentitiesOnly=yes" \ |
| 553 | git clone git@g1t.sh:<workspace>/<repo>.git |
| 554 | ``` |
| 555 | |
| 556 | Or name it in `~/.ssh/config` for every git command on that machine: |
| 557 | |
| 558 | ```text |
| 559 | Host g1t.sh |
| 560 | User git |
| 561 | IdentityFile ~/.ssh/g1t_deploy |
| 562 | IdentitiesOnly yes |
| 563 | ``` |
| 564 | |
| 565 | A machine that needs several repositories needs a key for each; give each |
| 566 | a `Host` alias with its own `IdentityFile`. |
| 567 | |
| 568 | ### Through the API |
| 569 | |
| 570 | | Route | MCP tool and action | What it does | |
| 571 | | --- | --- | --- | |
| 572 | | `GET /repos/{owner}/{name}/keys` | `access` `list_deploy_keys` | The repository's deploy keys. | |
| 573 | | `GET /repos/{owner}/{name}/keys/{id}` | `access` `get_deploy_key` | One key, by its `id`. | |
| 574 | | `POST /repos/{owner}/{name}/keys` | `access` `add_deploy_key` | Add a key. Body: `title`, `key` and `read_only` (true unless you send false). | |
| 575 | | `DELETE /repos/{owner}/{name}/keys/{id}` | `access` `remove_deploy_key` | Delete a key. | |
| 576 | |
| 577 | Listing and reading need the `access:read` scope; adding and deleting |
| 578 | need `access:admin`. Each needs the Admin role on the repository. |
| 579 | |
| 580 | ```sh |
| 581 | curl https://api.g1t.sh/repos/acme/rocket/keys \ |
| 582 | -H "Authorization: Bearer $G1T_TOKEN" \ |
| 583 | -H "Content-Type: application/json" \ |
| 584 | -d '{"title": "Build server", "key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGb9ECWmEzf6FQbrBZ9w7lshQhqowtrbLDFw4rXAxZuE", "read_only": true}' |
| 585 | ``` |
| 586 | |
| 587 | ```json |
| 588 | { |
| 589 | "id": "dk_01kp3f2g3h4j5k6m7n8p9q0r1s", |
| 590 | "title": "Build server", |
| 591 | "key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGb9ECWmEzf6FQbrBZ9w7lshQhqowtrbLDFw4rXAxZuE", |
| 592 | "fingerprint": "SHA256:ubxEl41fJDnUoEPKSZE0y6R0ZjjAQf/wV5vZgeBV8qk", |
| 593 | "read_only": true, |
| 594 | "created_at": "2026-10-08T09:12:00.000Z", |
| 595 | "created_by": "ada", |
| 596 | "last_used_at": null |
| 597 | } |
| 598 | ``` |
| 599 | |
| 600 | See [create a deploy key](/reference/api/access/create-deploy-key/) in the |
| 601 | API reference. |