ssh-gh-id keeps a managed block inside your authorized_keys file in sync with one or more GitHub accounts by fetching https://github.com/<username>.keys.
It is intentionally narrow in scope:
- stores a list of GitHub usernames
- fetches each user's published SSH keys from GitHub
- validates fetched and cached SSH public keys before writing them
- rewrites only its own managed block in
authorized_keys - schedules periodic refreshes with
systemdorcrontab
It does not overwrite unmanaged keys outside the managed block.
- CLI flags for add, delete, list, update, update-all, install, uninstall, self-update, interval changes, scheduler selection, status, help, and version
- XDG-style storage by default
- config:
~/.config/ssh-gh-id/ - data:
~/.local/share/ssh-gh-id/ - state/logs:
~/.local/state/ssh-gh-id/
- config:
- atomic writes for config, cache, and
authorized_keys - file locking to avoid concurrent runs
- cache-based updates so transient fetch failures do not automatically erase previously known keys
- managed scheduler install with
systemdfor root, andcrontaborsystemd --userfor non-root users
go install github.com/M1k0t0/ssh-gh-id@latest
ssh-gh-id -igo build -o ssh-gh-id .
./ssh-gh-id -iWhen you run -i as a non-root user, the install target is derived from the currently running ssh-gh-id binary on disk. For example, running /some/path/ssh-gh-id -i installs the managed binary at /some/path/ssh-gh-id, and the scheduler/PATH helper follow that location.
When you run -i as root, the install target is always the fixed root-owned path /usr/local/bin/ssh-gh-id. System-level systemd units point at that fixed path rather than at the directory where the installer was run.
Install the latest release with curl:
ARCH="$(uname -m)"; case "$ARCH" in x86_64|amd64) ASSET="ssh-gh-id-linux-amd64" ;; aarch64|arm64) ASSET="ssh-gh-id-linux-arm64" ;; *) echo "Unsupported architecture: $ARCH" >&2; exit 1 ;; esac && \
mkdir -p "$HOME/.local/bin" && \
curl -fsSL -o "$HOME/.local/bin/ssh-gh-id" "https://github.com/M1k0t0/ssh-gh-id/releases/latest/download/${ASSET}" && \
chmod +x "$HOME/.local/bin/ssh-gh-id" && \
"$HOME/.local/bin/ssh-gh-id" -iInstall the latest release with wget:
ARCH="$(uname -m)"; case "$ARCH" in x86_64|amd64) ASSET="ssh-gh-id-linux-amd64" ;; aarch64|arm64) ASSET="ssh-gh-id-linux-arm64" ;; *) echo "Unsupported architecture: $ARCH" >&2; exit 1 ;; esac && \
mkdir -p "$HOME/.local/bin" && \
wget -qO "$HOME/.local/bin/ssh-gh-id" "https://github.com/M1k0t0/ssh-gh-id/releases/latest/download/${ASSET}" && \
chmod +x "$HOME/.local/bin/ssh-gh-id" && \
"$HOME/.local/bin/ssh-gh-id" -iThese convenience snippets trust GitHub Releases over HTTPS and execute the downloaded binary. If you need stronger provenance, download a pinned release plus its .sha256 asset and verify it before running the binary.
If the installed binary directory is not already on your current PATH, ssh-gh-id -i will add a managed PATH block to your shell profile automatically. Open a new shell or source the profile file to pick it up in the current session.
~/.local/bin/ssh-gh-id --uninstallor:
ssh-gh-id --uninstallRoot installs remove /usr/local/bin/ssh-gh-id.
ssh-gh-id --add <username> # or -a <username>
ssh-gh-id --del <username> # or -d <username>
ssh-gh-id --list # or -l
ssh-gh-id --update <username> # or -u <username>
ssh-gh-id --update-all # or -U
ssh-gh-id --set-interval <spec> # or -t <spec>
ssh-gh-id --set-scheduler <backend> # auto | systemd | systemd-user | crontab
ssh-gh-id --install # or -i
ssh-gh-id --uninstall
ssh-gh-id --self-update
ssh-gh-id --status # or -s
ssh-gh-id --version # or -v
ssh-gh-id --help # or -hAdd a user and immediately fetch their current keys:
ssh-gh-id -a <username>Refresh every configured user:
ssh-gh-id -UList configured users:
ssh-gh-id --listDelete a user and remove their cached keys from the managed block:
ssh-gh-id -d <username>Set scheduler backend and interval, then reinstall the scheduler:
ssh-gh-id --set-scheduler crontab
ssh-gh-id --set-interval '@hourly'
ssh-gh-id -iShow status:
ssh-gh-id --statusUpdate the installed binary to the latest GitHub Release:
ssh-gh-id --self-update--self-update currently supports the Linux release assets published by this project (linux-amd64 and linux-arm64). It downloads the matching binary and .sha256 asset from the latest GitHub Release, verifies the binary against that checksum, then replaces the currently running executable path. This checksum check catches download mismatch/corruption; it does not protect against a compromised repository, maintainer account, workflow token, release asset, or checksum asset.
--install writes the managed binary and installs a scheduler using the configured interval and backend.
Scheduler backend values:
auto— default. Root uses system-levelsystemd; non-root users prefercrontabwhen available, then fall back tosystemd --userwhen usable.systemd— writes/etc/systemd/system/ssh-gh-id.serviceand.timer; requires root.systemd-user— writes user units under$XDG_CONFIG_HOME/systemd/user; may require linger (loginctl enable-linger <user>) to keep running after logout.crontab— writes a managed block to the current user's crontab.
After changing the interval or scheduler backend, rerun ssh-gh-id -i so the timer or crontab entry is rewritten.
--set-interval stores the refresh interval used by --install.
Supported values:
hourly,daily,weekly,monthly,yearly@hourly,@daily,@weekly,@monthly,@yearly- 5-field cron expressions, for example
0 */6 * * * - simple durations for systemd-style timers, for example
12h,24h,7d
Examples:
ssh-gh-id --set-interval daily
ssh-gh-id --set-interval '@hourly'
ssh-gh-id --set-interval '0 */6 * * *'
ssh-gh-id --set-interval 12hDuration-style intervals are best suited for systemd timers. Some simple durations can be converted to cron; others require systemd/systemd-user.
The tool writes a block like this inside authorized_keys:
# >>> ssh-gh-id managed block >>>
# managed by ssh-gh-id; edits outside this block are preserved
# user: alice
ssh-ed25519 AAAA...
# user: bob
ssh-ed25519 BBBB...
# <<< ssh-gh-id managed block <<<
Anything outside that block is left alone.
If a user has no cached keys yet, the block contains only a comment for that user until a successful fetch occurs. If authorized_keys contains incomplete or duplicate ssh-gh-id managed blocks, the tool fails loudly instead of guessing which block is authoritative; remove duplicate/corrupt managed blocks manually and rerun the command.
By default:
- config:
~/.config/ssh-gh-id/config.json - user list:
~/.local/share/ssh-gh-id/users.json - per-user key cache:
~/.local/state/ssh-gh-id/cache/*.json - status:
~/.local/state/ssh-gh-id/status.json - logs:
~/.local/state/ssh-gh-id/logs/ssh-gh-id.log - lock file:
~/.local/state/ssh-gh-id/lock
Useful environment overrides:
XDG_CONFIG_HOMEXDG_DATA_HOMEXDG_STATE_HOMESSH_GH_ID_AUTHORIZED_KEYS_PATH
This tool trusts GitHub's .keys endpoint for every configured username. It does not support custom key-source endpoints from environment variables.
That means:
- if GitHub serves a new valid public key for a configured account, the next refresh can add it
- if GitHub stops serving a key, the next successful refresh can remove it from the managed block
- a compromised GitHub account can publish attacker-controlled keys
- network failures do not automatically delete previously cached keys, because refresh failures fall back to the last cached copy
- fetched and cached key lines are parsed and validated as bare SSH public keys before they are written
You should only configure GitHub accounts you intentionally trust for SSH access.
If something looks wrong:
- Check current status:
ssh-gh-id --status
- Inspect the log:
tail -n 100 ~/.local/state/ssh-gh-id/logs/ssh-gh-id.log - Force a refresh:
ssh-gh-id --update-all
- If needed, remove only the managed block from
authorized_keysmanually. Unmanaged entries can stay in place. - If scheduler configuration is stale, rerun:
ssh-gh-id --install
If you want to fully stop automation but keep your user list and cache, run:
ssh-gh-id --uninstallThat removes the scheduler and installed binary, but leaves config/data/state files alone.
Run the full local check suite:
make checkUseful individual commands:
make fmt
make test
make vet
make build