macos-keychain-copy creates a generic-password item that command-line automation can read through macOS's /usr/bin/security tool.
The secret is entered interactively on the controlling terminal. It is never supplied as a command argument, written to shell history, or printed by this tool.
Credentials stored in Passwords/iCloud Keychain may require user presence and may not be available through the legacy security CLI. This tool creates a separate, explicitly weaker generic-password copy for automation while leaving the original credential untouched.
Use it only when that trade-off is appropriate for the credential.
- macOS
- Python 3.10 or newer
/usr/bin/security
With pipx:
pipx install git+https://github.com/Glutexo/macos-keychain-copy.gitOr in a virtual environment:
python3 -m venv .venv
. .venv/bin/activate
python -m pip install .keychain-copy store com.example.automation user@example.comThe underlying system command asks twice:
password data for new item: # enter the secret
retype password for new item: # enter the same secret again
Input is intentionally invisible. If macOS asks to unlock the login Keychain, authenticate in the separate system dialog; the command waits without imposing its own timeout.
store deliberately refuses to update an existing matching item. The system CLI cannot reliably narrow an existing item's older access-control list during -U updates. To replace a copy, explicitly run delete, review the result, and then run store again.
By default, /usr/bin/security is trusted to read the new item. Supplying any --trusted-app option replaces that default; repeat the option to name every application that should be trusted. Each value must be an absolute path to an existing executable:
keychain-copy store com.example.automation user@example.com \
--trusted-app /usr/bin/security \
--trusted-app /absolute/path/to/example-clientIf /usr/bin/security is omitted from --trusted-app, the tool does not perform its usual read-back verification after storing. This avoids requesting access through an application you deliberately chose not to trust. A later check may require authorization or report the item as inaccessible.
Optional metadata:
keychain-copy store com.example.automation user@example.com \
--label "Example automation credential" \
--kind "Application password"keychain-copy check com.example.automation user@example.comOutput and exit status:
accessible, exit0not accessible, exit1
keychain-copy delete com.example.automation user@example.comSuccessful deletion prints deleted and exits 0. A missing item prints not found and exits 1; other deletion failures are reported as errors on standard error and also exit 1.
Resolve the default user Keychain, perform a targeted lookup there, and consume the result inside the same process. Do not print it:
import shlex
import subprocess
security = "/usr/bin/security"
default_result = subprocess.run(
[security, "default-keychain", "-d", "user"],
check=True,
capture_output=True,
text=True,
)
keychains = shlex.split(default_result.stdout)
if len(keychains) != 1:
raise RuntimeError("could not determine the default user Keychain")
secret = subprocess.run(
[
security,
"find-generic-password",
"-s",
"com.example.automation",
"-a",
"user@example.com",
"-w",
keychains[0],
],
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
).stdout.rstrip(b"\r\n")
# Use secret in this process without logging it or placing it in child argv.
del secretFor network requests, prefer an in-process client so the secret does not appear in process arguments.
- The original credential is not read, modified, or deleted.
- A new generic-password item is created in the default Keychain.
- The default user Keychain path is resolved immediately before
store. The system add command writes to that default; the cached exact path is then passed explicitly tocheckanddelete, avoiding ambiguous matches elsewhere in the Keychain search list. - The secret is entered directly into
/usr/bin/securitythrough the terminal. - The
-woption is last and has no value, so the secret never appears in argv. checkdiscards both secret output and diagnostics.- Keychain ACL entries are limited to the executable paths passed with
-T; the default is/usr/bin/security. Trusting/usr/bin/securitystill allows any process running as the same user to invoke that binary and request the copied credential, so it is not caller-level isolation. - This copy is intentionally easier to automate and therefore weaker than a credential protected by user-presence checks.
See SECURITY.md for disclosure and operational guidance.
Run unit tests:
PYTHONPATH=src python3 -m unittest discover -s tests -vRun the opt-in real-Keychain integration test on macOS:
RUN_KEYCHAIN_INTEGRATION=1 \
PYTHONPATH=src \
python3 -m unittest tests.test_macos_integration -vThe integration test uses a PTY to exercise both real hidden prompts with a randomly generated synthetic value, verifies targeted retrieval, cleans up the temporary item, and never uses a real credential.
MIT