A Secure Enclave SSH agent for macOS that lives entirely in your terminal.
Paprika is an SSH key manager first and foremost. It generates SSH keys inside the Secure Enclave (they never leave, not even into RAM), manages them through the standard ssh-agent protocol, and requires a fresh Touch ID authentication for every single signing operation.
Any tool that invokes ssh as a subprocess gets hardware-backed keys for free, because the ssh binary is what reads SSH_AUTH_SOCK. This covers most of the command-line ecosystem: ssh itself, git (via core.sshCommand), scp, rsync, mosh, and Ansible's default SSH transport. Tools with their own in-process SSH implementations — GUI git clients built on libgit2 (e.g. Tower, GitKraken, GitHub Desktop), IDE plugins that embed their own SSH stack, and anything using go-git or JGit — do not route through the agent and will not work with Paprika. If in doubt, check whether your tool exec's /usr/bin/ssh under the hood.
Git commit signing is an excellent side benefit: because Paprika serves the same keys over the agent protocol, git commit -S and git log --show-signature work against Secure Enclave keys with no extra plumbing. The signing is hardware-bound, Touch ID-gated, and — to our knowledge — as strong as anything a macOS command-line tool can offer.
Secretive is excellent and the right tool for most people. Paprika is for a different workflow:
| Paprika | Secretive | |
|---|---|---|
| Interface | CLI daemon | GUI menu bar app |
| Requires GUI login session | No | Yes |
| Scripting / dotfiles | paprika generate mykey |
Manual clicks |
| Codebase size | ~600 lines of Swift | Much larger |
| One-line Git signing setup | paprika git-setup --global |
Manual |
Both tools still require a human finger on a Touch ID sensor for every sign. Neither is suitable for fully unattended "headless" signing — see Paprika is not for unattended automation below.
Choose Paprika if you live in the terminal, manage Macs with Ansible or bootstrap scripts, or want a launchd agent with no GUI dependencies that starts before you log in. The CLI-first model composes naturally with dotfiles, scripts, and SSH config.
Choose Secretive if you want a polished GUI with a dedicated Touch ID dialog that names the requesting application. Secretive has years of production use and a much larger user base.
Paprika is designed around a single assumption: every signing operation has a human present who is willing to authenticate. The Secure Enclave enforces this in hardware. There is no caching, no "trust this machine," no "sign the next 50 things without asking." If you think about SSH keys in terms of "my CI system pushes tags to Git," Paprika is the wrong tool.
Specifically, Paprika is not suitable for:
- Unattended servers — a Mac mini running as a build machine, a CI runner, a fleet automation box. No human is at the keyboard, nobody will answer the Touch ID prompt, every sign hangs until someone does.
- Background services signing on a timer — cron jobs that auto-sign artifacts,
git pushfrom a scheduled launch agent, any form of "sign something every 5 minutes without asking." - Headless Macs with no Touch ID sensor — a Mac mini without a Touch ID-equipped keyboard cannot even generate Paprika keys. The access control policy requires biometry, and there's no hardware to provide it. (A
--auth=passcodeoption that falls back to device passcode is possible and would remove the sensor requirement, but it still requires a human to type the passcode, so it doesn't solve the underlying problem.) - Any workflow where "no prompt" is a feature, not a bug.
For any of those, use a tool whose design matches your threat model:
| Need | Use this instead |
|---|---|
| Unattended signing on a Mac or Linux server | YubiKey with touch-required bypass or PIN caching — hardware-backed, but policy-adjustable |
| CI signing in the cloud | AWS KMS / GCP Cloud KMS / Azure Key Vault — remote HSMs with service account auth |
| TPM-backed keys on Linux | ssh-tpm-agent or similar |
| Headless CI / build farm | Whatever your CI provider offers for secret management |
Paprika's only job is to be the SSH agent for you, personally, at your laptop, right now, with a finger ready. That's a real use case and we lean into it hard — but it is a narrow one. Know which one you're in.
Paprika is pre-0.1. This repository is source only:
- There is no signed, notarized release binary yet.
- There is no Homebrew tap, no published tarball, no installer.
- The code in
mainbuilds cleanly and the tests pass, but running it on your own Mac requires signing and provisioning work described below.
The public distribution story (brew install paprika → notarized binary → everything just works) is on the roadmap, not shipped. Specifically, the remaining work is: request a Developer ID Application certificate, register an App ID on developer.apple.com, generate a Developer ID provisioning profile, wrap the binary in a .app bundle, notarize with notarytool, staple, publish to GitHub Releases, write a Homebrew formula.
If you clone this today, treat it as a source-level preview and an exercise in how SE-backed SSH agents get built on macOS. If you want to help with the release pipeline, that's an excellent contribution area — open an issue.
- macOS 14+
- A Mac with Secure Enclave (any Apple Silicon Mac, or Intel Mac with T2 chip)
- A paid Apple Developer account — not optional. Secure Enclave keychain access requires a stable team identifier baked into the code signature, and macOS enforces this with AMFI at process exec time. Ad-hoc signing (
codesign --sign -) will fail witherrSecMissingEntitlement;Apple Developmentcert + barecodesignwill be SIGKILLed at launch.
macOS's Secure Enclave access rules are strict: to create and use persistent SE keys, the binary must be code-signed and accompanied by a provisioning profile that authorizes the keychain-access-groups entitlement. For a bare command-line Mach-O binary, this means wrapping it in a minimal .app bundle and embedding the profile at Contents/embedded.provisionprofile.
-
Clone and build:
git clone https://github.com/klobucar/paprika.git cd paprika swift build -c release -
In Xcode, create a throwaway macOS App target with bundle identifier
com.paprika.agent, set the signing team to your Apple Developer team, and enable the Keychain Sharing capability. Build it once (⌘B). Xcode will register the App ID on developer.apple.com and generate a Mac Team Provisioning Profile containingkeychain-access-groups = <teamid>.*. The profile lands at~/Library/Developer/Xcode/UserData/Provisioning Profiles/<uuid>.provisionprofile. -
Wrap the built binary into a bundle, embed the profile, sign with Apple Development cert + the four required entitlements (
com.apple.application-identifier,com.apple.developer.team-identifier,keychain-access-groups,com.apple.security.get-task-allow), and test. Seescripts/codesign.shin the repo history for a working example — it's not committed because it's inherently team-specific, but it documents the exact invocation. -
The resulting
Paprika.app/Contents/MacOS/paprikawill run locally on your Mac. It will not run on other Macs because the provisioning profile is limited to devices registered in your development team. For distribution across Macs, the roadmap (Developer ID + notarization) applies.
# After signing Paprika.app, point launchd at the inner binary:
.build/release/Paprika.app/Contents/MacOS/paprika install
# Load immediately (also runs at login via launchd)
launchctl bootstrap gui/$UID ~/Library/LaunchAgents/com.paprika.agent.plistAdd to your shell profile (.zshrc, .bashrc, etc.):
export SSH_AUTH_SOCK="$HOME/.paprika/agent.sock"The primary workflow is: generate a key, copy the public half somewhere that accepts SSH keys (a server's authorized_keys, GitHub, GitLab, a Git host, a cloud instance), then use ssh / git / scp / rsync normally. Paprika handles the rest.
paprika generate github
paprika generate prod-bastion
paprika generate work-laptopThe private key is created inside the Secure Enclave and is permanently non-extractable — it cannot be copied, backed up, or exported. Key generation does not prompt Touch ID (the SE only records the access-control policy); Touch ID is required for every key use (signing), which you'll see the first time ssh or git commit -S reaches for the agent.
# Print in ssh-keygen format:
paprika show github
# Pipe straight into a remote authorized_keys:
paprika show prod-bastion | ssh bastion 'cat >> ~/.ssh/authorized_keys'
# Or copy to clipboard for pasting into GitHub/GitLab:
paprika show github | pbcopy
# SHA256 fingerprint (for GitHub/GitLab key verification pages):
paprika show github --fingerprintNo SSH config changes needed. As long as SSH_AUTH_SOCK points at Paprika (see Installation above), every tool that uses SSH authentication will route through Paprika:
ssh prod-bastion # Touch ID prompts, then you're in
git push origin main # Touch ID prompts, then pushes
scp ./file bastion:~/ # Touch ID prompts, then transfers
rsync -av src/ bastion:dst/ # Touch ID prompts, then syncsYou'll see a Touch ID prompt the first time in a session and on every signing operation — there is no authentication caching.
Can Paprika run on a headless Mac mini? No — but you might still want to use it from one. You don't run Paprika on the Mac mini; you run it on your laptop and forward the agent socket to the mini through SSH. Paprika's security model requires a human finger on a Touch ID sensor for each sign, so the agent has to live on the machine with a finger available. The headless machine becomes a client of your laptop's agent, not a host of its own.
The standard SSH agent forwarding flag is -A:
# On your laptop (where Paprika runs):
export SSH_AUTH_SOCK="$HOME/.paprika/agent.sock"
# SSH to the Mac mini with agent forwarding enabled:
ssh -A minimac
# Now, on minimac, SSH_AUTH_SOCK is a tunneled socket back to your laptop.
# Any git/ssh/scp operation on the mini will transparently reach Paprika:
git push origin main # Touch ID prompts on YOUR LAPTOP, not the mini
ssh deploy@other-server # Same — prompt appears on the laptopTypical setup: your laptop with Touch ID is the agent host, the Mac mini (or Linux box, or any remote system) is the workstation or build server, and signing authority stays physically bound to you. This is the workflow Paprika is actually built for. -A is also how you chain through jump hosts — every hop back to the laptop is valid as long as the chain stays up.
Warning about agent forwarding: SSH agent forwarding means any user who compromises root on a forwarded host can use your agent socket to sign things while your session is active. This is a long-standing SSH caveat, not a Paprika-specific one, but it's worth naming. Forward only to hosts you trust; prefer -J (ProxyJump) over -A when you don't need the downstream machine to use the agent itself.
paprika delete githubThis destroys the key in the Secure Enclave. There is no recovery — the private key no longer exists anywhere in the universe.
Because the same SSH keys are served over the agent protocol, Git can use them to sign commits with zero extra setup:
# Auto-detects your key if you only have one:
paprika git-setup --global
# Specify a key by name:
paprika git-setup --global github
# Also add to ~/.config/git/allowed_signers (so `git log --show-signature` verifies):
paprika git-setup --global --add-to-allowedEvery git commit -S will now prompt Touch ID and produce a hardware-bound, Secure Enclave-backed signature. Your Git history becomes provably authored by someone with physical presence at your Mac.
A traditional SSH key lives in ~/.ssh/id_ed25519 with mode 0600. That file is readable by any process running as your user: your shell, your editor, your terminal multiplexer, a browser subprocess that escaped its sandbox, an npm postinstall script, a VS Code extension, a curl | bash from a stale tutorial. chmod 0600 is not a security boundary against code running as you — it's a label.
The Secure Enclave is a dedicated hardware security coprocessor baked into every Apple Silicon Mac (and T2-generation Intel Macs). It runs its own sealed operating system on a physically separate chip that the main CPU cannot inspect, with its own memory the kernel cannot read. Keys generated inside the Secure Enclave have four properties that no file-based key can match:
- Created in the SE.
SecKeyCreateRandomKeywithkSecAttrTokenIDSecureEnclavegenerates key material inside the SE hardware. The private key bytes never exist in the main CPU's memory at any point, not even for a microsecond. Userspace only ever holds an opaque handle. - Cannot be exported. There is no API — public, private, or
@_spi— that returns raw private key bytes for an SE-backed key. The SE will sign things for you, but it will not hand over the material.paprika deletedoes not copy the key out; it tells the SE to forget it. - Bound to this specific Mac. The SE encrypts its stored keys with a hardware-fused root key unique to the SoC. You cannot move an SE-backed key to another Mac, and a kernel-level exfiltration attack on your Mac cannot produce anything usable off-device.
- Require Touch ID per use (with
.biometryCurrentSet). Every signing operation invokes the Secure Enclave, which invokes the Touch ID sensor, which prompts you. No authentication caching. No session reuse. No "allow forever" checkbox. Enrolling a new fingerprint after key creation invalidates the key — so an attacker with brief physical access to an unlocked Mac cannot silently gain persistent signing authority.
For the complete treatment, see Apple's Platform Security Guide — the Secure Enclave chapter covers the hardware design, boot attestation, and the key protection hierarchy in detail.
Translation: malware with full user-level code execution on your Mac cannot extract a Paprika key. It can ask the agent to sign arbitrary data, but you will see a Touch ID dialog and can refuse. That's a strictly better security story than a 0600 file, at the cost of requiring physical presence for every signing operation — which was the goal.
sequenceDiagram
autonumber
actor User
participant Client as ssh / git / scp
participant Sock as ~/.paprika/agent.sock<br/>(unix domain, 0600)
participant Agent as paprika serve<br/>(user process)
participant Sec as Security.framework
participant SE as Secure Enclave<br/>(separate chip)
participant TID as Touch ID sensor
Client->>Sock: connect
Note over Sock: kernel enforces peer UID<br/>via file-system permissions
Client->>Agent: SSH2_AGENTC_SIGN_REQUEST<br/>(data-to-sign + key blob)
Agent->>Sec: SecKeyCreateSignature<br/>(fresh LAContext)
Sec->>SE: sign(hash, key-handle)
SE->>TID: require biometry
TID-->>User: system dialog<br/>"Paprika: authorize SSH signing"
User->>TID: fingerprint
TID->>SE: authorized
SE->>Sec: signature bytes<br/>(private key never exits SE)
Sec->>Agent: signature
Agent->>Client: SSH2_AGENT_SIGN_RESPONSE
Client->>Client: use signature in SSH handshake
A few things worth pointing out about what is not in this flow:
- The private key never transits any box except "Secure Enclave." The
Security.framework,paprika serve, the Unix socket, and the SSH client never touch key material — only signatures. - The Touch ID dialog is drawn by the OS, not by paprika. Paprika cannot spoof it, style it, or inject text into it. An attacker impersonating the agent cannot fake a Touch ID prompt.
paprika serveruns as an unprivileged user process. It is not a kernel extension, not a root daemon, not a system service. Its only entitlement beyond the standard cert iskeychain-access-groups, which scopes which keychain items it can see — not what it can do to the system.- The socket is the trust boundary. The kernel enforces it via file-system permissions (
0600file under a0700directory owned by you). No userspace ACL check, nogetpeereidgymnastics — justchmod.
- Non-extractable keys: Keys are
kSecAttrIsExtractable: falseand generated directly inside the Secure Enclave. The private key material never exists outside the SE, not even in RAM. - Touch ID on every signing operation: Paprika creates a fresh
LAContextper signing request. There is no authentication caching — each SSH, Git, scp, or rsync operation requires fresh physical presence. - Current-biometry binding: Keys are generated with
.biometryCurrentSet, so enrolling a new fingerprint after key creation invalidates the key. An attacker with brief physical access to an unlocked Mac cannot silently gain persistent signing authority. - File-system peer authentication: The Unix socket at
~/.paprika/agent.sockis0600under a0700directory, both re-enforced on every start. Only the process owner canconnect(2)— no other local user can reach the agent. - Bounded message handling: Incoming agent messages are capped at 256 KB. A malicious local client cannot force unbounded memory allocation by claiming a gigabyte-sized frame.
- No network access: Paprika listens only on a Unix domain socket. It never opens a TCP/UDP port.
- Protocol: Implements
SSH2_AGENTC_REQUEST_IDENTITIES(11) andSSH2_AGENTC_SIGN_REQUEST(13) from the SSH agent protocol. Signatures are ECDSA P-256 (ecdsa-sha2-nistp256). - Crypto:
CryptoKitand theSecurityframework. No third-party crypto dependencies.
Paprika protects your SSH private keys from:
- Exfiltration by malware with user-level access (keys never leave the Secure Enclave)
- SSH agent hijacking by another local user (socket is 0600; parent dir is 0700; ownership verified at startup)
- Silent signing — every operation requires a fresh Touch ID authentication
- Attackers who briefly touch an unlocked machine to enroll a new fingerprint (keys become invalid on biometry set change)
- Memory exhaustion via oversized agent frames
Paprika does not protect against:
- An attacker who can present your finger to the sensor
- A compromised OS kernel or Secure Enclave firmware
- An attacker already executing code as your user while your session is unlocked and actively authenticated (they will still need Touch ID for each signature, so they cannot sign silently — but they can prompt you and hope you approve)
- An ad-hoc signed binary: keychain access group isolation depends on a proper Developer ID. For production use, sign the release binary with your team ID.
Paprika routes daemon diagnostics through the macOS unified log system (subsystem com.paprika.agent). Automatic rotation, privacy redaction, and rich filtering are handled by the OS.
# Live-tail the daemon (like `tail -f`):
log stream --predicate 'subsystem == "com.paprika.agent"'
# Historical events from the last hour:
log show --predicate 'subsystem == "com.paprika.agent"' --last 1h
# Only errors:
log show --predicate 'subsystem == "com.paprika.agent" && messageType == error' --last 24hParts of Paprika were written with Claude as a pair-programmer — design discussions, SSH agent protocol wiring, Secure Enclave access control, test scaffolding, and this README itself. Every line was reviewed, tested, and accepted by a human before landing. This note exists because attribution belongs somewhere readers can see it, not buried in commit trailers.
MIT
