Add and Audit GitHub Deploy Keys with gh
By the end of this guide, a GitHub repository will have a deliberately scoped SSH deploy key, and you will 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. 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 determine whether the API operation succeeds.
Checkpoint 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, not 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 command needs only the public file. Do not paste or upload the private file.
test -s "$HOME/.ssh/widget-deploy.pub" && ssh-keygen -lf "$HOME/.ssh/widget-deploy.pub"
Expected output contains an SHA256 fingerprint and the comment, for example:
256 SHA256:REPLACE_WITH_THE_FINGERPRINT widget deploy key (ED25519)
Checkpoint 2: add a read-only key
Adding a key changes the repository's access controls. Start without --allow-write. The installed help describes that flag as granting write access, so omitting it 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 accepts 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; 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. Record that ownership in your runbook before relying on the key.
Checkpoint 3: list and record the key ID
List the repository's deploy keys before making further changes. The list command is also the safest way to identify the numeric ID required 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 only the position in the list. A typical record has this shape:
[{"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.
Optional: grant write access only deliberately
Write access lets the holder push to the repository. That is a wider failure impact than read-only checkout, so use the flag only when the service genuinely needs to publish commits or tags. Add a new key or change the access decision through your normal review process; do not treat a convenient clone failure as a reason to grant write access.
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.
Remove a key safely
Deletion is an access-control change and cannot be undone with an undo flag. Before deleting, copy the exact ID from the list output and confirm the repository, title and fingerprint with the owner of the automation. Removing a key will stop clients 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 selected ID is no longer listed. If you removed the wrong key, recover by creating a new dedicated public key and adding it again with 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. This prevents a default repository or working directory from sending 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 JSON output. A title is only a label; it does not enforce permissions.
Done means
- The key was generated for this repository and its private half is protected.
gh repo deploy-key listshows the expected title, fingerprint, ID and access level.- The key is read-only unless a reviewed service requirement justified
--allow-write. - The owner knows that the current gh authentication token controls the key's lifetime.
- For retired access, the exact ID was checked before deletion and a second list confirmed removal.