Created
February 25, 2026 05:14
-
-
Save samkcarlile/552f4428c3bf4103b6eec1d1cb6db430 to your computer and use it in GitHub Desktop.
A ZSH function to manage API keys and other secret environment variables π»
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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 'π [1;33m%s[0m - [2m%s[0m%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