8 min read

Secure Enclave and Touch ID for AI Agent Secrets

Secure Enclave and Touch ID can protect developer secrets from AI coding agents when credentials stay behind a gated action boundary.

Secure Enclave and Touch ID for AI Agent Secrets

An AI coding agent should be able to ask for an action without ever receiving the credential that authorizes it. That is the security property worth building around. Secure Enclave and Touch ID can enforce part of that property on a Mac, but only when the secret stays behind a boundary the agent cannot read.

A Mac prompt that says an agent may access "GitHub" is not a security design. A copied ghp_... token, an exported AWS_SESSION_TOKEN, or an SSH private key pasted into a tool call gives the agent durable power that no later Touch ID prompt can retract. The hard part is not encrypting the string. The hard part is refusing to hand the string to software that will relay arbitrary text.

I have seen this mistake in several forms: a token placed in .env, a credential helper that prints a password, a private key mounted into a container, then an agent instructed to "use whatever credentials are available." Each choice feels temporary. Each makes the secret part of the agent's working set.

Secure Enclave does not make a bearer token safe to hand over

The Secure Enclave can protect cryptographic operations, but it cannot make a bearer token harmless after a process reads it. That distinction decides whether Touch ID protects a developer secret or merely adds a ceremonial prompt before the leak.

Apple describes the Secure Enclave as isolated hardware that can create and use private keys without exposing their plaintext to the main processor. Its documented restriction matters: Secure Enclave private keys are generated there, cannot import existing plaintext private keys, and support specific P-256 signing and key-agreement operations. An API token is not one of those private keys. It is usually an opaque string that a remote service accepts from anyone who presents it. Apple Developer Documentation, "Protecting keys with the Secure Enclave," is clear about both the isolation and the limits.

That leaves three separate objects that people routinely collapse into one:

  • A Secure Enclave private key is non-exportable key material used for limited cryptographic operations.
  • A Keychain item is encrypted application data whose access macOS can constrain.
  • A bearer credential is a copyable value that grants access wherever a service accepts it.

Treating those as synonyms produces bad designs. A Secure Enclave key can sign a challenge without revealing itself. A Keychain item can require user presence before the system returns its data. A bearer token becomes an ordinary secret the moment a process receives its bytes.

This is why "we store the token in Keychain" is not a complete answer for an agent. It answers the question of storage at rest. It does not answer who can ask for the token, which process receives it, whether that process can pass it to a child process, or whether it can write it to stdout.

Apple's Keychain guidance makes the boundary plain. Keychain Services can require authentication before returning an item, and the Secure Enclave supplies only a pass or fail result for the biometric check. Neither the app nor the operating system gets fingerprint data. That is excellent protection for the biometric template. It says nothing about what an authorized app does with the returned password bytes afterward.

Do not ask Touch ID to solve a problem after you have already given the agent a secret.

A better model has two layers. The vault process can retrieve or use a credential after the user satisfies its gate. The agent can request an action by naming a credential and a destination, but it cannot read the credential, replace it, or ask another tool to print it. The vault process performs the HTTP request or SSH authentication and returns a deliberately limited result.

That is a narrower interface. It is also the point.

The boundary belongs before stdout and environment variables

Developer secrets escape through ordinary plumbing long before an attacker needs to defeat encryption. Environment variables, child-process inheritance, shell tracing, debug logs, crash reports, tool responses, and copied terminal output all turn a protected secret into a portable one.

I am blunt about this because the failure mode is so predictable: if an agent can run printenv, read .env, invoke a credential helper, or obtain an API key as an MCP tool result, it has the credential. Whether the original storage used Keychain, a password manager, or an encrypted file no longer changes the threat.

Consider this common flow:

Agent -> runs a shell command -> credential helper reads Keychain
      -> helper prints token -> shell captures stdout
      -> agent receives token -> token appears in context or logs

The Keychain prompt may have worked exactly as designed. The system authenticated the Mac user. The helper then converted a protected item into text, which the agent received through the same channel used for compiler errors and test output.

That is the moment the design fails.

A safe credential path looks materially different:

