Audit GitHub Repository Deploy Keys with gh
You will finish with a repeatable way to list the deploy keys attached to a GitHub repository, inspect whether each key is read-only, and save a focused JSON view for an audit. The examples use GitHub CLI gh 2.87.3, the executable installed on this machine. The local package database reports gh 2.45.0-1ubuntu0.3+esm3, so check the executable you actually invoke when version-specific output matters.
The route
Jump straight to the step you need, or tick off Done means at the end.
Allow about ten minutes. You need GitHub CLI, a repository you are allowed to inspect, and an authenticated account or token with access to that repository. This guide only lists data. It does not add, rotate or delete a deploy key, and no elevated privilege is needed.
1. Confirm the executable and repository
Check which program will run, then confirm the repository name you intend to query. Use the full OWNER/REPO form in scripts so the result does not depend on the current directory:
$ command -v gh
/usr/bin/gh
$ gh --version
gh version 2.87.3 (2026-02-23)
Replace OWNER/REPO below with an existing repository, for example acme/widget. The optional HOST/ prefix supports a GitHub Enterprise host:
$ gh repo deploy-key list --repo OWNER/REPO
The command also accepts the short inherited spelling:
$ gh repo deploy-key list -R OWNER/REPO
Checkpoint: these two forms select the same target. Do not omit the owner when running from a directory that is not a Git checkout.
2. Read the normal list
Run the command without formatting flags when you want the standard human-readable list:
$ gh repo deploy-key list -R OWNER/REPO
The command lists the deploy keys GitHub returns for that repository. An empty result is a valid result: it means there are no keys visible to the account for that repository. A failure such as an HTTP 404 can also mean that the repository does not exist, the host is wrong, or your account cannot see it. Check the target and authentication before treating that as evidence that no keys exist.
Deploy keys are SSH public keys, but their presence grants access according to the key's repository permissions. Treat titles, fingerprints and access state as security-sensitive inventory. Never paste a private key into a title, a ticket or a command line, and do not confuse a public deploy key shown here with the private key used by a build system.
3. Select fields for an audit record
Use --json when another tool needs structured output. This installed version exposes exactly these fields: createdAt, id, key, readOnly and title:
$ gh repo deploy-key list -R OWNER/REPO \
--json id,title,key,readOnly,createdAt
[]
The example's [] is the valid JSON result for a repository with no visible deploy keys. With keys present, the result is an array of objects containing only the fields requested. Field order in the command is for readability; use names from the supported list rather than guessing at API fields.
The key field is useful for comparing a listed public key with an authorised inventory, but it can expose more detail than a basic report needs. Leave it out when a title, ID, access mode and creation time are enough:
$ gh repo deploy-key list -R OWNER/REPO \
--json id,title,readOnly,createdAt
Checkpoint: pipe this output to a file only after checking its destination and permissions. A local audit file may contain repository names and key material, even though the key value is public SSH data.
4. Filter the JSON without changing GitHub
--jq applies a jq expression to the JSON result. For example, print only keys that are marked read-only, with their title and creation time:
$ gh repo deploy-key list -R OWNER/REPO \
--json title,readOnly,createdAt \
--jq '.[] | select(.readOnly == true) | [.title, .createdAt] | @tsv'
build-read-only 2026-03-10T14:22:31Z
If the repository has no matching key, the filter prints nothing and normally exits successfully. That is easy to mistake for a failed request. Keep the unfiltered command or an ID-only JSON command nearby when you need to distinguish an empty list from a transport or permission error.
To produce a compact count for a shell check, use the array length:
$ gh repo deploy-key list -R OWNER/REPO \
--json id \
--jq 'length'
0
That count is a snapshot, not a lock or a continuous monitor. Another administrator can add or remove a key after the command completes.
5. Use templates only when their text format is useful
The command also accepts --template, using GitHub CLI's Go template formatting. A simple title and access-state report is suitable for a terminal or log:
$ gh repo deploy-key list -R OWNER/REPO \
--json title,readOnly \
--template '{{range .}}{{.title}}: read-only={{.readOnly}}{{"\n"}}{{end}}'
build-read-only: read-only=true
Use JSON rather than a template when another program will consume the result. Templates turn structured values into presentation text, and a title containing punctuation or a newline can make a log harder to parse.
6. Recover from the common failures
If GitHub CLI reports that you are not authenticated, inspect the current account with gh auth status. If the repository is private or belongs to an organisation, ask its administrator for the minimum access needed to read repository administration data. Do not work around a permission error by copying a deploy key or using a personal token in shell history.
If you receive a 404, verify the exact owner, repository spelling and GitHub host. The command's -R or --repo value must be [HOST/]OWNER/REPO. If the command returns JSON but a requested field is missing, rerun gh repo deploy-key list --help and use the fields listed under JSON FIELDS for the installed executable.
There is no undo step because listing is read-only. If your audit process saved output containing a key value, remove that report using your normal records-retention procedure, then check that no shell history, CI log or terminal capture retained it.
Done means
- You confirmed the
ghexecutable and selected the repository explicitly with-Ror--repo. - You can list the visible deploy keys without changing repository state.
- You can request only supported fields, including
readOnlyandcreatedAt. - You can filter the JSON with
--jqand recognise that an empty result is not automatically an error. - You kept any saved inventory and public key values within the repository's security and retention rules.