WARNING: I've only dabbled with this in some scripts, and am not an expert. This document is based on my explorations, and has not been reviewed as actual best practices. USE AT YOUR OWN RISK!*
Securely managing macOS keychain items from the command line requires careful handling of access controls, error paths, and credential lifecycles. This guide collects recommended patterns and links to official documentation.
- Whitelist only necessary binaries
When adding an item, use-T
to restrict which executables can access it.security add-generic-password \ -s "MyService" \ -a "username" \ -w "${SECRET}" \ -T /usr/bin/security \ -T /usr/local/bin/my-helper-script
This prevents “Any Application” from retrieving your secret. See “Using the keychain to manage user secrets” (https://developer.apple.com/documentation/security/using-the-keychain-to-manage-user-secrets)
-
Use ACLs for finer control For code-signed tools,
set-key-partition-list
lets you grant access by keychain-access groups rather than binary paths.security set-key-partition-list \ -S apple-tool:,apple: \ -s "MyService" \ -k "${KEYCHAIN_PASS}" \ /path/to/MyApp.keychain
-
Avoid plaintext in scripts Don’t embed secrets in source. Add them interactively or via a CI/CD secret store:
security add-generic-password -s "MyService" -a "username" -w
-
Use
-w
for clean output When retrieving,-w
prints only the password, simplifying parsing:SECRET=$(security find-generic-password \ -s "MyService" \ -a "username" \ -w)
-
Quote all expansions Prevent injection or word-splitting:
security find-generic-password -s "$SERVICE" -a "$ACCOUNT" -w
-
Dedicated “automation” keychain Keep script secrets out of the user’s login keychain:
security create-keychain -p "${KC_PASS}" ci.keychain security set-keychain-settings -l 3600 ci.keychain # auto-lock 1 hr security unlock-keychain -p "${KC_PASS}" ci.keychain security list-keychains -d user -s ci.keychain \ ~/Library/Keychains/login.keychain-db
When done, you can
security lock-keychain ci.keychain
or delete it. -
Pin the search list Avoid fetching items from unintended keychains:
security list-keychains -d user -s /path/to/ci.keychain
-
Clean up After your script finishes, lock or remove temporary keychains:
security lock-keychain /path/to/ci.keychain security delete-keychain ci.keychain
-
Check exit codes Detect missing items, locked keychains, or ACL denials:
if ! VAL=$(security find-generic-password -s "$SVC" -a "$ACC" -w); then echo "❌ Couldn’t fetch secret (exit $?)" >&2 exit 1 fi
-
Fail fast Use
set -euo pipefail
at the top of your script to catch undefined variables and errors early.
-
Pre-authorize in non-GUI contexts macOS will block prompts under cron, SSH, or CI agents. Pre-grant
/usr/bin/security
(and any custom helper) with-T
or via Keychain Access → Access Control to avoid “User interaction is not allowed.” -
Use environment-isolated keychains In CI pipelines, spin up an ephemeral keychain just for that run, then destroy it, ensuring no leftover secrets.
-
Prefer native APIs for apps Shell scripts are convenient but apps should integrate via Keychain Services (Objective-C/Swift) or CryptoKit with proper entitlements and keychain-access-groups. See the Security framework reference (https://developer.apple.com/documentation/security)
-
Threat model your use case Understand who might target your automation, and choose expiration, ACLs, and encryption strategies accordingly. A simple script may suffice for personal use; high-security environments may demand hardware tokens (e.g., Secure Enclave) or HSM solutions.
security(1)
man page https://ss64.com/osx/security.html- StackOverflow Q&A on scripting
security
https://scriptingosx.com/2021/04/get-password-from-keychain-in-shell-scripts/ - CommandMasters examples https://commandmasters.com/commands/security-osx/
- Tech-Notes tutorial https://tech-notes.maxmasnick.com/storing-and-retrieving-passwords-in-bash-scripts-from-the-macos-keychain
Keep scripts minimal, tightly scoped, and always treat your keychain items as first-class security assets.
Some old Zsh code that worked in 2022, but hasn't been tested or security reviewed:
# DESC: Retrieves secret from keychain
# ARGS: $2 (REQUIRED) secret name to be retrieved from keychain
# OUTS: None
function _getsecret() {
local Function_Name Secret_Name Secret_Value
Function_Name=$(echo ${FUNCNAME[0]} | cut -c 2-)
#printf "\n\tparams: 0: ${0-} 1: ${1-} 2: ${2-} 3: ${3-}\n"
if [ $Function_Name == "${1-}" ]; then #being called from _main
#printf "\tcalled from _main\n"
shift
else # function being called internally
#printf "\tcalled internally\n"
Function_Name="getsecret"
fi
if [ -z "${1-}" ] ; then
script_exit "ERROR: '$script_name $Function_Name' requires a <secret.name> to search. See '$script_name $Function_Name --help'."
fi
if [[ "${1:-}" =~ ^-h$|^--help$ ]]
then
cat <<EOF
Name: getsecret from login keychain
Description:
Searches for retrieves the named secret from the current user's
MacOS login keychain.
Usage:
$script_name $Function_Name <secret.name>
$script_name $Function_Name -h | --help
Options:
-h --help Display this usage information.
Example:
profile getsecret christophera.github.token
EOF
return 0
fi
Current_User=$( /usr/bin/stat -f "%Su" /dev/console )
Secret_Value="$(security find-generic-password -a $Current_User -s "$1" -w $LOCAL_KEYCHAIN)"
## TBD: We need to do some better error handling here. For instance, if result is
## "security: SecKeychainSearchCopyNext: The specified item could not be found in the keychain."
## then we need to return this error so the function calling check and do the correct thing.
## in particular, in `profile check` the `Desired_Host_Name=` will be empty on first use as
## "$Device_Serial_Number.hostname" has not been set yet.
echo $Secret_Value
}
# DESC: Removes secret from keychain
# ARGS: $2 (REQUIRED) secret name on keychain to delete
# OUTS: None
function _rmsecret() {
local Function_Name Secret_Name Secret_Value
Function_Name=$(echo ${FUNCNAME[0]} | cut -c 2-)
if [ $Function_Name == "${1-}" ]; then #being called from _main
shift
else # function being called internally
Function_Name="rmsecret"
fi
if [ -z "${1-}" ] ; then
script_exit "ERROR: '$script_name $Function_Name' requires a <secret.name> to delete. See '$script_name $Function_Name --help'."
fi
if [[ "${1:-}" =~ ^-h$|^--help$ ]]
then
cat <<EOF
Name: rmsecret from login keychain
Description:
Searches for retrieves the named secret from the current user's
MacOS login keychain.
Usage:
$script_name $Function_Name <secret.name>
$script_name $Function_Name -h | --help
Options:
-h --help Display this usage information.
Example:
profile getsecret christophera.github.token
EOF
return 0
fi
Current_User=$( /usr/bin/stat -f "%Su" /dev/console )
Result="$(security delete-generic-password -a $Current_User -s "$1" $LOCAL_KEYCHAIN)"
#echo $Result
}
# DESC: Sets secret to local keychain
# ARGS: $2 (REQUIRED) secret name to be stored in local keychain
# ARGS: $3 (REQUIRED) value for secret to be stored in local keychain
# OUTS: None
function _setsecret() {
local Function_Name Secret_Name Secret_Value Result
Function_Name=$(echo ${FUNCNAME[0]} | cut -c 2-)
#printf "\n\tparams: 0: ${0-} 1: ${1-} 2: ${2-} 3: ${3-}\n"
if [ $Function_Name == "${1-}" ]; then #being called from _main
#printf "\tcalled from _main\n"
shift
else # function being called internally
#printf "\tcalled internally\n"
Function_Name="setsecret"
fi
if [[ -z "${0-}" || -z "${1-}" || -z "${2-}" ]] ; then
script_exit "ERROR: '$script_name $Function_Name' requires a <secret.name> & <value> to set. See '$script_name $Function_Name --help'."
fi
if [[ "${1:-}" =~ ^-h$|^--help$ ]]
then
cat <<EOF
Name: setsecret from login keychain
Description:
Sets the named secret in the current user's MacOS login keychain
to the value.
Usage:
$script_name $Function_Name <secret.name> <value>
$script_name $Function_Name -h | --help
Options:
-h --help Display this usage information.
Example:
profile set secret christophera.git.email [email protected]
EOF
return 0
fi
Current_User=$( /usr/bin/stat -f "%Su" /dev/console )
Result="$(security add-generic-password -D secret -U -a $Current_User -s "$1" -w "$2" $LOCAL_KEYCHAIN)"
#echo $Result
}