Skip to content

Instantly share code, notes, and snippets.

@nijikokun
Last active June 27, 2025 03:05
Show Gist options
  • Save nijikokun/87d3be3a47c7614190fed00b2d4af7c5 to your computer and use it in GitHub Desktop.
Save nijikokun/87d3be3a47c7614190fed00b2d4af7c5 to your computer and use it in GitHub Desktop.
pgrok - manage multiple ngrok accounts easily via the cli

pgrok

Easily work across multiple ngrok accounts, configurations, and environments—all from a single CLI.


Features

  • Fast profile switching for ngrok v2/v3 agent config
  • Manage unlimited ngrok config files ("profiles"), each with its own authtoken and settings
  • Set and update credentials securely (input hidden)
  • See all endpoints and tunnels for a profile in a pretty table
  • Works with ngrok v3 and the legacy agent (v2, v3)
  • Safe: automatically backs up your original ngrok config on first use (and makes it the default)
  • Built-in update checker (if you use the Gist installer)
  • Pure Bash + yq (for YAML parsing)

Installation

Install the script:

curl -fsSL https://gist.githubusercontent.com/raw/87d3be3a47c7614190fed00b2d4af7c5/pgrok.sh -o ~/pgrok
chmod +x ~/pgrok
sudo mv ~/pgrok ~/.local/bin/         # or another directory in your \$PATH

Make sure ~/.local/bin is in your $PATH. You can also use /usr/local/bin (may require sudo).

Dependency: Install yq v4+

  • macOS: brew install yq
  • Ubuntu: sudo snap install yq
  • Others: see yq releases

Quick Start

  1. Create a profile and set your authtoken:

    pgrok create dev --authtoken <your_ngrok_token>
    pgrok use dev
  2. Switch between profiles:

    pgrok list
    pgrok use prod
    pgrok use dev
  3. See endpoints and tunnels in the current profile:

    pgrok services
  4. Set authtoken or api_key (input hidden if value omitted):

    pgrok set authtoken
    pgrok set api_key
  5. Edit a profile’s config:

    pgrok edit dev

Usage

pgrok - Easily work across multiple ngrok accounts (profiles).

Usage:
  pgrok use <name>                                          # Activate profile by symlinking config
  pgrok list                                                # List all profiles
  pgrok current                                             # Show current profile
  pgrok create <name> [from] [--authtoken <token>]          # Create new profile, optional copy or token
  pgrok delete <name>                                       # Delete profile
  pgrok view [<name>]                                       # View contents of current or specified profile
  pgrok services [<name>]                                   # List the services that can be started on this profile
  pgrok edit [<name>]                                       # Edit profile config
  pgrok set authtoken [<value>]                             # Set authtoken for current profile
  pgrok set api_key [<value>]                               # Set api_key for current profile
  pgrok cleanup                                             # Remove all profiles and restore original config
  pgrok restore                                             # Restore backed-up original config
  pgrok run [--nd] -- <args>                                # Run ngrok or nd ngrok with current profile

Examples:
  pgrok create dev --authtoken <token> && ngrok http 80
  pgrok set authtoken mytoken123        # sets directly
  pgrok set authtoken                   # prompts securely
  pgrok set api_key                     # prompts securely
  pgrok services                        # list of services for current profile

Notes:
  - the `services` and `set` commands require yq (https://github.com/mikefarah/yq)
  - the `edit` command respects the global \$EDITOR variable, yours is "$EDITOR"

How it works

  • On first run, pgrok backs up your ngrok config and creates a "default" profile from it.
  • Each profile is a folder under ~/.ngrok-profiles/, containing its own ngrok.yml.
  • When you use a profile, pgrok symlinks your selected config to ngrok’s default config path, so you can use ngrok as usual.
  • pgrok set authtoken and pgrok set api_key use hidden input if value is not provided, so you never leak secrets to your shell history.

FAQ

Q: Can I use this with multiple computers or share profiles?

A: Yes! Copy the ~/.ngrok-profiles directory between systems.

Q: How do I update pgrok? A:

curl -fsSL https://gist.githubusercontent.com/raw/87d3be3a47c7614190fed00b2d4af7c5/pgrok.sh -o ~/pgrok
chmod +x ~/pgrok

Q: What if I break my config?

pgrok restore   # restores the original ngrok config
pgrok cleanup   # removes all profiles and resets to original config (this will delete all backups!!)

Credits


License

MIT License (or specify your preferred license)

#!/bin/bash
PGROK_VERSION="0.0.2"
PGROK_AUTHOR="nijikokun"
PROFILE_DIR="$HOME/.ngrok-profiles"
BACKUP_FILE="$PROFILE_DIR/.backup"
CURRENT_FILE="$PROFILE_DIR/.current"
DEFAULT_PROFILE="default"
# Determine if we're running in dev mode
USE_ND=0
if [[ "$1" == "--nd" ]]; then
USE_ND=1
shift
fi
# Use the appropriate ngrok runner
if [[ "$USE_ND" -eq 1 ]]; then
NGROK_PREFIX="nd ngrok --"
else
NGROK_PREFIX="ngrok"
fi
# Detect default config path via ngrok itself
CONFIG_FILE=$(eval "$NGROK_PREFIX config check 2>/dev/null" | grep -oE '/.*ngrok.yml')
if [ -z "$CONFIG_FILE" ]; then
echo "Could not detect ngrok config path using '$NGROK_PREFIX config check'"
exit 1
fi
first_run_setup() {
# Create dirs if missing
mkdir -p "$PROFILE_DIR"
mkdir -p "$(dirname "$CONFIG_FILE")"
# First run setup
if [ ! -f "$CURRENT_FILE" ]; then
echo "[ngrok-profile] First-time setup..."
# Backup current config (if any)
if [ -f "$CONFIG_FILE" ]; then
cp "$CONFIG_FILE" "$PROFILE_DIR/.backup"
echo "[ngrok-profile] Backed up $CONFIG_FILE to $PROFILE_DIR/.backup"
mkdir -p "$PROFILE_DIR/default"
cp "$CONFIG_FILE" "$PROFILE_DIR/default/ngrok.yml"
echo "[ngrok-profile] Created default profile from current config"
else
# Create empty default profile
mkdir -p "$PROFILE_DIR/default"
echo -e "version: \"3\"\nagent:\n authtoken: \"\"" > "$PROFILE_DIR/default/ngrok.yml"
echo "[ngrok-profile] Created blank default profile"
fi
# Link default profile
ln -sf "$PROFILE_DIR/default/ngrok.yml" "$CONFIG_FILE"
# Double check again
[ -z "$CONFIG_FILE" ] && {
echo "Could not detect ngrok config path using 'ngrok config check'";
exit 1;
}
echo "default" > "$CURRENT_FILE"
echo "[ngrok-profile] Linked default profile to ngrok config path"
echo "[ngrok-profile] Setup complete. Now using profile: default"
fi
}
edit_profile_config() {
local file="$1"
if [ -n "$EDITOR" ]; then
"$EDITOR" "$file"
elif command -v nano >/dev/null 2>&1; then
nano "$file"
else
vi "$file"
fi
}
get_path() {
echo "$PROFILE_DIR/$1/ngrok.yml"
}
get_current() {
[ -f "$CURRENT_FILE" ] && cat "$CURRENT_FILE" || echo "$DEFAULT_PROFILE"
}
set_current() {
echo "$1" > "$CURRENT_FILE"
}
backup_original_config() {
if [ ! -f "$BACKUP_FILE" ] && [ -f "$CONFIG_FILE" ]; then
cp "$CONFIG_FILE" "$BACKUP_FILE"
echo "Backed up original config to $BACKUP_FILE"
fi
}
restore_original_config() {
if [ -f "$BACKUP_FILE" ]; then
cp "$BACKUP_FILE" "$CONFIG_FILE"
echo "Restored original ngrok config"
else
echo "No backup found to restore"
fi
}
link_profile() {
local profile="$1"
local profile_config
profile_config="$(get_path "$profile")"
[ -f "$profile_config" ] || { echo "Profile '$profile' not found at $profile_config"; exit 1; }
backup_original_config
ln -sf "$profile_config" "$CONFIG_FILE"
set_current "$profile"
echo "Now using profile: $profile"
}
print_service_row() {
local type="$1"
local name="$2"
local url="$3"
local upstream_url="$4"
# If upstream_url or addr is just digits, show as localhost:port
if [[ "$upstream_url" =~ ^[0-9]+$ ]]; then
upstream_url="::$upstream_url"
fi
if [[ "$url" =~ ^[0-9]+$ ]]; then
url="::$url"
fi
printf "%-10s %-15s %-35s %-15s\n" "$type" "$name" "$url" "$upstream_url"
}
check_for_update() {
# Set this to your public Gist raw version URL
GIST_ID="87d3be3a47c7614190fed00b2d4af7c5"
GIST_VERSION_URL="https://gist.githubusercontent.com/raw/${GIST_ID}/pgrok-version.txt"
GIST_SOURCE_URL="https://gist.githubusercontent.com/raw/${GIST_ID}/pgrok.sh"
latest_version=$(curl -fsSL "$GIST_VERSION_URL" 2>/dev/null | head -1 | tr -d '[:space:]')
if [ -z "$latest_version" ]; then
return
fi
if [ "$PGROK_VERSION" != "$latest_version" ]; then
echo
echo "🔔 A new version of pgrok is available: $latest_version (you have $PGROK_VERSION)"
echo
echo " To update run: "
echo " curl -fsSL ${GIST_SOURCE_URL} -o ~/pgrok && chmod +x ~/pgrok && sudo mv ~/pgrok /usr/lib/bin"
echo
fi
}
usage() {
cat <<EOF
pgrok - Easily work across multiple ngrok accounts (profiles).
Usage:
pgrok use <name> # Activate profile by symlinking config
pgrok list # List all profiles
pgrok current # Show current profile
pgrok create <name> [from] [--authtoken <token>] # Create new profile, optional copy or token
pgrok delete <name> # Delete profile
pgrok view [<name>] # View contents of current or specified profile
pgrok services [<name>] # List the services that can be started on this profile
pgrok edit [<name>] # Edit profile config
pgrok set authtoken [<value>] # Set authtoken for current profile
pgrok set api_key [<value>] # Set api_key for current profile
pgrok cleanup # Remove all profiles and restore original config
pgrok restore # Restore backed-up original config
pgrok run [--nd] -- <args> # Run ngrok or nd ngrok with current profile
Examples:
pgrok create dev --authtoken <token> && ngrok http 80 # create new profile and run ngrok on it
pgrok set authtoken mytoken123 # sets directly
pgrok set authtoken # prompts securely
pgrok set api_key # prompts securely
pgrok services # list of services for current profile
Notes:
- the services and set commands require yq (https://github.com/mikefarah/yq)
- the edit command respects the global \$EDITOR variable, yours is "$EDITOR"
EOF
exit 1
}
# Check for updates
check_for_update
# First time setup
first_run_setup
# Command Handler
case "$1" in
use)
[ -z "$2" ] && usage
link_profile "$2"
;;
list)
current="$(get_current)"
echo "Available profiles:"
find "$PROFILE_DIR" -name "ngrok.yml" | sed "s|.*/\([^/]*\)/ngrok.yml|\1|" | while read -r name; do
if [ "$name" = "$current" ]; then
echo " * $name (current)"
else
echo " $name"
fi
done
;;
current)
echo "Current profile: $(get_current)"
;;
create)
[ -z "$2" ] && usage
name="$2"
from=""
authtoken=""
shift 2
while [[ $# -gt 0 ]]; do
case "$1" in
--authtoken)
authtoken="$2"
shift 2
;;
*)
from="$1"
shift
;;
esac
done
dest="$(get_path "$name")"
mkdir -p "$(dirname "$dest")"
if [ -n "$from" ]; then
cp "$(get_path "$from")" "$dest"
echo "Created profile '$name' from '$from'"
link_profile "$name"
else
echo -e "version: \"3\"\nagent:" > "$dest"
if [ -n "$authtoken" ]; then
echo " authtoken: \"$authtoken\"" >> "$dest"
else
echo " authtoken: \"\"" >> "$dest"
fi
echo "Created new profile '$name'"
link_profile "$name"
fi
;;
delete)
[ -z "$2" ] && usage
rm -rf "$PROFILE_DIR/$2"
echo "Deleted profile '$2'"
;;
view)
profile="${2:-$(get_current)}"
config="$(get_path "$profile")"
if [ -f "$config" ]; then
cat "$config"
echo "" # avoid `%` at the end sometimes
else
echo "[ngrok-profile] Profile '$profile' does not exist at $config"
exit 1
fi
;;
services)
profile="${2:-$(get_current)}"
config="$(get_path "$profile")"
if [ ! -f "$config" ]; then
echo "[ngrok-profile] Profile '$profile' does not exist at $config"
exit 1
fi
if ! command -v yq >/dev/null 2>&1; then
echo "[ngrok-profile] 'yq' is required for this command. Install via 'brew install yq' or https://github.com/mikefarah/yq"
exit 1
fi
# Collect v3 endpoints: name, url, upstream_url, type
endpoints=$(yq -r '
.endpoints[]? |
["endpoint", .name, .url, (.upstream.url // "")]
| @tsv
' "$config")
# Collect v2 tunnels: name, proto://domain, addr, type
tunnels=$(yq -r '
.tunnels // {} | to_entries[] |
[
"tunnel",
.key,
((.value.proto // "") + "://" + (.value.domain // "")),
(.value.addr // "")
] | @tsv
' "$config")
# Print merged table header
printf "%-10s %-15s %-35s %-15s\n" "TYPE" "SERVICE NAME" "URL" "UPSTREAM URL"
printf "%-10s %-15s %-35s %-15s\n" "----" "------------" "---" "------------"
# Print endpoints
if [ -n "$endpoints" ]; then
echo "$endpoints" | while IFS=$'\t' read -r type name url upstream_url; do
print_service_row "$type" "$name" "$url" "$upstream_url"
done
fi
# Print tunnels
if [ -n "$tunnels" ]; then
echo "$tunnels" | while IFS=$'\t' read -r type name url upstream_url; do
print_service_row "$type" "$name" "$url" "$upstream_url"
done
fi
# If nothing found
if [ -z "$endpoints" ] && [ -z "$tunnels" ]; then
echo "[ngrok-profile] No endpoints or tunnels found in profile '$profile'."
fi
;;
set)
profile="$(get_current)"
config="$(get_path "$profile")"
[ ! -f "$config" ] && { echo "[ngrok-profile] Profile '$profile' does not exist at $config"; exit 1; }
shift # remove 'set'
if [[ $# -eq 2 ]]; then
key="$1"
value="$2"
elif [[ $# -eq 1 ]]; then
key="$1"
# Prompt securely
if [[ "$key" != "authtoken" && "$key" != "api_key" ]]; then
echo "Error: Can only set authtoken or api_key"
exit 1
fi
echo -n "Enter value for $key (input hidden): "
read -s value
echo
else
echo "Usage: pgrok set authtoken <value>"
echo " or: pgrok set api_key <value>"
echo " or: pgrok set authtoken # will prompt securely"
exit 1
fi
if [[ "$key" != "authtoken" && "$key" != "api_key" ]]; then
echo "Error: Can only set authtoken or api_key"
exit 1
fi
if ! command -v yq >/dev/null 2>&1; then
echo "[ngrok-profile] 'yq' is required for this command."
exit 1
fi
yq -i ".agent.$key = \"$value\"" "$config"
echo "[ngrok-profile] Set $key for profile '$profile'."
;;
edit)
profile="${2:-$(get_current)}"
${EDITOR:-nano} "$(get_path "$profile")"
;;
cleanup)
echo "[ngrok-profile] Cleaning up all profiles and restoring original ngrok config..."
# If the config file is a symlink, remove it to avoid broken links
if [ -L "$CONFIG_FILE" ]; then
rm "$CONFIG_FILE"
echo "[ngrok-profile] Removed symlinked ngrok config."
fi
# Restore backup if it exists
if [ -f "$BACKUP_FILE" ]; then
cp "$BACKUP_FILE" "$CONFIG_FILE"
if cmp -s "$BACKUP_FILE" "$CONFIG_FILE"; then
echo "[ngrok-profile] Restored original ngrok config from backup."
else
echo "[ngrok-profile] ERROR: Failed to restore config from backup!" >&2
exit 1
fi
else
# No backup, create blank
echo -e 'version: "3"\nagent:\n authtoken: ""' > "$CONFIG_FILE"
if [ -s "$CONFIG_FILE" ]; then
echo "[ngrok-profile] No backup found, created blank ngrok config."
else
echo "[ngrok-profile] ERROR: Failed to create blank ngrok config!" >&2
exit 1
fi
fi
# Remove profiles
if [ -d "$PROFILE_DIR" ]; then
rm -rf "$PROFILE_DIR"
echo "[ngrok-profile] Removed all profiles and profile metadata."
fi
echo "[ngrok-profile] Cleanup complete."
;;
restore)
restore_original_config
;;
run)
[ "$1" == "--" ] && shift
exec $NGROK_RUN "$@"
;;
*)
usage
;;
esac
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment