Skip to content

Instantly share code, notes, and snippets.

@samkcarlile
Created February 25, 2026 05:14
Show Gist options
  • Select an option

  • Save samkcarlile/552f4428c3bf4103b6eec1d1cb6db430 to your computer and use it in GitHub Desktop.

Select an option

Save samkcarlile/552f4428c3bf4103b6eec1d1cb6db430 to your computer and use it in GitHub Desktop.
A ZSH function to manage API keys and other secret environment variables πŸ‘»
# sts - Secrets Manager
# Manages environment variables backed by macOS Keychain via `security`.
# Config file maps ENV_VAR_NAME to keychain account name (one per line).
#
# Usage:
# sts # Toggle all keys (load/unload)
# sts load [name] # Load all keys, or a specific key by name
# sts unload [name] # Unload all keys, or a specific key by name
# sts i # Interactive fzf session for managing keys
: ${STS_CONFIG:="$HOME/.sts"}
# Ensure config file exists
[[ -f "$STS_CONFIG" ]] || touch "$STS_CONFIG"
# --- Helpers ---
_sts_parse_config() {
# Reads config, outputs lines of "ENV_NAME<tab>ACCOUNT_NAME"
while IFS='=' read -r name account || [[ -n "$name" ]]; do
name="${name## }"; name="${name%% }"
account="${account## }"; account="${account%% }"
[[ -z "$name" || "$name" == \#* ]] && continue
printf '%s %s
' "$name" "$account"
done < "$STS_CONFIG"
}
_sts_load_key() {
local name="$1" account="$2"
local value
value=$(security find-generic-password -a "$account" -w 2>/dev/null)
if [[ -n "$value" ]]; then
export "$name"="$value"
gum log --level info "Loaded $name"
return 0
else
gum log --level error "Failed to load $name (account: $account)"
return 1
fi
}
_sts_unload_key() {
local name="$1"
if [[ -n "${(P)name}" ]]; then
unset "$name"
gum log --level info "Unloaded $name"
return 0
fi
return 1
}
_sts_find_key() {
# Find a key by env var name, prints "name<tab>account" or returns 1
local target="$1"
_sts_parse_config | while IFS=$' ' read -r name account; do
if [[ "$name" == "$target" ]]; then
printf '%s %s
' "$name" "$account"
return 0
fi
done
return 1
}
_sts_add_key() {
local name account value
name=$(gum input --header "New Secret" --placeholder "ENV_VAR_NAME" --prompt "Name: " --char-limit 80) || return 1
[[ -z "$name" ]] && return 1
account=$(gum input --header "New Secret" --placeholder "Keychain account name" --prompt "Account: " --char-limit 120) || return 1
[[ -z "$account" ]] && return 1
value=$(gum input --header "New Secret" --placeholder "secret value" --prompt "Value: " --password --char-limit 500) || return 1
[[ -z "$value" ]] && return 1
# Store in keychain
if ! security add-generic-password -a "$account" -s "$name" -w "$value" -U 2>/dev/null; then
gum log --level error "Failed to store in keychain"
return 1
fi
# Append to config
echo "${name}=${account}" >> "$STS_CONFIG"
gum log --level info "Added $name"
}
_sts_edit_key() {
local line="$1"
local name account
name=$(echo "$line" | sed 's/.*πŸ”‘ //' | awk '{print $1}')
local match
match=$(_sts_find_key "$name")
[[ -z "$match" ]] && return 1
account=$(echo "$match" | cut -f2)
local new_value
new_value=$(gum input --header "Edit: $name" --placeholder "new secret value (blank to cancel)" --prompt "Value: " --password --char-limit 500) || return 1
[[ -z "$new_value" ]] && return 0
if security add-generic-password -a "$account" -s "$name" -w "$new_value" -U 2>/dev/null; then
gum log --level info "Updated $name"
else
gum log --level error "Failed to update $name"
fi
}
_sts_remove_key() {
local line="$1"
local name account
name=$(echo "$line" | sed 's/.*πŸ”‘ //' | awk '{print $1}')
local match
match=$(_sts_find_key "$name")
[[ -z "$match" ]] && return 1
account=$(echo "$match" | cut -f2)
gum confirm "Remove $name?" || return 0
# Remove from keychain
security delete-generic-password -a "$account" -s "$name" >/dev/null 2>&1
# Remove from config (escape special chars for sed)
local escaped_name="${name//\//\/}"
local escaped_account="${account//\//\/}"
sed -i '' "/^${escaped_name}=${escaped_account}$/d" "$STS_CONFIG"
gum log --level info "Removed $name"
}
_sts_format_lines() {
_sts_parse_config | while IFS=$' ' read -r name account; do
local loaded=""
if [[ -n "${(P)name}" ]]; then
loaded=" βœ“"
fi
printf 'πŸ”‘ %s - %s%s
' "$name" "$account" "$loaded"
done
}
# --- Main ---
sts() {
local action="$1"
shift 2>/dev/null
case "$action" in
l|load)
local target="$1"
local failed=0
if [[ -n "$target" ]]; then
local match
match=$(_sts_find_key "$target")
if [[ -z "$match" ]]; then
gum log --level error "Unknown key: $target"
return 1
fi
local acct=$(echo "$match" | cut -f2)
_sts_load_key "$target" "$acct" || failed=1
else
local lines=("${(@f)$(_sts_parse_config)}")
for entry in "${lines[@]}"; do
[[ -z "$entry" ]] && continue
local lname="${entry%%$' '*}"
local lacct="${entry#*$' '}"
_sts_load_key "$lname" "$lacct" || failed=1
done
fi
return $failed
;;
u|unload)
local target="$1"
if [[ -n "$target" ]]; then
if ! _sts_find_key "$target" >/dev/null; then
gum log --level error "Unknown key: $target"
return 1
fi
_sts_unload_key "$target"
else
local count=0
local lines=("${(@f)$(_sts_parse_config)}")
for entry in "${lines[@]}"; do
[[ -z "$entry" ]] && continue
local uname="${entry%%$' '*}"
_sts_unload_key "$uname" && ((count++))
done
[[ $count -eq 0 ]] && gum log --level warn "No keys loaded"
fi
;;
s|status)
local lines=("${(@f)$(_sts_parse_config)}")
local loaded=0 total=0
for entry in "${lines[@]}"; do
[[ -z "$entry" ]] && continue
((total++))
local sname="${entry%%$' '*}"
[[ -n "${(P)sname}" ]] && ((loaded++))
done
echo ""
printf ' %s %s
' "$(gum style --bold --foreground 212 'sts')" "$(gum style --italic "$loaded/$total loaded")"
echo ""
_sts_format_lines
echo ""
;;
i)
local header="ctrl-a:add | ctrl-e:edit | ctrl-x:remove | ctrl-l:load | ctrl-u:unload"
local result key selection
while true; do
result=$(_sts_format_lines | FZF_DEFAULT_OPTS='' fzf --ansi \
--header "$header" \
--layout reverse \
--height 50% \
--no-sort \
--expect "ctrl-a,ctrl-e,ctrl-x,ctrl-l,ctrl-u")
[[ -z "$result" ]] && break
key=$(head -1 <<< "$result")
selection=$(tail -1 <<< "$result")
case "$key" in
ctrl-a) _sts_add_key ;;
ctrl-e) _sts_edit_key "$selection" ;;
ctrl-x) _sts_remove_key "$selection" ;;
ctrl-l)
if [[ -n "$selection" ]]; then
local lname=$(echo "$selection" | sed 's/.*πŸ”‘ //' | awk '{print $1}')
sts load "$lname"
fi
;;
ctrl-u)
if [[ -n "$selection" ]]; then
local uname=$(echo "$selection" | sed 's/.*πŸ”‘ //' | awk '{print $1}')
sts unload "$uname"
fi
;;
*) break ;;
esac
done
;;
"")
# Toggle: if any key is loaded, unload all; otherwise load all
local any_loaded=0
local lines=("${(@f)$(_sts_parse_config)}")
for entry in "${lines[@]}"; do
[[ -z "$entry" ]] && continue
local kname="${entry%%$' '*}"
[[ -n "${(P)kname}" ]] && any_loaded=1 && break
done
if [[ $any_loaded -eq 1 ]]; then
sts unload
else
sts load
fi
;;
*)
echo "sts - s(ecre)ts manager
USAGE
sts Toggle all keys (load/unload)
sts l, load [name] Load all keys or a specific key
sts u, unload [name] Unload all keys or a specific key
sts s, status Show loaded/unloaded status
sts i Interactive key manager (fzf)" | bat --style=plain --no-pager -l help
;;
esac
}
# --- Completions ---
_sts() {
local -a subcommands
subcommands=(
'l:Load secrets into environment'
'load:Load secrets into environment'
'u:Unload secrets from environment'
'unload:Unload secrets from environment'
's:Show loaded/unloaded status'
'status:Show loaded/unloaded status'
'i:Interactive key manager'
)
if (( CURRENT == 2 )); then
_describe 'command' subcommands
elif (( CURRENT == 3 )); then
case "${words[2]}" in
l|load|u|unload)
local -a keys
keys=(${(f)"$(_sts_parse_config | cut -f1)"})
_describe 'key' keys
;;
esac
fi
}
(( $+functions[compdef] )) && compdef _sts sts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment