Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

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