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
- rewrites only its own managed block in
authorized_keys - schedules periodic refreshes with
systemd --userwhen available, otherwisecrontab
It does not overwrite unmanaged keys outside the managed block.
- CLI flags for add, delete, list, update, update-all, install, uninstall, interval changes, 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
systemd --userpreferred,crontabfallback
go build -o ssh-gh-id .
./ssh-gh-id -iWhen you run -i, the install target directory is derived from the currently running ssh-gh-id binary on disk. In other words, if you run /some/path/ssh-gh-id -i, the managed install target becomes /some/path/ssh-gh-id. The scheduler and PATH helper follow that location.
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" -iIf 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 --uninstallssh-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 --install # or -i
ssh-gh-id --uninstall
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>Show status:
ssh-gh-id --status--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 12hIf a scheduler is already installed, rerun --install after changing the interval so the timer or crontab entry is rewritten.
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.
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_PATHSSH_GH_ID_KEYS_BASE_URL(primarily for testing)
This tool trusts GitHub's .keys endpoint for every configured username.
That means:
- if GitHub serves a new 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
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 tests and build locally:
go test ./...
go build ./...