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
601 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, Agents, 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, and for agents, unless a ruleset lists them
154as able to bypass 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 agents' included:
369their sandboxes use the same git endpoints you do, and a pull request's
370working copy counts for its repository's workspace. Every workspace has 50,000
371a month included. Past that, a workspace on the g1t plan pays $0.18 per
3721,000, and a free workspace is never charged: past 50,000 in a month, its
373git requests past 60 in an hour are answered `429` with when to try again,
374until the month turns. Counting starts on 2026-10-14. See
375[git operations](/guides/usage-and-billing/#git-operations).
376
377### Request limits
378
379Git requests without credentials are limited to 120 a minute from each IP
380address, about 40 clones; with credentials, 1,200 a minute for each set of
381credentials. Anonymous clones of one repository that g1t has not cached
382are limited to 120 a minute, whoever makes them. Past a limit, git is
383answered `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
387What 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
392Every answer g1t gives git carries a `Server-Timing` header: how many
393milliseconds each step of the request took. To see it, run git with its
394HTTP trace on:
395
396```sh
397GIT_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
415A push's checks run side by side, so each also has its own entry, after
416the steps and not counted in the total: `read` (reading the push's
417objects), `rules`, `scan` (secrets and email addresses) and, for an access
418token, `gate` (workflow files).
419
420g1t asks git to send each push whole: every object a delta in it builds on
421is in the push too (the `no-thin` capability), so checking a push reads
422nothing from the repository. A push that changes a large file a little
423uploads more than it would otherwise, and stays within the
424[size limits](#size-limits). If your git sends a push that builds on
425objects outside it anyway, g1t reads up to 200 of those from the
426repository to check it, and says `thin;desc=yes`.
427
428Some 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
437The ref listing git asks for first on every clone and fetch is kept for up
438to a minute, and only the same question about the same refs gets the same
439answer: a push, a merge or any other change to a repository's branches and
440tags makes the next fetch ask the git store again. A change can take up to
4415 seconds to reach every fetch.
442
443A 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
445or until the repository's branches or tags next change. The next clone that
446asks for the same commits in the same way gets the same pack without the
447git store building it again. A fetch into a repository you already have,
448and any pack over 200 MB, always goes to the git store.
449
450Include the header when you report a slow clone, fetch or push.
451
452## SSH
453
454Git over SSH is not available yet. Use HTTPS, which works for clone,
455fetch and push everywhere SSH would.
456
457Why: git over SSH needs raw TCP connections on port 22, and g1t runs
458entirely on Cloudflare's network. Accepting inbound TCP traffic directly
459into Workers is in a beta from Cloudflare that g1t has applied for and is
460waiting on. SSH keys can already be added under
461[Settings → SSH keys](https://g1t.sh/settings/keys),
462and will be used once SSH is on. So can [deploy keys](#deploy-keys).
463
464## Deploy keys
465
466A deploy key is an SSH key that reaches one repository and nothing else.
467Give one to a server or a pipeline that needs to clone a repository, or
468push to it, without a person's account behind it. A deploy key belongs to
469the repository: it keeps working when the person who added it leaves the
470workspace, and it is not tied to anyone's role.
471
472:::note
473Deploy keys are used over SSH, which is [not on yet](#ssh). You can add
474them now, and they will work as soon as SSH is. Until then, a machine can
475clone 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
481A deploy key is read-only unless you choose **Allow write access** when
482you 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
489Either way it reaches only its own repository: any other address is
490refused, in its workspace or anywhere else, and so is pushing to an
491address with no repository, which would otherwise make one.
492
493A key with write access can change workflows, and workflows run with the
494repository's secrets. Allow it only for a machine that must push. You
495cannot change a key's access later: delete it and add it again.
496
497### Add a deploy key
498
499You need the Admin role on the repository and a confirmed email address.
500
5011. 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
5082. Open the repository's **Settings → Deploy keys**,
509 `g1t.sh/<workspace>/<repo>/settings/keys`.
5103. 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.
5134. Choose **Allow write access** only if the machine must push.
5145. Choose **Add deploy key**.
515
516g1t takes `ssh-ed25519`, `ecdsa-sha2-nistp256`, `ecdsa-sha2-nistp384`,
517`ecdsa-sha2-nistp521` and `ssh-rsa` keys. A public key can be registered
518once on g1t: a key that is already someone's SSH key, or a deploy key on
519any repository, is refused with **Key is already in use.** Give each
520machine, and each repository, its own key. A repository can have up to
521100 deploy keys.
522
523### Manage deploy keys
524
525**Settings → Deploy keys** lists every key on the repository, oldest
526first, with its title, fingerprint, whether it is **Read-only** or **Read
527and write**, who added it and when, and when it was last used. **Last
528used** 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
530more.
531
532To remove a key, choose **Delete** beside it and confirm. Anything using
533it stops at once.
534
535Only people with the Admin role on the repository see the page and
536manage its keys. An agent's token never can, and neither can a deploy key.
537A 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
539deleting 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
543Deploy keys are kept by repository, so renaming or transferring the
544repository keeps them. While a repository is deleted its keys do not work,
545and when it is removed for good they go with it.
546
547### Use a deploy key with git
548
549Once SSH is on, point git at the key for the repository's remote:
550
551```sh
552GIT_SSH_COMMAND="ssh -i ~/.ssh/g1t_deploy -o IdentitiesOnly=yes" \
553 git clone git@g1t.sh:<workspace>/<repo>.git
554```
555
556Or name it in `~/.ssh/config` for every git command on that machine:
557
558```text
559Host g1t.sh
560 User git
561 IdentityFile ~/.ssh/g1t_deploy
562 IdentitiesOnly yes
563```
564
565A machine that needs several repositories needs a key for each; give each
566a `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
577Listing and reading need the `access:read` scope; adding and deleting
578need `access:admin`. Each needs the Admin role on the repository.
579
580```sh
581curl 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
600See [create a deploy key](/reference/api/access/create-deploy-key/) in the
601API reference.