Agent -> requests "POST api.example.com/releases" using credential "release-bot"
      -> gateway asks for authorization if required
      -> gateway obtains or uses credential internally
      -> gateway sends HTTPS request with Authorization header
      -> agent receives status, selected headers, and response body

The agent receives the result of the authenticated action, not the authorization header. This sounds like a small API choice. It is the dividing line between delegated execution and secret distribution.

The same rule applies to SSH. Do not pass an agent a path to ~/.ssh/id_ed25519, an SSH_AUTH_SOCK that can sign arbitrary challenges without a visible boundary, or a command that can dump a key from a secure store. Let a narrowly scoped helper establish the SSH connection and run the requested command. Return stdout, stderr, exit status, and host identity information. Keep the private key out of the agent's process tree.

There is a cost. Some developer tools assume they can read credentials directly, and a gateway approach means adapters, narrower tool APIs, and occasional friction around unusual authentication flows. I would rather pay that engineering cost once than rotate a production token after it turns up in an agent transcript.

Do not confuse an encrypted disk with a controlled execution path.

Touch ID proves presence, not intent

Touch ID can prove that a person approved a gate at a moment in time. It cannot prove that the person understood an agent's next command, that the command matches the repository's intent, or that the destination is safe.

Apple's LocalAuthentication framework deliberately exposes a limited result to the app: the framework coordinates with the Secure Enclave and returns success or failure. The calling app supplies the reason text and chooses an authentication policy. That separation is correct. The system should not pretend it can interpret application intent from a biometric event.

For agent work, treat Touch ID as a gate over a capability, not an approval of prose. A card that says "Allow this agent to use production credentials" gives a human too little information. A card that names the signed process, the target host, the credential label, and whether approval lasts for one call or one run gives the human something actionable.

I prefer three distinct authorization moments:

  1. Unlock the vault. While the vault is locked, every secret-backed action fails. There should be no "use an unprotected fallback" branch.
  2. Approve a new agent process for the duration of its run. The approval should identify the code-signing authority, not merely a mutable process name like node or python.
  3. Require user presence for each use of credentials with irreversible reach, such as a production deploy token, a cloud owner role, or an SSH key that can alter a fleet.

Per-session approval is the right default for coding agents. A new process is a meaningful boundary: a fresh launch may use a different binary, a different workspace, different inherited environment variables, or a different MCP configuration. Permanent approval makes later actions quiet precisely when their provenance has become harder to inspect.

Per-call approval should remain rare, but it should be uncompromising where the credential can cause broad harm. A token that only opens a read-only issue tracker does not warrant a fingerprint for every GET. An SSH credential that can run kubectl apply against production does. This creates interruption, and that interruption is intentional.

The popular alternative is a large policy file: approve commands that match this regular expression, permit domains in this allowlist, reject arguments containing certain words. That approach feels scalable because it replaces prompts with automation. It also creates a second programming language that must model shell quoting, redirects, wrappers, symlinks, curl --config, encoded payloads, remote command expansion, and every new tool an agent installs.

I would not put production authority behind a grammar nobody audits after Friday.

Use a small decision ladder instead: locked or unlocked, this run approved or not, this credential needs a fresh approval or not. Those controls have visible meaning when an incident review starts.

Keychain access controls have sharp edges

A biometric Keychain item is useful only when you choose the access constraint deliberately, understand its fallback behavior, and prevent the authorized process from becoming a secret dispenser.

Apple documents SecAccessControlCreateWithFlags for attaching accessibility and authorization requirements to a Keychain item. For a local developer secret that should not migrate through backup or iCloud Keychain, kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly is usually the sensible storage class. It requires a device passcode and makes the item unavailable if the passcode is removed; the ThisDeviceOnly suffix also keeps it from transfer to another device.

For a secret that must require a biometric check, use an access-control flag rather than trying to bolt a prompt around a plain Keychain lookup. This simplified Swift fragment stores a generic password that only the currently enrolled biometric set can release:

import Security

let access = SecAccessControlCreateWithFlags(
    kCFAllocatorDefault,
    kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
    .biometryCurrentSet,
    nil
)!

