Skip to content

Instantly share code, notes, and snippets.

@lbussy
Last active August 23, 2026 13:17
Show Gist options
  • Select an option

  • Save lbussy/7f96a137d40e3074cacd2fc0732dabd0 to your computer and use it in GitHub Desktop.

Select an option

Save lbussy/7f96a137d40e3074cacd2fc0732dabd0 to your computer and use it in GitHub Desktop.
Safely Switch GitHub Remotes Between HTTPS and SSH

Safely Switch GitHub Remotes Between HTTPS and SSH

This Gist provides two focused commands:

  • git-https-to-ssh converts a GitHub HTTPS remote to SSH.
  • git-ssh-to-https converts a GitHub SSH remote to HTTPS.

Both commands default to origin, accept another remote name, show the exact proposed and rollback commands, support dry-run, require an exact confirmation by default, and verify the stored URL after changing it.

Safety behavior

  • Uses git remote get-url rather than parsing display-oriented git remote -v output.
  • Accepts only constrained remote names.
  • Converts only recognized github.com URL forms with one owner and repository component.
  • Does not use eval.
  • Rejects credentials, query strings, fragments, extra path components, and non-GitHub hosts.
  • Dry-run performs no Git configuration write.
  • Interactive use requires typing yes; --yes is available for deliberate automation.
  • Prints a copy-ready rollback command before mutation.
  • Verifies the resulting remote and attempts restoration if verification fails.

These tools change only the selected repository remote. They do not alter submodule URLs, other remotes, global Git URL rewrite rules, credentials, SSH keys, or GitHub permissions.

Requirements

  • Bash 3.2 or newer
  • Git
  • A Git repository with the selected remote

One-line installs - skip review

These revision-free commands install the current remote scripts directly with elevated privileges.

HTTPS to SSH:

curl -fsSL https://gist.githubusercontent.com/lbussy/7f96a137d40e3074cacd2fc0732dabd0/raw/git_https_to_ssh.sh | sudo bash -c 'set -euo pipefail; install_tmp=$(mktemp); trap "rm -f -- \"$install_tmp\"" EXIT; cat > "$install_tmp"; test -s "$install_tmp"; bash -n "$install_tmp"; install -m 0755 "$install_tmp" /usr/local/bin/git-https-to-ssh'

SSH to HTTPS:

curl -fsSL https://gist.githubusercontent.com/lbussy/7f96a137d40e3074cacd2fc0732dabd0/raw/git_ssh_to_https.sh | sudo bash -c 'set -euo pipefail; install_tmp=$(mktemp); trap "rm -f -- \"$install_tmp\"" EXIT; cat > "$install_tmp"; test -s "$install_tmp"; bash -n "$install_tmp"; install -m 0755 "$install_tmp" /usr/local/bin/git-ssh-to-https'

These commands intentionally skip review and follow the current unpinned Gist revision. Anyone controlling the served content could install code with elevated privileges.

Install after inspection

curl -fsSLO https://gist.githubusercontent.com/lbussy/7f96a137d40e3074cacd2fc0732dabd0/raw/git_https_to_ssh.sh
curl -fsSLO https://gist.githubusercontent.com/lbussy/7f96a137d40e3074cacd2fc0732dabd0/raw/git_ssh_to_https.sh
less git_https_to_ssh.sh
less git_ssh_to_https.sh
bash git_https_to_ssh.sh --install
bash git_ssh_to_https.sh --install
rm -f git_https_to_ssh.sh git_ssh_to_https.sh

The URLs remain intentionally revision-free.

Usage

Preview HTTPS to SSH:

git-https-to-ssh --dry-run

Apply after confirmation:

git-https-to-ssh

Convert a different remote without prompting:

git-https-to-ssh --yes upstream

Reverse the conversion:

git-ssh-to-https

Both converters accept -n for dry-run, -y for noninteractive confirmation, and an optional remote name.

Supported URL forms

HTTPS to SSH accepts:

https://github.com/OWNER/REPOSITORY
https://github.com/OWNER/REPOSITORY.git

SSH to HTTPS accepts:

git@github.com:OWNER/REPOSITORY
git@github.com:OWNER/REPOSITORY.git
ssh://git@github.com/OWNER/REPOSITORY
ssh://git@github.com/OWNER/REPOSITORY.git

The normalized outputs include .git.

Remaining risks

  • Changing transport can break authentication until SSH keys or HTTPS credentials are configured.
  • A separately configured push URL is not changed by these tools; inspect git remote -v.
  • Git worktrees share repository configuration, so the change affects every worktree using that repository.
  • Existing Git processes may continue using a URL captured before the change.
  • The scripts do not test GitHub connectivity or permissions; they only validate and update local configuration.
  • GitHub Enterprise and other hosting providers are intentionally rejected.
  • --yes removes the confirmation barrier and should be used carefully in automation.

License

MIT License. Use at your own risk.

#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
REMOTE="origin"
DRY_RUN=false
ASSUME_YES=false
INSTALL_TARGET="/usr/local/bin/git-https-to-ssh"
die() { printf 'Error: %s\n' "$*" >&2; exit 1; }
usage() {
cat <<'EOF'
Usage: git-https-to-ssh [OPTIONS] [REMOTE]
Convert one GitHub remote from HTTPS to SSH.
Options:
-n, --dry-run Show the change without modifying Git configuration.
-y, --yes Apply without an interactive confirmation.
-h, --help Show help.
--install Install this inspected file in /usr/local/bin.
EOF
}
install_self() {
local source="${BASH_SOURCE[0]}"
[[ -f "$source" && ! -L "$source" ]] ||
die "--install requires a local, regular, non-symlink script file."
source="$(cd "$(dirname "$source")" && pwd -P)/$(basename "$source")"
if (( EUID == 0 )); then
install -d -m 0755 /usr/local/bin
install -m 0755 "$source" "$INSTALL_TARGET"
else
command -v sudo >/dev/null || die "sudo is required for /usr/local/bin."
sudo install -d -m 0755 /usr/local/bin
sudo install -m 0755 "$source" "$INSTALL_TARGET"
fi
printf 'Installed %s.\n' "$INSTALL_TARGET"
}
validate_remote() {
[[ "$REMOTE" =~ ^[A-Za-z0-9][A-Za-z0-9._/-]*$ &&
"$REMOTE" != *..* && "$REMOTE" != *//* && "$REMOTE" != */ ]] ||
die "Remote name contains unsafe or unsupported characters: $REMOTE"
}
parse_args() {
local positional=""
while (( $# )); do
case "$1" in
-n|--dry-run) DRY_RUN=true ;;
-y|--yes) ASSUME_YES=true ;;
-h|--help) usage; exit 0 ;;
--install) install_self; exit 0 ;;
--) shift; break ;;
-*) die "Unknown option: $1" ;;
*)
[[ -z "$positional" ]] || die "Only one remote name may be supplied."
positional="$1"
;;
esac
shift
done
[[ -z "$positional" ]] || REMOTE="$positional"
validate_remote
}
github_https_parts() {
local url="$1" path owner repo
case "$url" in
https://github.com/*) path="${url#https://github.com/}" ;;
*) return 1 ;;
esac
path="${path%.git}"
[[ "$path" == */* && "$path" != */*/* ]] || return 1
owner="${path%%/*}"
repo="${path#*/}"
[[ "$owner" =~ ^[A-Za-z0-9_.-]+$ && "$repo" =~ ^[A-Za-z0-9_.-]+$ ]] ||
return 1
printf '%s\n%s\n' "$owner" "$repo"
}
confirm_change() {
local answer
[[ "$ASSUME_YES" == true ]] && return
printf "Type 'yes' to update remote '%s': " "$REMOTE"
read -r answer < /dev/tty || die "Could not read confirmation."
[[ "$answer" == yes ]] || die "Cancelled without changing Git configuration."
}
main() {
parse_args "$@"
command -v git >/dev/null || die "git is required."
git rev-parse --git-dir >/dev/null 2>&1 || die "Run inside a Git repository."
local old_url parts owner repo new_url actual
old_url=$(git remote get-url "$REMOTE" 2>/dev/null) ||
die "Remote does not exist: $REMOTE"
parts=$(github_https_parts "$old_url") ||
die "Remote is not a recognized GitHub HTTPS URL: $old_url"
owner=${parts%%$'\n'*}
repo=${parts#*$'\n'}
new_url="git@github.com:$owner/$repo.git"
printf 'Remote: %s\nCurrent: %s\nProposed: %s\n' "$REMOTE" "$old_url" "$new_url"
printf "Rollback: git remote set-url '%s' '%s'\n" "$REMOTE" "$old_url"
[[ "$DRY_RUN" == false ]] || { printf 'Dry run: no change made.\n'; return; }
confirm_change
git remote set-url "$REMOTE" "$new_url"
actual=$(git remote get-url "$REMOTE")
if [[ "$actual" != "$new_url" ]]; then
git remote set-url "$REMOTE" "$old_url" || true
die "Verification failed; attempted to restore the original URL."
fi
printf 'Updated %s to %s.\n' "$REMOTE" "$new_url"
}
main "$@"
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
REMOTE="origin"
DRY_RUN=false
ASSUME_YES=false
INSTALL_TARGET="/usr/local/bin/git-ssh-to-https"
die() { printf 'Error: %s\n' "$*" >&2; exit 1; }
usage() {
cat <<'EOF'
Usage: git-ssh-to-https [OPTIONS] [REMOTE]
Convert one GitHub remote from SSH to HTTPS.
Options:
-n, --dry-run Show the change without modifying Git configuration.
-y, --yes Apply without an interactive confirmation.
-h, --help Show help.
--install Install this inspected file in /usr/local/bin.
EOF
}
install_self() {
local source="${BASH_SOURCE[0]}"
[[ -f "$source" && ! -L "$source" ]] ||
die "--install requires a local, regular, non-symlink script file."
source="$(cd "$(dirname "$source")" && pwd -P)/$(basename "$source")"
if (( EUID == 0 )); then
install -d -m 0755 /usr/local/bin
install -m 0755 "$source" "$INSTALL_TARGET"
else
command -v sudo >/dev/null || die "sudo is required for /usr/local/bin."
sudo install -d -m 0755 /usr/local/bin
sudo install -m 0755 "$source" "$INSTALL_TARGET"
fi
printf 'Installed %s.\n' "$INSTALL_TARGET"
}
validate_remote() {
[[ "$REMOTE" =~ ^[A-Za-z0-9][A-Za-z0-9._/-]*$ &&
"$REMOTE" != *..* && "$REMOTE" != *//* && "$REMOTE" != */ ]] ||
die "Remote name contains unsafe or unsupported characters: $REMOTE"
}
parse_args() {
local positional=""
while (( $# )); do
case "$1" in
-n|--dry-run) DRY_RUN=true ;;
-y|--yes) ASSUME_YES=true ;;
-h|--help) usage; exit 0 ;;
--install) install_self; exit 0 ;;
--) shift; break ;;
-*) die "Unknown option: $1" ;;
*)
[[ -z "$positional" ]] || die "Only one remote name may be supplied."
positional="$1"
;;
esac
shift
done
[[ -z "$positional" ]] || REMOTE="$positional"
validate_remote
}
github_ssh_parts() {
local url="$1" path owner repo
case "$url" in
git@github.com:*) path="${url#git@github.com:}" ;;
ssh://git@github.com/*) path="${url#ssh://git@github.com/}" ;;
*) return 1 ;;
esac
path="${path%.git}"
[[ "$path" == */* && "$path" != */*/* ]] || return 1
owner="${path%%/*}"
repo="${path#*/}"
[[ "$owner" =~ ^[A-Za-z0-9_.-]+$ && "$repo" =~ ^[A-Za-z0-9_.-]+$ ]] ||
return 1
printf '%s\n%s\n' "$owner" "$repo"
}
confirm_change() {
local answer
[[ "$ASSUME_YES" == true ]] && return
printf "Type 'yes' to update remote '%s': " "$REMOTE"
read -r answer < /dev/tty || die "Could not read confirmation."
[[ "$answer" == yes ]] || die "Cancelled without changing Git configuration."
}
main() {
parse_args "$@"
command -v git >/dev/null || die "git is required."
git rev-parse --git-dir >/dev/null 2>&1 || die "Run inside a Git repository."
local old_url parts owner repo new_url actual
old_url=$(git remote get-url "$REMOTE" 2>/dev/null) ||
die "Remote does not exist: $REMOTE"
parts=$(github_ssh_parts "$old_url") ||
die "Remote is not a recognized GitHub SSH URL: $old_url"
owner=${parts%%$'\n'*}
repo=${parts#*$'\n'}
new_url="https://github.com/$owner/$repo.git"
printf 'Remote: %s\nCurrent: %s\nProposed: %s\n' "$REMOTE" "$old_url" "$new_url"
printf "Rollback: git remote set-url '%s' '%s'\n" "$REMOTE" "$old_url"
[[ "$DRY_RUN" == false ]] || { printf 'Dry run: no change made.\n'; return; }
confirm_change
git remote set-url "$REMOTE" "$new_url"
actual=$(git remote get-url "$REMOTE")
if [[ "$actual" != "$new_url" ]]; then
git remote set-url "$REMOTE" "$old_url" || true
die "Verification failed; attempted to restore the original URL."
fi
printf 'Updated %s to %s.\n' "$REMOTE" "$new_url"
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment