Add and Audit GitHub Deploy Keys with gh
A deploy key gives one machine access to one GitHub repository, without handing out anyone's personal login. By the end of this guide the repository has a deliberately scoped SSH deploy key, and you know how to find its numeric ID and remove it. The installed command is GitHub CLI 2.87.3, packaged here as gh 2.45.0-1ubuntu0.3+esm3. Allow about 10 minutes if the key pair does not exist yet.
The route
Jump straight to the step you need, or tick off Done means at the end.
Before you start
- Install and authenticate GitHub CLI. Check the active version and account with
gh --versionandgh auth status. - Choose the exact repository in
OWNER/REPOform. The examples useacme/widget; replace it before pressing Enter. - Decide whether the automation only clones and fetches, or must push too. A deploy key is repository-specific: it is not the same thing as a personal SSH key.
No command in this guide needs sudo. Authentication, repository permissions and the current GitHub token decide whether the API call succeeds.
1. Make a dedicated key pair
Keep a deploy key separate from your normal login key. This example creates an Ed25519 pair with no passphrase so a non-interactive service can use the private half. Store that private file in the service's protected secret store, never in the repository.
ssh-keygen -t ed25519 -C "widget deploy key" -N "" -f "$HOME/.ssh/widget-deploy"
The command writes a private key at ~/.ssh/widget-deploy and the public key at ~/.ssh/widget-deploy.pub. The next step needs only the public file; do not paste or upload the private one.
test -s "$HOME/.ssh/widget-deploy.pub" && ssh-keygen -lf "$HOME/.ssh/widget-deploy.pub"
Example
Expected output contains an SHA256 fingerprint and the comment:
256 SHA256:REPLACE_WITH_THE_FINGERPRINT widget deploy key (ED25519)
2. Add a read-only key first
Adding a key changes the repository's access controls, so start without --allow-write. The installed help describes that flag as granting write access, so leaving it out keeps the deploy key read-only.
gh repo deploy-key add "$HOME/.ssh/widget-deploy.pub" \
--repo acme/widget \
--title "widget deploy key"
The command takes a key file, an optional title and the inherited --repo selector; you can use -R acme/widget instead. A successful call normally returns the new key details, but the exact formatting is controlled by the installed gh release and should not be parsed as a stable interface.
Security boundary
GitHub CLI associates keys added by this command with the current authentication token. The gh manual warns that de-authorising that GitHub CLI app or token removes keys it added, so record that ownership in your runbook before relying on the key.
3. List and record the key ID
List the repository's deploy keys before making further changes. This is also the safest way to find the numeric ID you will need for deletion.
gh repo deploy-key list \
--repo acme/widget \
--json id,title,key,readOnly,createdAt
Each JSON object can contain createdAt, id, key, readOnly and title. Check the title and public-key fingerprint, not just the position in the list. A typical record looks like this:
[{"createdAt":"2026-09-23T10:20:00Z","id":12345678,"key":"ssh-ed25519 AAAA...","readOnly":true,"title":"widget deploy key"}]
For a shorter review, select fields with --jq:
gh repo deploy-key list -R acme/widget \
--json id,title,readOnly \
--jq '.[] | "\(.id)\t\(.title)\treadOnly=\(.readOnly)"'
Save the ID in your change record. Do not confuse it with the SSH fingerprint or with a repository ID.
4. Grant write access only deliberately
Write access lets the holder push to the repository, a much wider failure impact than a read-only checkout. Use the flag only when the service genuinely needs to publish commits or tags, and add a new key through your normal review process rather than broadening an existing one because a clone failed.
gh repo deploy-key add "$HOME/.ssh/widget-deploy.pub" \
--repo acme/widget \
--title "widget publisher" \
--allow-write
Checkpoint
List the keys again and confirm that the intended record has the expected title, and that readOnly is false only for the publisher key.
5. Remove a key safely
Deletion is an access-control change with no undo flag. Before deleting, copy the exact ID from the list output and confirm the repository, title and fingerprint with the automation's owner. Removing a key stops any client using its private half from authenticating.
gh repo deploy-key list -R acme/widget --json id,title,key,readOnly
# After checking the record, replace 12345678 with that exact ID.
gh repo deploy-key delete 12345678 -R acme/widget
Verify that the record is gone:
gh repo deploy-key list -R acme/widget --json id,title,readOnly \
--jq '.[] | select(.id == 12345678)'
No output from that filter means the ID is no longer listed. If you removed the wrong key, recover by creating a new dedicated public key and adding it again at the intended access level; the old private key cannot restore a deleted GitHub deploy-key record.
Common failure points
- Authentication required. Run
gh auth status, then authenticate or refresh the account with the repository administration needed for deploy keys. The parent command documents exit code 4 for authentication required. - Wrong repository. Pass
--repo OWNER/REPOexplicitly when a script may run outside a clone, so a default repository or working directory cannot send the change elsewhere. - Private key exposed. Stop using it if it was committed, pasted into a ticket or sent to the wrong person. Remove the GitHub deploy-key record, then create and distribute a replacement through the approved secret-handling path.
- Unexpected write access. Inspect
readOnlyin the JSON output; a title is only a label, it does not enforce permissions.
Done means
- Generated a key for this repository, with its private half protected.
- Confirmed that
gh repo deploy-key listshows the expected title, fingerprint, ID and access level. - Kept the key read-only unless a reviewed service requirement justified
--allow-write. - Recorded that the current gh authentication token controls the key's lifetime.
- Checked the exact ID before any deletion, and confirmed removal with a second list.