let item: [CFString: Any] = [
    kSecClass: kSecClassGenericPassword,
    kSecAttrService: "com.example.agent-gateway",
    kSecAttrAccount: "release-bot",
    kSecAttrAccessControl: access,
    kSecValueData: Data(token.utf8)
]

let status = SecItemAdd(item as CFDictionary, nil)
precondition(status == errSecSuccess)

biometryCurrentSet is stricter than biometryAny. It binds access to the current enrolled fingerprints or face data, so changing the enrolled biometric set invalidates the protected item. biometryAny accepts any enrolled biometric and does not give you the same enrollment-change tripwire. Apple lists both flags in SecAccessControlCreateFlags; choose the former when a new fingerprint should force deliberate reprovisioning.

Retrieval needs an authentication context and an operation prompt that names the operation in human terms:

import LocalAuthentication
import Security

let context = LAContext()
context.localizedReason = "Use release-bot for the requested deployment action"

let query: [CFString: Any] = [
    kSecClass: kSecClassGenericPassword,
    kSecAttrService: "com.example.agent-gateway",
    kSecAttrAccount: "release-bot",
    kSecReturnData: true,
    kSecMatchLimit: kSecMatchLimitOne,
    kSecUseAuthenticationContext: context,
    kSecUseOperationPrompt: "Authorize credential use"
]

var result: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &result)

The code is intentionally incomplete in one respect: it does not tell you what to do with result. A conventional app might cast it to Data, create an Authorization header, and carry on. An agent gateway must ensure that data stays inside the process that makes the authenticated call. Do not return it through the MCP response. Do not place it in a temporary file. Do not log it when the request fails.

Be careful with reuse windows. Apple's sample shows that Touch ID satisfaction can reuse a recent device-unlock event for a configured duration, up to five minutes. That convenience helps ordinary apps avoid duplicate prompts. For an agent control point, it can erase the moment of deliberate approval you meant to require. Use no grace window for every-use credentials unless you have made a conscious, documented decision to accept it.

There is another limit worth stating. Biometrics can fail, be unavailable, or lock out after failed attempts. Apple exposes policies that permit device-passcode fallback and policies that require biometrics. Decide which one your security claim needs. If you say "Touch ID on every production deploy," then silently accepting an unrelated fallback path changes the claim (and should change the UI wording).

Follow one malicious instruction to the point it wins

Keep the gateway local
Run one signed Mac menu-bar app with the vault core in-process, not a separate credential daemon.

An agent does not need a dramatic exploit to abuse a credential. It needs one plausible instruction, one broad capability, and one output channel that returns more than the user intended.

Imagine a coding agent helping with a pull request. It reads a repository document added by an attacker. The document says that the release check requires running a helper script. The helper script uses the developer's existing cloud CLI session, lists deployment credentials, and sends an encoded request to an external endpoint. The agent has permission to run shell commands and has inherited AWS_PROFILE, GH_TOKEN, or access to an SSH agent.

The first failure happened before the script ran: the developer gave the agent ambient credentials. The second failure happened when the tool runner let the agent choose arbitrary network destinations. The third happened when logs and command output returned authentication material or session details into the agent context.

Touch ID at login would not rescue this design. The person may have authenticated an hour ago, then walked away. A Keychain item protected only by "device unlocked" can become available to a process the user never intended to authorize for this task. Apple itself warns that unlocked-device access may not be restrictive enough for every use case.

Now change the architecture. The agent requests an action with these fields:

{
  "channel": "http",
  "credential": "release-bot",
  "method": "POST",
  "url": "https://api.example.com/releases",
  "body": {"branch": "feature/fix-ci"}
}

The gateway resolves release-bot internally. It compares the requested destination with the action it will perform, asks for authorization if the credential requires it, injects the header itself, then records the action. The agent gets the HTTP status and a redacted response. It never sees the header value.

The repository document can still persuade the agent to request a bad deployment. That is why destination and action review matter. But the document cannot tell the agent to exfiltrate a token it never held. It also cannot reuse the token against another service after the approved call ends.

This is a meaningful reduction in blast radius, not magic. A compromised agent that has approval to deploy can still deploy something harmful. The system has constrained credential theft and made the action attributable; it has not solved malicious code review or replaced human judgment.

