Skip to content

Instantly share code, notes, and snippets.

@tranch
Last active March 2, 2026 07:38
Show Gist options
  • Select an option

  • Save tranch/2d67a066e4ae1a12b89623771b28310e to your computer and use it in GitHub Desktop.

Select an option

Save tranch/2d67a066e4ae1a12b89623771b28310e to your computer and use it in GitHub Desktop.
Tools to help load and export environment variables from Apple’s Keychain.
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage: load2keychain [--service NAME] [--dry-run] [FILE]
Imports KEY=value pairs from a dotenv-style file into macOS Keychain as
generic-password items.
Default behavior:
- FILE defaults to .env
- service defaults to the current directory name
Storage layout:
- keychain service: <service>
- keychain account: <dotenv key>
- keychain password: <dotenv value>
This matches with-keychain, which loads all generic-password items whose
service equals the current directory name and turns each account into an
environment variable name.
Supported dotenv syntax:
- blank lines
- comment lines starting with #
- optional "export " prefix
- KEY=value pairs
- single-quoted or double-quoted whole values
Not supported:
- multiline values
- inline comments after unquoted values
- shell interpolation or command substitution
EOF
}
service_name_from_pwd() {
basename "$PWD"
}
trim_whitespace() {
local value="$1"
value="${value#"${value%%[![:space:]]*}"}"
value="${value%"${value##*[![:space:]]}"}"
printf '%s\n' "$value"
}
decode_double_quoted_value() {
local value="$1"
value="${value//\\\\/\\}"
value="${value//\\n/$'\n'}"
value="${value//\\r/$'\r'}"
value="${value//\\t/$'\t'}"
value="${value//\\\"/\"}"
printf '%s' "$value"
}
parse_env_line() {
local line="$1"
local key_part
local value_part
line="$(trim_whitespace "$line")"
[[ -n "$line" ]] || return 1
[[ "$line" =~ ^# ]] && return 1
if [[ "$line" == export\ * ]]; then
line="${line#export }"
line="$(trim_whitespace "$line")"
fi
[[ "$line" == *=* ]] || {
printf 'Unsupported line (missing =): %s\n' "$line" >&2
return 2
}
key_part="${line%%=*}"
value_part="${line#*=}"
key_part="$(trim_whitespace "$key_part")"
if [[ ! "$key_part" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]]; then
printf 'Unsupported key name: %s\n' "$key_part" >&2
return 2
fi
if [[ "$value_part" =~ ^\".*\"$ ]]; then
value_part="${value_part:1:${#value_part}-2}"
value_part="$(decode_double_quoted_value "$value_part")"
elif [[ "$value_part" =~ ^\'.*\'$ ]]; then
value_part="${value_part:1:${#value_part}-2}"
fi
printf '%s\t%s\n' "$key_part" "$value_part"
}
import_file() {
local file="$1"
local service="$2"
local dry_run="$3"
local line
local parsed
local env_name
local env_value
local imported_count=0
while IFS= read -r line || [[ -n "$line" ]]; do
if ! parsed="$(parse_env_line "$line")"; then
status=$?
if [[ $status -eq 1 ]]; then
continue
fi
exit "$status"
fi
env_name="${parsed%%$'\t'*}"
env_value="${parsed#*$'\t'}"
if [[ "$dry_run" == "1" ]]; then
printf '[dry-run] security add-generic-password -a %q -s %q -w %q -U\n' \
"$env_name" "$service" "$env_value"
else
security add-generic-password \
-a "$env_name" \
-s "$service" \
-w "$env_value" \
-U >/dev/null
printf 'Imported %s into service %s\n' "$env_name" "$service"
fi
imported_count=$((imported_count + 1))
done < "$file"
if [[ "$dry_run" == "1" ]]; then
printf 'Would import %d item(s) into service %s\n' "$imported_count" "$service"
else
printf 'Imported %d item(s) into service %s\n' "$imported_count" "$service"
fi
}
main() {
local file=".env"
local service=""
local dry_run=0
while [[ $# -gt 0 ]]; do
case "$1" in
--service)
[[ $# -ge 2 ]] || {
printf 'Missing value for --service\n' >&2
exit 1
}
service="$2"
shift 2
;;
--dry-run)
dry_run=1
shift
;;
-h|--help)
usage
exit 0
;;
-*)
printf 'Unknown option: %s\n' "$1" >&2
usage >&2
exit 1
;;
*)
file="$1"
shift
;;
esac
done
[[ -n "$service" ]] || service="$(service_name_from_pwd)"
if [[ ! -f "$file" ]]; then
printf 'File not found: %s\n' "$file" >&2
exit 1
fi
import_file "$file" "$service" "$dry_run"
}
main "$@"
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage: with-keychain <command> [args...]
Loads all generic-password items whose service matches the current directory
name, exports them as environment variables for the target command, then
replaces the shell with that command.
Each keychain account name becomes an environment variable name after:
1. uppercasing
2. replacing non [A-Z0-9_] characters with _
3. prefixing with _ if the name would start with a digit
EOF
}
service_name_from_pwd() {
basename "$PWD"
}
env_name_for_account() {
local account="$1"
local env_name
env_name="$(printf '%s' "$account" | tr '[:lower:]' '[:upper:]' | tr -c 'A-Z0-9_' '_')"
if [[ -z "$env_name" ]]; then
return 1
fi
if [[ "$env_name" =~ ^[0-9] ]]; then
env_name="_${env_name}"
fi
printf '%s\n' "$env_name"
}
list_accounts_for_service() {
local service="$1"
security dump-keychain 2>/dev/null | awk -v target="$service" '
/^class:/ {
if (in_genp && svce == target && acct != "") {
print acct
}
in_genp = index($0, "\"genp\"") > 0
svce = ""
acct = ""
next
}
in_genp && /"svce"<blob>=/ {
if (match($0, /"svce"<blob>="([^"]*)"/)) {
svce = substr($0, RSTART + 14, RLENGTH - 15)
}
next
}
in_genp && /"acct"<blob>=/ {
if (match($0, /"acct"<blob>="([^"]*)"/)) {
acct = substr($0, RSTART + 14, RLENGTH - 15)
}
next
}
END {
if (in_genp && svce == target && acct != "") {
print acct
}
}
' | awk '!seen[$0]++'
}
load_service_into_env() {
local service="$1"
local account
local env_name
local password
local -A exported_names=()
while IFS= read -r account; do
[[ -n "$account" ]] || continue
env_name="$(env_name_for_account "$account")" || {
printf 'Skipping keychain account with unsupported name: %s\n' "$account" >&2
continue
}
if [[ -n "${exported_names[$env_name]:-}" ]]; then
printf 'Refusing to continue: multiple accounts map to %s (%s, %s)\n' \
"$env_name" "${exported_names[$env_name]}" "$account" >&2
exit 1
fi
if ! password="$(security find-generic-password -s "$service" -a "$account" -w 2>/dev/null)"; then
printf 'Failed to read password for service=%s account=%s\n' "$service" "$account" >&2
exit 1
fi
exported_names["$env_name"]="$account"
export "$env_name=$password"
done < <(list_accounts_for_service "$service")
}
main() {
local service
if [[ $# -eq 0 ]]; then
usage >&2
exit 1
fi
service="$(service_name_from_pwd)"
load_service_into_env "$service"
exec "$@"
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment