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.

g1t.sh
600 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. To make it public, see
49[change who can see a repository](/guides/managing-repositories/#change-who-can-see-a-repository).
50
51```sh
52git push https://g1t.sh/<workspace>/new-repo.git main
53```
54
55## Private repositories
56
57A private repository is visible only to people with a
58[role](/guides/access-and-roles/) on it. Cloning and fetching need
59Read, and pushing needs Write. To
60everyone else it looks exactly like a repository that does not exist, both
61on the site and to git.
62
63On the site, an address you cannot see gives the same page either way, with
64status 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
71Issues, pull requests and workspace pages work the same way. The sidebar
72does not open the project or workspace the address names, so nothing on
73the page hints at whether it exists; a missing file, commit or issue in a
74project you can see keeps that project's sidebar. A profile that does not
75exist says **No one on g1t goes by that name**, since profiles are public.
76
77## Download a ZIP
78
79On a repository's **Files** page, **Code** → **Download ZIP** downloads the
80branch shown as one zip, its files in a folder named `<repo>-<branch>`. It
81works for anyone who can see the repository, and for any branch, tag or
82commit at `g1t.sh/<workspace>/<repo>/archive/<ref>.zip`, or as a gzipped
83tar at `…/archive/<ref>.tar.gz`. Executable files and symlinks stay as
84they are in git. An archive holds the files, not the history; clone for
85that. A repository with more than 10,000 files or over 24 MB is too large
86to download this way, so clone it instead.
87
88The API serves the same archives at GitHub's addresses,
89[`GET /repos/{owner}/{repo}/tarball/{ref}`](/reference/api/repositories/download-tarball-archive/)
90and [`/zipball/{ref}`](/reference/api/repositories/download-zipball-archive/):
91each answers with a redirect to a link that needs no token, good for 10
92minutes, for the commit the ref names. Its files are in one folder named
93as GitHub names it, `<workspace>-<repo>-<first 7 of the commit>`, which is
94what `actions/checkout` expects when it downloads a repository in a job
95container without git. With a token, a private repository's work too:
96
97```sh
98curl -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
105around it. Any file is at:
106
107```text
108https://g1t.sh/<workspace>/<repo>/raw/<branch, tag or commit>/<path>
109```
110
111That address sends you on to the commit the branch or tag names now, on
112g1t's file host:
113
114```text
115https://g1tusercontent.com/<workspace>/<repo>/raw/<commit>/<path>
116```
117
118g1tusercontent.com is a site of its own so that nothing in a repository
119can reach your g1t.sh session: it never receives g1t.sh's cookies, and a
120file opened there cannot run script. Images, video, audio and PDFs are
121served as themselves; any other text, HTML, SVG source, XML and
122JavaScript included, as plain text; anything else as a download. A file
123is 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
133Images in a file's page show from its raw address, and so do pictures in a
134README that name a file in the repository (`![Diagram](docs/diagram.png)`,
135relative to the README's folder, or `/docs/diagram.png` from the
136repository's root): they show the file at the commit the page shows.
137Uploaded avatars are on the same host, at
138`https://g1tusercontent.com/avatars/<sha256>`.
139
140## Browsing without an account
141
142Public projects, Explore, Search and profiles are open to everyone, in the
143same sidebar members use. Signed out, the sidebar has Explore and Search,
144and in a project its Code, Issues, Pull requests, Workflows and
145Deployments; pages only people with a role on the repository see, such as
146Security and Settings, are left out. **Sign in** and **Sign up** sit at the bottom, and both bring you
147back to the page you were on.
148
149## Protected branches
150
151A repository protects its branches and tags with [rulesets](/guides/rules/),
152under **Settings → Rules**. A push that breaks a rule is refused for
153everyone, whatever their role, unless a ruleset lists them as able to
154bypass it. Git prints which ruleset and rule refused it, and how
155to fix it:
156
157```text
158remote: error: rules for refs/heads/main declined this push:
159remote: - Changes to main must be made through a pull request. [ruleset "Protect main", pull_request]
160remote: 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
164With **Require a pull request before merging**, changes reach a branch only
165by merging a pull request. Creating the branch, such as the first push to
166an empty repository, is still allowed. Rulesets also block force pushes and
167deletions, restrict who creates branches and tags, check commit messages,
168signatures and the files a push changes, and set what a merge needs. See
169[rules](/guides/rules/).
170
171## Branches
172
173Push any branch to a repository you can write to, and open a
174[pull request](/concepts/overview/#pull-requests) from it on the
175repository's **Pull requests** tab.
176
177```sh
178git switch -c my-change
179git push origin my-change
180```
181
182### The Branches page
183
184A repository's **Branches** tab, `g1t.sh/<workspace>/<repo>/branches`, lists
185its 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
205On a phone, the columns fold under the name. Up to 60 branches are read for
206a page, those with an open pull request first; search finds the rest.
207
208People 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
226The default branch is never deleted, and renaming it needs Admin.
227[Rules](/guides/rules/) hold for every one of these changes: a ruleset that
228restricts creating, deleting or renaming a branch refuses it here as it
229would a push, and says why. When nothing protects the default branch,
230people with the Admin role see a note saying so, with **Protect this
231branch**, which starts a [ruleset](/guides/rules/), and **Dismiss**, which
232this browser remembers for them and that repository. An archived
233repository offers none of these changes.
234
235From 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
243To restore a deleted branch, make it again with the `hash` deleting it
244answered with as `from`. Encode a branch name with slashes as one path
245segment: `feature/login` is `feature%2Flogin`.
246
247```sh
248curl -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
252curl -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
258Making a branch sends `git.push`, and deleting one sends `git.deleted`, to
259[webhooks](/guides/webhooks/#events), as a push would; both
260are recorded in the [audit log](/guides/audit-log/) as `branch.created` and
261`branch.deleted`. g1t's token never deletes a branch.
262
263The **Tags** tab lists tags newest first, up to 100, each with its commit
264and a ZIP of its files.
265
266**Compare**, `g1t.sh/<workspace>/<repo>/compare/<base>...<head>`, shows
267what one branch has that another does not: its commits, then every change.
268Pick the two branches at the top; **Open a pull request** starts one from
269the compared branch.
270
271On **Files**, each file and folder shows the commit that last changed it and
272when, from the branch's whole history. On a long history the first view
273can show some of them blank while g1t finishes reading it; a later view
274fills them in, and after a push only the new commits are read. The branch menu at the top switches branch and keeps the
275folder or file you are on.
276
277## Pull request forks
278
279A pull request that was not opened from a branch has its own remote:
280
281```text
282https://g1t.sh/pulls/<pull request id>.git
283```
284
285Only whoever opened the pull request can push to it, or, for one g1t
286made, whoever asked for it. Pushes to a fork
287update the pull request's head commit on its page.
288
289## Pull request refs
290
291g1t keeps `refs/pull/<number>/head` and `refs/pull/<number>/merge` in a
292repository for each open pull request (see
293[What a pull request's run builds](/guides/actions/#what-a-pull-requests-run-builds)). You can
294fetch them like any branch, and a `git clone --mirror` gets them, as on
295GitHub:
296
297```sh
298git fetch origin refs/pull/12/head
299```
300
301They are hidden refs, as on GitHub: only g1t moves them. A push leaves
302them out of what it reads from the repository, and a push that names one
303is 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
305GitHub 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
311Git then says some refs failed to push; the branches and tags are there.
312To leave them out, push branches and tags alone:
313`git push origin --all && git push origin --tags`. A push with `--atomic`
314that names one is refused whole.
315
316## Limits
317
318### Size limits
319
320Repositories are stored in Cloudflare Artifacts. g1t checks its limits
321before a push is stored, and declines a push that would cross one. git
322prints the reason beside each branch (`! [remote rejected] main (…)`), and
323what 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
333To push a large history in parts:
334
335```sh
336git rev-list --reverse HEAD | awk 'NR % 500 == 0' | xargs -I{} git push origin {}:refs/heads/main
337git push origin main
338```
339
340Each push sends only what the one before did not.
341
342### Pushes of many branches or tags
343
344Every 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
346push 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
355To have tags start workflows, push them 3 or fewer at a time.
356
357### When the store is busy
358
359If Cloudflare Artifacts is rate limiting g1t or not answering, g1t tries
360reads again for a moment, then answers git with HTTP `429` (rate limited)
361or `503` (unavailable) and a `Retry-After` header saying how many seconds
362to wait. Pushes are never tried again on your behalf: run `git push`
363again. On g1t.sh the page says the git storage is busy instead of failing,
364and [status.g1t.sh](https://status.g1t.sh) shows **Git storage**.
365
366### Git operations
367
368Each clone, fetch and push is a git operation, your coding agents' and
369workflow jobs' included. Every workspace has 50,000
370a month included. Past that, a workspace on the g1t plan pays $0.18 per
3711,000, and a free workspace is never charged: past 50,000 in a month, its
372git requests past 60 in an hour are answered `429` with when to try again,
373until the month turns. Counting starts on 2026-10-14. See
374[git operations](/guides/usage-and-billing/#git-operations).
375
376### Request limits
377
378Git requests without credentials are limited to 120 a minute from each IP
379address, about 40 clones; with credentials, 1,200 a minute for each set of
380credentials. Anonymous clones of one repository that g1t has not cached
381are limited to 120 a minute, whoever makes them. Past a limit, git is
382answered `429` with a message saying to wait a minute. Clone with
383[credentials](#authentication) to count against your own limit. See
384[rate limits](/reference/rate-limits/).
385
386What these limits mean in practice, and what to do instead, is on
387[What g1t can't do yet](/about/limitations/#git).
388
389## Where a slow request's time went
390
391Every answer g1t gives git carries a `Server-Timing` header: how many
392milliseconds each step of the request took. To see it, run git with its
393HTTP trace on:
394
395```sh
396GIT_TRACE_CURL=1 git ls-remote https://g1t.sh/<owner>/<repo>.git 2>&1 | grep -i server-timing
397```
398
399| Step | What it is |
400| --- | --- |
401| `repo` | Finding the repository, and checking your credentials if you sent any |
402| `moved` | Only for an address with no repository: looking for a renamed workspace or a transferred repository to send you to |
403| `access` | Deciding whether you may fetch from or push to it |
404| `kept` | A free workspace's limits, and looking for a ref listing and a store credential made a moment ago |
405| `mint` | Only when no credential was kept: the git store making one for the request |
406| `store` | The git store's answer to a clone or fetch |
407| `recv` | Only for a push: receiving it from git |
408| `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 |
409| `upload` | Only for a push: handing it to the git store and its answer |
410| `refs` | Only for a push: recording that the repository's refs changed |
411| `total` | Everything g1t did |
412| `repos` | The same, measured where your request arrived |
413
414A push's checks run side by side, so each also has its own entry, after
415the steps and not counted in the total: `read` (reading the push's
416objects), `rules`, `scan` (secrets and email addresses) and, for an access
417token, `gate` (workflow files).
418
419g1t asks git to send each push whole: every object a delta in it builds on
420is in the push too (the `no-thin` capability), so checking a push reads
421nothing from the repository. A push that changes a large file a little
422uploads more than it would otherwise, and stays within the
423[size limits](#size-limits). If your git sends a push that builds on
424objects outside it anyway, g1t reads up to 200 of those from the
425repository to check it, and says `thin;desc=yes`.
426
427Some entries say how a step went rather than how long it took:
428
429| Entry | Values |
430| --- | --- |
431| `refs;desc=` | `hit-colo` or `hit-shared` when the ref listing came from g1t's cache, `miss` when the git store was asked |
432| `pack;desc=` | Only for a fresh clone: `hit` when its pack came from g1t's cache, `miss` when the git store built it |
433| `cred;desc=` | `isolate` or `shared` for a store credential made a moment ago, `mint` for a new one |
434| `thin;desc=` | Only for a push: `no` when it came whole, `yes` when it built on objects outside it |
435
436The ref listing git asks for first on every clone and fetch is kept for up
437to a minute, and only the same question about the same refs gets the same
438answer: a push, a merge or any other change to a repository's branches and
439tags makes the next fetch ask the git store again. A change can take up to
4405 seconds to reach every fetch.
441
442A fresh clone, one that has no objects yet (shallow clones such as
443`git clone --depth=1` included), has its pack kept too, for up to 7 days
444or until the repository's branches or tags next change. The next clone that
445asks for the same commits in the same way gets the same pack without the
446git store building it again. A fetch into a repository you already have,
447and any pack over 200 MB, always goes to the git store.
448
449Include the header when you report a slow clone, fetch or push.
450
451## SSH
452
453Git over SSH is not available yet. Use HTTPS, which works for clone,
454fetch and push everywhere SSH would.
455
456Why: git over SSH needs raw TCP connections on port 22, and g1t runs
457entirely on Cloudflare's network. Accepting inbound TCP traffic directly
458into Workers is in a beta from Cloudflare that g1t has applied for and is
459waiting on. SSH keys can already be added under
460[Settings → SSH keys](https://g1t.sh/settings/keys),
461and will be used once SSH is on. So can [deploy keys](#deploy-keys).
462
463## Deploy keys
464
465A deploy key is an SSH key that reaches one repository and nothing else.
466Give one to a server or a pipeline that needs to clone a repository, or
467push to it, without a person's account behind it. A deploy key belongs to
468the repository: it keeps working when the person who added it leaves the
469workspace, and it is not tied to anyone's role.
470
471:::note
472Deploy keys are used over SSH, which is [not on yet](#ssh). You can add
473them now, and they will work as soon as SSH is. Until then, a machine can
474clone and push over HTTPS with a
475[workspace access token](/guides/workspaces/#workspace-access-tokens).
476:::
477
478### Read-only or read and write
479
480A deploy key is read-only unless you choose **Allow write access** when
481you add it:
482
483| Access | It can |
484| --- | --- |
485| **Read-only** (the default) | Clone and fetch the repository, private or not. |
486| **Read and write** | Clone, fetch and push, workflow files under `.g1t/workflows/` and `.github/workflows/` included. |
487
488Either way it reaches only its own repository: any other address is
489refused, in its workspace or anywhere else, and so is pushing to an
490address with no repository, which would otherwise make one.
491
492A key with write access can change workflows, and workflows run with the
493repository's secrets. Allow it only for a machine that must push. You
494cannot change a key's access later: delete it and add it again.
495
496### Add a deploy key
497
498You need the Admin role on the repository and a confirmed email address.
499
5001. Make a key pair on the machine that will use it, without a passphrase
501 if it runs unattended:
502
503 ```sh
504 ssh-keygen -t ed25519 -C "deploy@build-server" -f ~/.ssh/g1t_deploy -N ""
505 ```
506
5072. Open the repository's **Settings → Deploy keys**,
508 `g1t.sh/<workspace>/<repo>/settings/keys`.
5093. Under **Add a deploy key**, give it a **Title**, such as the machine that
510 uses it, and paste the public key (`~/.ssh/g1t_deploy.pub`) into **Key**.
511 Left without a title, it takes the key's comment.
5124. Choose **Allow write access** only if the machine must push.
5135. Choose **Add deploy key**.
514
515g1t takes `ssh-ed25519`, `ecdsa-sha2-nistp256`, `ecdsa-sha2-nistp384`,
516`ecdsa-sha2-nistp521` and `ssh-rsa` keys. A public key can be registered
517once on g1t: a key that is already someone's SSH key, or a deploy key on
518any repository, is refused with **Key is already in use.** Give each
519machine, and each repository, its own key. A repository can have up to
520100 deploy keys.
521
522### Manage deploy keys
523
524**Settings → Deploy keys** lists every key on the repository, oldest
525first, with its title, fingerprint, whether it is **Read-only** or **Read
526and write**, who added it and when, and when it was last used. **Last
527used** is when the key last signed in over SSH, to within 5 minutes;
528**Never used** means it never has. Use it to find keys nothing uses any
529more.
530
531To remove a key, choose **Delete** beside it and confirm. Anything using
532it stops at once.
533
534Only people with the Admin role on the repository see the page and
535manage its keys. A deploy key never can.
536A workspace's own access token can only when an owner gave it Admin. See
537[access and roles](/guides/access-and-roles/#deploy-keys). Adding and
538deleting a key is recorded in the workspace's
539[audit log](/guides/audit-log/) as `repo.deploy_key_added` and
540`repo.deploy_key_removed`.
541
542Deploy keys are kept by repository, so renaming or transferring the
543repository keeps them. While a repository is deleted its keys do not work,
544and when it is removed for good they go with it.
545
546### Use a deploy key with git
547
548Once SSH is on, point git at the key for the repository's remote:
549
550```sh
551GIT_SSH_COMMAND="ssh -i ~/.ssh/g1t_deploy -o IdentitiesOnly=yes" \
552 git clone git@g1t.sh:<workspace>/<repo>.git
553```
554
555Or name it in `~/.ssh/config` for every git command on that machine:
556
557```text
558Host g1t.sh
559 User git
560 IdentityFile ~/.ssh/g1t_deploy
561 IdentitiesOnly yes
562```
563
564A machine that needs several repositories needs a key for each; give each
565a `Host` alias with its own `IdentityFile`.
566
567### Through the API
568
569| Route | MCP tool and action | What it does |
570| --- | --- | --- |
571| `GET /repos/{owner}/{name}/keys` | `access` `list_deploy_keys` | The repository's deploy keys. |
572| `GET /repos/{owner}/{name}/keys/{id}` | `access` `get_deploy_key` | One key, by its `id`. |
573| `POST /repos/{owner}/{name}/keys` | `access` `add_deploy_key` | Add a key. Body: `title`, `key` and `read_only` (true unless you send false). |
574| `DELETE /repos/{owner}/{name}/keys/{id}` | `access` `remove_deploy_key` | Delete a key. |
575
576Listing and reading need the `access:read` scope; adding and deleting
577need `access:admin`. Each needs the Admin role on the repository.
578
579```sh
580curl https://api.g1t.sh/repos/acme/rocket/keys \
581 -H "Authorization: Bearer $G1T_TOKEN" \
582 -H "Content-Type: application/json" \
583 -d '{"title": "Build server", "key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGb9ECWmEzf6FQbrBZ9w7lshQhqowtrbLDFw4rXAxZuE", "read_only": true}'
584```
585
586```json
587{
588 "id": "dk_01kp3f2g3h4j5k6m7n8p9q0r1s",
589 "title": "Build server",
590 "key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGb9ECWmEzf6FQbrBZ9w7lshQhqowtrbLDFw4rXAxZuE",
591 "fingerprint": "SHA256:ubxEl41fJDnUoEPKSZE0y6R0ZjjAQf/wV5vZgeBV8qk",
592 "read_only": true,
593 "created_at": "2026-10-08T09:12:00.000Z",
594 "created_by": "ada",
595 "last_used_at": null
596}
597```
598
599See [create a deploy key](/reference/api/access/create-deploy-key/) in the
600API reference.