When I review agent setups, I look for the earliest place an instruction can become a secret string. That is usually where the fix belongs.

Capability handles are safer than secret strings

Use labels, not secret strings
Replace ambient environment credentials with named vault entries that Sallyport resolves only when acting.

An agent should ask for a named capability with structured action parameters, while a trusted local process resolves that capability to a credential and performs the side effect.

The word "capability" gets used loosely. In this case, it means a reference that is useful only inside the gateway, such as release-bot, staging-ssh, or billing-read. It is not a token alias the agent can exchange for the token. It is not a template variable that expands into an environment value. It is a selector passed to a process that retains exclusive access to the credential.

That choice imposes discipline on the tool interface. HTTP tools should accept the method, URL, headers that are safe for the agent to provide, and body. The credential injection must happen after validation, inside the gateway. SSH tools should accept a host, user, command, and selected credential identity, then invoke a helper that owns the authentication path. They should not return an IdentityFile path or offer a generic "read secret" operation.

This is where a boring interface wins. A general shell that inherits every developer credential will support more tools on day one. It will also make it nearly impossible to answer which agent used which account for which outbound request. A constrained HTTP and SSH interface has less initial range, but it preserves the facts you need to make a security decision.

Sallyport follows this shape: the agent uses the bundled sp mcp stdio shim to request HTTP or SSH actions, while the app keeps API and SSH credentials inside its encrypted vault and performs the action itself. The agent receives the result, not plaintext credentials.

The limitation is honest. A tool that only knows HTTP and SSH will not automatically cover every desktop application, database client, package registry, or local binary in a developer's environment. Adding a channel should require designing its action model, its redaction behavior, its authorization semantics, and its audit fields. A broad "run anything with my login" switch is easier to ship and harder to defend.

Use explicit credential labels that reveal intended reach. prod-deployer is better than token-4. github-readonly-org is better than github. The label becomes part of the human decision and the audit record, so ambiguity becomes an operational problem rather than a naming preference.

Keep the request format narrow enough that the gateway can display it without interpretation. A reviewer can understand POST https://api.example.com/releases. A reviewer cannot reliably infer the effect of a Base64 blob piped through a shell wrapper.

An audit trail must describe the action without copying the secret

A credential gateway needs two records: the agent run that obtained authority and each side effect it attempted. A single stream of terminal logs cannot provide both without either losing context or leaking sensitive material.

Record the run when a new agent process asks for access. Capture the process identity available to the gateway, its code-signing authority, start time, approval decision, and revocation state. If the user revokes the run, later requests from that process should fail even if the process remains alive.

Record each action separately. For HTTP, retain the credential label, method, destination, status, timing, and a carefully chosen summary of the body. For SSH, retain the credential label, host, remote user, command, exit code, and timing. Do not record Authorization headers, bearer values, private-key bytes, full request bodies containing customer data, or unrestricted command output.

Logs that contain secrets become another vault with worse access controls.

Tamper evidence matters because an agent incident often begins with a disputed timeline: "Did the agent call this endpoint?" "Was the session approved?" "Did somebody edit the local history?" A hash-chained event log gives later verification a concrete object to inspect. The verifier should work without needing to decrypt every event, otherwise the person checking integrity must first receive the sensitive data the log was meant to protect.

Sallyport projects its Sessions and Activity journals from one write-blind encrypted, hash-chained audit log, and sp audit verify checks the chain offline without a vault key. That property is useful because integrity review should not require access to developer secrets.

A hash chain does not stop a compromised machine from attempting a bad action. It makes quiet rewriting of the recorded sequence harder and gives an investigation a consistent sequence of events. Do not oversell it as prevention.

Apple Platform Security and the Apple Developer Security documentation are useful reading here because they separate hardware protections, system controls, and application responsibilities. That separation is exactly what agent security needs. Secure hardware can gate access, but the application still chooses what it sends over the network and what it writes to disk.

Production access needs fewer paths, not smarter guesses

Keep SSH keys off limits
Use the bundled sp-ssh helper so agents run SSH commands without receiving private key material.

