macOS Keychain for autonomous coding tools
Learn where macOS Keychain protects credentials, where that protection stops, and why autonomous tools need authenticated, brokered execution.

The macOS Keychain is good at protecting a credential while nobody is using it. That does not make it a complete security boundary for an autonomous coding tool. Once a process can retrieve an API token or private key, that process can usually copy it, print it, send it elsewhere, or keep using it after the operator thinks the task has ended.
That distinction matters more with an agent than with a conventional desktop app. A human selects a menu command and sees the immediate result. An agent interprets text, invokes tools, follows output, and may continue for hours. Keychain storage can answer, «Who may read this secret?» Brokered execution answers the harder question, «May this particular process perform this particular action now, without ever receiving the secret?» PAM session authentication answers a related but different question about who opened a login session. Treating those three controls as interchangeable leaves gaps that are easy to miss in a demo and painful to reconstruct after an incident.
Keychain protects stored credentials, not every use
Keychain encrypts stored secret material and mediates access through macOS security services. It is a much better home for a token than a .env file, shell history, a repository, or an agent configuration file. An item can carry access controls, and the system can ask for user presence before releasing it. Those are real protections against casual file theft and unauthorized reads.
The boundary ends at disclosure. If a coding tool calls a helper that returns a token as text, the token now exists in the helper, its IPC path, and the receiving process. It can appear in a debug message, exception object, transcript, crash report, swap, copied environment, child process, or malicious outbound request. Keychain cannot recall those bytes. Locking the keychain later does not invalidate a bearer token already copied into memory.
Apple's Keychain Services documentation describes storing and retrieving passwords, keys, and certificates. That wording is exact and useful: retrieval is a supported outcome. Developers sometimes describe Keychain as if it turns every secret into a non-exportable signing key. It does not. Some cryptographic keys can be created with access controls and used through security APIs without exporting their private material, but a general API token must still become an HTTP header somewhere. The security question is which trusted component builds and sends that request.
You can expose the problem with a small test account rather than a production token. Store a disposable value, let the same execution path used by the agent retrieve it, then inspect what the process can do with the returned bytes:
security find-generic-password -a agent-test -s example-api -w
The output shape is the secret followed by a newline:
test_token_7f3a...
If the agent or an agent-controlled shell can run that command successfully, the storage layer has already made its decision. Redaction in the terminal only changes what a person sees. It does not stop the process from redirecting the output, encoding it, or placing it in a request.
An allowed reader can become an unintended exporter
The risky principal is often not an unknown attacker. It is the coding tool that you deliberately allowed to work. An autonomous process needs broad access to files, build tools, package commands, and network clients to be useful. Give that same process readable credentials and a prompt injection, compromised dependency, or mistaken command can turn ordinary access into credential export.
Code-signing requirements can narrow which application accesses a Keychain item. They help when the threat is a different unsigned or differently signed process asking Keychain for the item. They do much less when the approved application is extensible, launches shells, loads plugins, accepts untrusted repository instructions, or exposes a tool protocol. The signed binary may be exactly the process executing attacker-influenced behavior.
There is also a delegation problem. Suppose an approved desktop helper reads a token and hands it to an agent through standard input. Keychain sees the approved helper, not the eventual consumer. Suppose the helper instead places the token in an environment variable for a command. Every child that inherits that environment may receive it. The original access decision says nothing about which downstream operation was intended.
A useful review traces the secret as data, not as a box in an architecture drawing. Ask four concrete questions:
- Which process receives the plaintext first?
- Can any child process, plugin, shell command, or transcript receive a copy?
- Can the recipient choose the destination, HTTP method, path, or SSH host?
- What event ends the recipient's authority, and does it erase already disclosed material?
Teams often answer the first question and skip the other three. That produces a design that looks protected at rest but behaves like a long-lived plaintext credential once an agent starts.
PAM proves a login event, not an agent's intent
Pluggable Authentication Modules, or PAM, let a service apply an authentication policy at a login boundary. On macOS, services such as sudo, login, and remote login paths use named PAM configurations. A PAM stack can authenticate a user, check an account, establish credentials, and open or close a session. That is useful for deciding whether a person may begin a privileged operating-system session.
PAM does not normally mediate each HTTP API call an already running agent makes. After authentication, the process acts with the operating-system identity and capabilities it received. If the process can read a token, PAM does not attach the earlier Touch ID gesture or password entry to one later request. It also does not understand that POST /releases is more sensitive than GET /status unless another component implements that application-level distinction.
The word «session» causes confusion because it names several unrelated lifetimes. A PAM session may surround a login or sudo activity. A terminal session may persist in a window. An agent session may mean one process run, one conversation, or a resumed task. An API session may be the lifetime of a bearer token. Ending one does not necessarily end the others.
A concrete failure looks like this: an operator authenticates to start a shell, launches an agent, and approves Keychain access. The agent reads a deployment token. Hours later the operator closes the visible conversation, but a child process remains alive with the token in its environment. PAM successfully authenticated the original session, and Keychain correctly authorized the read. Neither control prevented continued use because neither owned the action lifetime.
Use PAM when the decision is whether an operating-system account may establish a session. Do not cite PAM authentication as evidence that every action inside that session carries fresh human intent. That claim asks PAM to provide semantics it does not have.
Authentication and authorization answer different questions
Authentication establishes who a principal is, or at least which credential it presented. Authorization decides what that principal may do in a particular context. Secret storage preserves material until an authorized read or cryptographic operation occurs. These controls can support one another, but none substitutes for the others.
For autonomous tools, name the principal precisely. «The user» is too vague. The operator at the keyboard, the signed agent executable, a newly spawned agent process, an MCP server, a shell child, and the remote API account are different principals. If an approval applies to all of them at once, the design should say so plainly.
The most practical unit for routine work is often the process run. An operator can authorize a recognized agent process until it exits, then require a new decision for a new run. That boundary prevents a silent restart from inheriting old approval. For a highly sensitive credential, authorization should be narrower still: require approval for each use, even within an accepted run.
Per-call approval is not automatically safe. A dialog that says only «Allow network access?» gives the operator almost nothing to judge. The approval surface should identify the requesting process and show the action details that change risk, such as the credential alias, method and destination for HTTP, or user and host for SSH. It should also resist approval fatigue. If every harmless read triggers the same interruption as a production write, people will click through both.
Process identity also deserves more care than a path or display name. Both can be copied. On macOS, a code-signing requirement can tie the decision to a signing authority and designated requirement, while the process identifier distinguishes one running instance from the next. Show the operator the signing fact that the system actually verified. Do not reduce it to a friendly application name that an untrusted binary can imitate.
Code signing still does not prove that the process is behaving well. It establishes provenance under the signing model, not intent. A correctly signed agent can obey hostile instructions from a repository, and a signed plugin host can load attacker-controlled content. Session approval should therefore mean «this identified process may ask the broker to act until it exits,» not «all requests from this software are safe.»
Define how descendants relate to that session before implementation. Automatically trusting every child makes a long process tree hard to revoke and lets a general shell inherit more authority than expected. Forcing every short-lived helper through a new human prompt makes automation unusable. A clean design keeps the authorization at the broker connection owned by the approved agent process. Children do not inherit credentials, and they reach protected actions only through an explicitly mediated channel whose parent session can be revoked.
Process exit is a useful automatic end, but it is not the only end. The operator needs an immediate revoke control for a run that starts behaving strangely. The broker should reject queued and future actions after revocation, close or invalidate the session handle, and record the decision. Whether an already transmitted remote request can be stopped depends on the protocol and remote service, so the interface must not pretend that revocation rewinds completed work.
Test restart semantics as well. Some agent clients update themselves, reconnect after a helper crash, or resume a conversation in a new process. Convenience code often treats that as the same logical session. The security boundary should treat a new process as new unless a deliberate, authenticated handoff protocol proves continuity. A conversation identifier supplied by the agent is not proof because the agent can copy it.
This yields a decision ladder rather than one oversized permission:
- Is the credential store unlocked and available at all?
- Is this new agent process allowed to act during its lifetime?
- Does this particular credential require a decision on every use?
- Will a trusted executor constrain and record the requested operation?
The first three questions govern authority. The fourth governs execution and evidence. Combining them into a vague «trusted agent» flag makes revocation and incident review much harder.
Brokered execution keeps the credential out of agent memory
A broker changes the interface from «give me the token» to «perform this defined action for me.» The agent supplies a request containing non-secret parameters. The broker checks authority, retrieves the credential inside its own boundary, injects it into the outbound protocol, executes the operation, and returns only the result the agent needs.
For an HTTP API, the request to a broker might look like this:
{
"credential": "staging-release-api",
"method": "POST",
"url": "https://api.example.invalid/v1/releases",
"headers": {"content-type": "application/json"},
"body": {"commit": "8a31c2e", "channel": "candidate"}
}
The broker adds the bearer, basic, or custom authentication header after authorization. The agent-visible response can retain status, selected headers, and body while omitting the injected credential:
{
"status": 201,
"headers": {"content-type": "application/json"},
"body": {"release_id": "rel_1042", "state": "queued"}
}
This is a sharper boundary than redaction. Redaction assumes the secret entered an untrusted data path and tries to hide recognizable forms afterward. Brokered execution avoids disclosure in the first place. It also makes revocation effective: once the broker denies the next request, the agent has no cached bearer token with which to bypass it.
The broker still needs careful output handling. An API can echo request headers, an SSH command can print environment data, and a verbose client can include authentication material in an error. Filter known credential locations before returning results, cap diagnostic detail, and test failure paths as aggressively as successful calls. «The agent never receives credentials» must cover errors and logs, not only the normal response.
Brokered SSH follows the same idea but needs a protocol-aware executor. The agent asks to run a command against a named host identity. The executor uses the private key internally, performs host verification, runs the command, and returns stdout, stderr, and exit status. Handing the agent a temporary private-key file is still disclosure, even if cleanup runs later.
A broker limits credential theft, not harmful authorized actions
Keeping a token away from the agent removes a major class of failures, but it does not make the agent's requested action correct. An authorized DELETE request can erase data without leaking a credential. An SSH command can damage a host while the private key remains perfectly protected. A broker controls how authority is exercised; it cannot infer business intent from syntax alone.
Destination control matters. If the agent can choose any URL and the broker blindly adds a credential, the agent may send that credential to an attacker-controlled host. A sound HTTP executor binds each credential to appropriate destinations or otherwise ensures it injects authentication only where intended. Redirect handling needs the same scrutiny because a legitimate origin can redirect a client elsewhere. For SSH, host identity checking must be part of execution rather than an optional agent-supplied flag.
Response data also remains exposed. A safe credential can authorize a request that returns customer records, deployment configuration, or another secret. The agent needs only the minimum response required for its task, but generic APIs make that hard to enforce. Brokered execution reduces credential exposure; it does not provide automatic data-loss prevention.
Prompt injection stays relevant. Repository text, issue comments, build output, and fetched documentation can persuade an agent to request a valid but unwanted action. Human approval helps only when the card shows enough detail and the person reads it. High-risk systems may need narrower remote credentials, API-side scopes, protected branches, staging environments, command allowlists in the executor, or a separate workflow engine. Those controls belong near the action they understand.
The popular recommendation to «put every secret in Keychain and require Touch ID» fails because it combines a good storage choice with an overly broad use decision. Touch ID can prove that a person was present for a release of secret material. It cannot stop the approved recipient from misusing or copying that material afterward. Ask for biometric presence at the action boundary when the action deserves it, and avoid releasing the token to the agent at all.
Audit evidence must describe actions and resist rewriting
A useful audit record answers who requested an action, which process instance made the request, what credential identity was selected, which destination and operation were used, what authorization occurred, when the action ran, and how it ended. It should not contain the credential itself. A console transcript rarely meets that standard because it mixes model text, tool chatter, commands, and truncated output without a stable event model.
Keep session events separate from call events while preserving their relationship. The session journal should show when a process first appeared, how the system identified it, who approved it, whether someone revoked it, and when it exited. The activity journal should show each HTTP or SSH operation and its outcome. An incident reviewer can then answer both «Was this run authorized?» and «What did it actually do?»
Ordinary encrypted logs protect confidentiality at rest but do not prove that nobody deleted or reordered records. A hash chain links each entry to the previous entry so later verification can detect a changed, missing, or reordered ciphertext record. That does not prove every event was recorded, and it does not prevent deletion of an entire log store. It gives a verifier evidence about the integrity of the records that remain. Be precise about that limit.
A practical verification command should produce a simple, automatable result. For example, Sallyport projects its Sessions and Activity journals from one write-blind encrypted, hash-chained audit log, and its verifier works offline over ciphertext without a decryption key:
sp audit verify
A successful verifier should report the checked chain and return exit status zero; a broken link should identify failure and return a nonzero status. Put that check in incident collection and backup verification, not only in the product interface. A green icon rendered by the same mutable application does not provide independent evidence.
Logs also need an explicit redaction contract. Record credential aliases rather than secret values. Decide whether request bodies and command output are stored, truncated, hashed, or excluded. Test malformed requests and transport errors because failure logs often capture more raw context than success logs.
Evaluate the whole path with adversarial tests
Architecture claims become meaningful when a test tries to violate them. Build a disposable credential and endpoint, then exercise the exact binaries and process boundaries used in normal agent work. Screenshots of a permission dialog do not establish what reaches memory or survives in descendants.
A compact test plan can cover the important boundaries:
- Start a new agent process and confirm that old session approval does not carry over. Record the process identity shown to the operator.
- Request a low-risk API read, then a per-call protected write. Confirm that the approval card distinguishes the destination and operation.
- Search agent-visible stdout, stderr, environment, transcripts, tool payloads, and crash output for the disposable secret and encoded variants.
- Revoke the running session, then retry from both the parent and a previously spawned child. Both requests should fail before network or SSH execution.
- Alter, remove, and reorder copies of audit records. Confirm that offline verification returns a failure for each corrupted chain.
Add two abuse cases that teams often omit. Make the test endpoint redirect to a different origin and verify that authentication does not follow. Make the endpoint echo all received headers and fail with a verbose error; confirm that the broker removes credential material before the response reaches the agent.
For SSH, try an unknown host key, a changed host key, an interactive password prompt, and a command that prints its environment. Confirm who owns each decision. A stateless helper should not quietly create a second credential cache or inherit broader environment data than it needs.
Pass criteria must state observable outcomes. «Secret protected» is not testable. «The byte sequence and its base64 form never appear in agent-visible files, process environment, tool responses, or logs» is testable. «Revocation works» is vague. «Every call from the revoked process and its existing children fails before a socket opens» gives an engineer something to measure.
Choose controls by the authority an agent receives
Keychain alone can be enough when a conventional app retrieves a low-impact credential, the app's code path is narrow, the user directly initiates each operation, and disclosure to that app fits the threat model. It remains a sensible storage layer inside a stronger design. There is no prize for replacing a mature encrypted store with a home-grown secrets file.
An autonomous coding tool changes the answer when it can select operations, run untrusted project code, spawn arbitrary children, or keep working without continuous attention. In that setting, give the agent action capabilities rather than credential bytes. Authenticate each new process run, reserve per-call confirmation for sensitive keys, bind credentials to intended protocols and destinations, and make revocation cut off future use immediately.
Before choosing a design, write down the strongest claim you need to make. «The token is encrypted on disk» leads to a storage control. «Only this signed process may ask for operations during this run» needs process authentication and session authorization. «The process can call the release API but cannot obtain its token» needs a broker. «We can detect alteration of the surviving audit records» needs integrity evidence. Each sentence has a different test and owner.
Then record the residual risk beside each claim. A broker cannot undo a remote write, a session boundary cannot remove data already returned by an API, code signing cannot prove benign instructions, and a hash chain cannot prove that the whole log store was preserved. This exercise prevents a security review from awarding one control credit for jobs it never performs.
Migration does not require changing every integration at once. Start with the credentials whose disclosure would create the widest independent authority: deployment tokens, infrastructure API keys, and SSH identities that reach shared systems. Replace their read interface with a small action interface, then remove the old environment variables and helper commands. Keep the test credential active long enough to prove that old paths fail; otherwise a forgotten fallback may quietly preserve the original leak.
Operational ownership matters too. Someone must know who can unlock the vault, approve a process, mark a key for per-use confirmation, revoke a session, rotate a remote credential, and verify an audit chain. If those duties have no owner, the controls will work in a laboratory and drift in daily use. Document the expected response when an agent behaves strangely: revoke the session first, preserve and verify the audit material, then rotate any credential that may ever have reached agent memory.
Sallyport applies that model on macOS: agents connect through its sp mcp server for HTTP and SSH actions, while API keys and SSH keys stay in its encrypted in-process vault. Its fixed ladder uses an absolute vault gate, authorization for each new agent process by default, and optional approval on every use of a selected key.
Do not confuse that design with a general policy engine or a network interception proxy. A narrow broker can make strong claims about credential custody precisely because it owns a defined set of execution paths. Actions outside those paths need their own controls, and remote services should still enforce scopes, environment separation, and account-level revocation.
The decisive review question is concrete: after approval, can the autonomous process print, copy, or independently reuse the credential? If yes, Keychain protected storage but did not contain use. Keep Keychain for what it does well, then move the authority boundary to the executor that performs and records the action.
FAQ
Is macOS Keychain safe for storing API keys?
Yes, it is a sensible encrypted store for API keys on a Mac. The risk changes when an autonomous process can retrieve the plaintext, because the process can copy or reuse it beyond the original task.
Can an AI coding agent read passwords from Keychain?
It can if its process, helper, or command path satisfies the item's access controls and the user grants any required prompt. Once the value is returned, Keychain does not control what the recipient does with it.
Does Touch ID stop an agent from leaking a credential?
Touch ID can authorize access at a moment in time. It cannot prevent an approved process from leaking, caching, or misusing plaintext that it receives afterward.
What is the difference between PAM and Keychain?
PAM authenticates users and establishes operating-system sessions for participating services. Keychain stores secret material and mediates access to it; neither one automatically authorizes each remote action made inside a running agent session.
What does brokered credential execution mean?
The agent asks a trusted executor to perform an HTTP or SSH action instead of asking for a credential. The executor injects the secret internally and returns the operation's result without returning the secret.
Can a credential broker prevent destructive agent commands?
Not by keeping credentials hidden alone. The broker must constrain destinations or commands, remote services must enforce scopes, and sensitive actions may still need a clear per-call approval.
Should every agent action require Touch ID?
Usually not. Requiring the same biometric prompt for harmless reads and dangerous writes creates approval fatigue; use a process-session boundary for routine calls and reserve per-use checks for sensitive credentials.
How should I revoke an autonomous agent's access?
Revoke the live process session at the broker and disable or rotate the remote credential if plaintext may have escaped. Killing a chat window is not enough when child processes or copied tokens can survive.
What should an agent audit log record?
Record the process identity, session decision, credential alias, destination, operation, authorization event, result, and timing. Exclude secret values, and define how request bodies and command output are redacted or retained.
When is Keychain alone enough for a coding tool?
It can be enough for a narrow, user-driven app when credential disclosure to that app is acceptable and the remote account has limited authority. It is a poor final boundary for an autonomous tool that runs untrusted code or chooses actions on its own.