All posts 🔒

The Encrypted Handoff — How to Give AI Agents Secrets Without Exposing Them

Here's a fun problem. Your AI agent needs your Stripe key to deploy a webhook. Your Cloudflare token to push a worker. Your database URL to run migrations. Without credentials, the agent just sits there — capable of everything, authorized for nothing.

So you hand over the key. And the moment you do, you've lost control of it.

The secret lands in the conversation history. It might get logged by the provider. It could show up in generated code, debug output, or — worst case — training data. The agent didn't mean to leak it. It just treated your production API key like any other string in the chat.

This is the problem we kept running into: agents need secrets to do real work, but the way they consume information makes secrets unsafe. And no, environment variables don't fix it. echo $STRIPE_KEY is one tool call away. The value lands right back in the conversation.

We built a pattern called the encrypted handoff to deal with this. The secret reaches the agent's process but never enters its conversation. Here's how.

Why environment variables aren't enough

The standard advice? Put secrets in environment variables, not in code. That works for humans. You're not going to accidentally paste $DATABASE_URL into a commit message.

Agents are different. An agent's "memory" is its conversation context — every message, every tool output, every command result flows through it. When an agent runs printenv | grep STRIPE, the value shows up in its context window. From that point on, the secret is just text. The agent might reference it in a response, hardcode it in generated code, or send it to an API endpoint while debugging.

None of this is malicious. The agent is doing exactly what you asked. But now your secret lives in places you never intended:

The problem isn't storage. It's delivery. How do you get the secret from your credential store to the agent's process without it passing through the agent's eyes?

The encrypted handoff pattern

The idea is straightforward: load a secret into the shell environment without the raw value ever appearing in the agent's text context. Here's the step-by-step.

Step 1: Detect the caller

Before deciding how to deliver a secret, you need to know who's asking. The secret manager walks the process tree from the requesting process up to PID 1, checking each ancestor's binary name against known AI tool signatures — claude, cursor, copilot, codex, and others.

If any ancestor matches, the caller gets classified as an agent. The response switches from plaintext to encrypted handoff. Automatic — nothing to configure.

Step 2: Retrieve and authenticate

The secret gets pulled from the credential store. On macOS, that's Keychain — hardware-encrypted, protected by Touch ID. You authenticate with your fingerprint. The secret never touches disk in plaintext.

Step 3: Encrypt with a one-time key

A random ChaCha20-Poly1305 key and nonce get generated. The secret value is encrypted with this one-time key — authenticated encryption, so any tampering is detected. The encrypted payload, key, and nonce travel to the local helper over a Unix domain socket — local-only transport that never leaves the machine.

// Server side (simplified)
let key = ChaChaPoly.generateKey()           // 256-bit key
let sealed = try ChaChaPoly.seal(secret, using: key)

// Send over Unix socket
send(ciphertext: sealed.ciphertext,
     nonce: sealed.nonce,
     tag: sealed.tag,
     key: key)

Step 4: Write a self-deleting script

The local helper decrypts the payload with Apple's CryptoKit framework, then writes a temporary shell script to /tmp:

#!/bin/sh
export STRIPE_KEY='sk_live_...'
rm -f "$0"

The file gets 0700 permissions — only the owner can touch it. The script self-deletes the moment it's sourced; a background process removes it after 120 seconds if it never is. In the normal flow, the file exists on disk for milliseconds.

Step 5: Return a source command

The MCP tool returns one line:

source '/tmp/noxkey_a8f3c1.sh'

That's what the agent sees. Not the secret — a path to a script. The agent calls noxkey_get(account: "org/proj/STRIPE_KEY") through the bundled MCP server, then runs the returned line in Bash. That sources the temp script, exports the value into the shell environment, and deletes the script.

Step 6: Secret is loaded, never seen

Now $STRIPE_KEY lives in the environment. The agent's subprocesses — curl, npm, git push, deployment scripts — can access it. But the raw value never appeared in the agent's conversation. It flowed through the operating system, not through the chat.

Agent calls noxkey_get(account: "...")
  └─ MCP server talks to NoxKey app over Unix socket
    └─ App authenticates (Touch ID), encrypts secret
      └─ MCP server decrypts, writes self-deleting script
        └─ Agent's Bash runs source → $STRIPE_KEY is set
          └─ Script deletes itself immediately

What this prevents

The encrypted handoff blocks the most common ways AI agents leak secrets:

What this doesn't prevent

We're going to be straight with you about the boundaries.

The agent can still access the environment variable. If it runs printenv STRIPE_KEY or echo $STRIPE_KEY after the handoff, the value shows up in its context. The handoff prevents automatic exposure during delivery, not deliberate access afterward. Treat the agent as untrusted: don't let it echo or print the env vars it just received.

Subprocesses inherit the environment. Any process the agent spawns can read the variable. That's by design — it's how the agent actually uses the secret. But a malicious dependency or compromised build script could exfiltrate it. That's not unique to the handoff; it's just how environment variables work.

The temp file exists briefly on disk. Between creation and self-deletion, the secret sits in a file on /tmp. The window is typically milliseconds, and the file is owner-only (0700), but it's not zero risk. A process with root access could read it.

Detection depends on known signatures. A brand-new AI tool that's not in the signatures list won't trigger the handoff — it'll get the human delivery path instead. We update the list with each release, but there's always a gap for new tools.

How NoxKey implements this

The encrypted handoff is the default behavior in NoxKey when an agent requests a secret through the bundled MCP server. No flags, no configuration:

# Human in the menu bar app — copies plaintext to clipboard
# (behind Touch ID, cleared after 30 seconds)

# Agent in Claude Code — calls the MCP tool:
noxkey_get(account: "noboxdev/api/STRIPE_KEY")
# → returns: source '/tmp/noxkey_a8f3c1.sh'

The agent runs that source line in Bash. After that, $STRIPE_KEY is available in the shell session:

# Agent can now use the secret without ever seeing it
curl -H "Authorization: Bearer $STRIPE_KEY" https://api.stripe.com/v1/charges

Need multiple secrets? Pass a prefix instead of a full path, and one MCP call returns a script that loads every key under it:

noxkey_get(account: "noboxdev/api")
# → source '/tmp/noxkey_xxx.sh'
#   which exports $STRIPE_KEY, $DATABASE_URL, … in one go

# All loaded under one Touch ID
npm run deploy

The MCP tool surface is intentionally narrow. There's no tool that returns the raw value, no tool that writes to the clipboard from agent context, and no bulk export. Anything that would land plaintext in the conversation simply isn't reachable from MCP — it lives only in the menu bar app behind Touch ID.

Why not just block agents entirely?

We thought about it. Simpler, right? Agent wants a secret? Denied.

But that just pushes developers back to .env files. If the agent can't get the Stripe key through NoxKey, the developer drops it in a .env in plaintext, and we're right back to the original problem — except now they paid for a secrets manager and still have .env files everywhere.

The encrypted handoff is the pragmatic middle ground. Agents get the access they need. The delivery mechanism is built around their specific threat model. The secret reaches the process without passing through the conversation. Not perfect — but meaningfully better than everything else we've seen.

The goal isn't to build an unbreakable wall. It's to make the default safe.

Frequently asked questions

Does the encrypted handoff work with all AI coding tools?

It works with anything that executes shell commands — Claude Code, Cursor, GitHub Copilot in the terminal, Windsurf, Codex, and others. Detection is based on process tree walking, so any tool that shows up as an ancestor process gets the handoff automatically. If a new tool isn't in the signatures list yet, it'll get the standard human delivery path until we add it.

Can the agent still read the secret after the handoff?

Yes — if it explicitly runs printenv or echo $VAR. The handoff prevents the value from appearing during delivery, not from being accessed afterward. A determined agent (or a badly written prompt) could still surface the value. The point is making accidental exposure impossible, not making deliberate access impossible.

What happens if the temp script isn't sourced within 120 seconds?

A background cleanup process deletes it. The secret is gone. The agent would need to call the MCP tool noxkey_get again, which triggers fresh Touch ID authentication and generates a new one-time encryption key. No stale scripts pile up in /tmp.

Is the encryption between CLI and server actually necessary?

The Unix socket is local-only — no network exposure. So you could argue plaintext over the socket is fine. We encrypt anyway because the cost is negligible (sub-millisecond on Apple Silicon) and it prevents a class of local attacks where another process monitors socket traffic. Defense in depth. The one-time key means even if an attacker captures one exchange, it's useless for the next.

Does this work on Linux or Windows?

Not yet. The current implementation uses macOS-specific APIs — proc_pidinfo for process names, LOCAL_PEERPID for socket peer identification, Keychain for storage, Touch ID for authentication. The pattern is portable. The implementation isn't. Linux could use /proc/[pid]/status for process trees and SO_PEERCRED for socket peers. We're focused on getting macOS right first.

Key Takeaway
The encrypted handoff solves the fundamental tension between AI agents needing credentials and credentials being unsafe in agent contexts. By detecting the caller, encrypting the value, and delivering it through a self-deleting temp script, the secret reaches the agent's shell environment without ever appearing in the conversation. It makes the default path safe — no configuration, no special flags, no developer effort required.