The safest rollout for AI coding agents is to start with a small set of named credentials, known destinations, visible session identity, and one irreversible action class that requires fresh approval. Broad ambient access is an invitation to discover your threat model during an outage.

Use this review before allowing an agent to touch a credential:

  1. Confirm the agent cannot read the secret through environment variables, files, a credential helper, tool output, or a child process.
  2. Confirm the trusted process performs the HTTP request or SSH authentication and injects the credential after the agent submits structured parameters.
  3. Set vault lock to deny every secret-backed action. Test this with the app locked, not just with the Mac screen locked.
  4. Require a fresh session approval when a new agent process starts, and make the approval identify its signing authority.
  5. Mark credentials with deployment, administrative, destructive, or broad SSH reach for every-use authorization.

Test the ugly cases. Add a fake secret such as canary-agent-secret-9f31 to a nonproduction credential. Ask the agent to inspect the repository, run tests, and report tool output. Then search its transcript, terminal history, temporary directories, logs, child-process environment, and audit records for that exact string. If it appears anywhere outside the vault process, the design has handed the agent a secret path.

Do the same for revocation. Approve a run, make one harmless request, revoke the run while the process remains open, then attempt the same request again. The second request must fail before it reaches the remote service. A revocation control that only takes effect after restart is paperwork, not containment.

Do not use a biometric prompt as decoration around a credential export. Put the prompt at the boundary where the human authorizes a specific process or a specific action, and keep the credential on the protected side. That is how Secure Enclave and Touch ID become useful controls for AI coding agents instead of a reassuring story about storage.

FAQ

Does the Secure Enclave store API tokens?

The Secure Enclave is an isolated hardware security processor. It can create and use certain private keys without exposing their plaintext to normal application memory, but it does not turn every API token in Keychain into a non-exportable object.

Can Touch ID tell whether an AI agent action is safe?

Touch ID confirms user presence to macOS and returns an allow or deny result. It does not inspect a shell command, understand a repository, or decide whether an API request is appropriate.

Is a Keychain secret safe after an AI agent reads it once?

No. A bearer token copied into an agent prompt, environment variable, config file, shell history, or tool output has already crossed the boundary that mattered. Revoke it and issue a replacement.

When should an agent require Touch ID for every call?

Use per-session approval for a narrowly scoped agent run that needs ordinary development access. Require approval for every use of credentials that can deploy, alter production data, change identity settings, or reach a sensitive SSH target.

What is the difference between biometryCurrentSet and biometryAny?

A biometryCurrentSet constraint ties access to the biometrics currently enrolled on the Mac. Adding or removing a fingerprint invalidates access until the item is provisioned again; biometryAny does not provide that same enrollment-change signal.

Does Keychain access control stop a compromised app from leaking a token?

No. Keychain protection controls whether a macOS process can retrieve an item, but a process that receives the token can still copy it to memory, a log, or a child process. Keep the credential in a process that performs the request itself.

What should an audit log record for agent credential use?

The useful record names the agent process, authorization decision, credential identity, destination, request method or SSH target, result, and timestamp. It should omit bearer values, private-key material, authorization headers, and sensitive response bodies.

Why is per-session approval better than permanent approval?

A fresh agent process deserves a fresh approval because process identity and launch context are part of the security decision. Long-lived approval across unrelated tasks turns one click into an invisible standing permission.

Should I use a policy engine to control AI coding agents?

A general policy engine looks attractive because it promises automatic decisions at scale. In agent workflows, it often hides the decision behind patterns that nobody reviews after the first week; a small fixed set of visible gates is easier to operate correctly.

What happens if a Mac has no Touch ID or no enrolled fingerprint?

No. Secure Enclave capabilities require supported Apple hardware, and Touch ID requires enrolled biometrics. A serious design needs an explicit locked-state behavior that denies secret-backed actions rather than quietly falling back to an unprotected store.

Sallyport

Sallyport runs API calls and SSH commands for your AI agent. The keys stay in a local vault on your Mac; you approve each run and every action lands in a sealed journal.

© 2026 Sallyport · Open source under Apache-2.0 · Oleg Sotnikov