A Keychain-backed environment manager for macOS. Group API keys, database URLs, and other secrets; inject one group into a child process with a single Touch ID prompt. Useful today for scoping what an AI coding agent can see, or running commands with project-specific credentials. On the roadmap: a shell hook with per-project .mima.yaml discovery, at which point mima becomes a full direnv replacement with Keychain-backed values.
Alpha. What works today:
mima secret— add / list / get / remove secrets in the macOS Keychain with a.userPresenceACLmima group— map environment-variable names to secret names, organised into groups (~/.mima.yaml)mima run <group> -- cmd— one Touch ID unlocks the whole group; secrets are injected into the child viaexecve, never into the parentenvironmima env <group>— print shellexportlines foreval "$(...)"in an interactive shell
Not yet (see Roadmap):
- Shell hook (
mima hook zsh|bash|fish) with per-project.mima.yamldiscovery — three loading modes (wrapper / public-env / compat), see Threat model mima run --scrubfor aggressively-scoped agent sandboxing- Session agent so Touch ID amortises across a shell session instead of firing on every prompt
direnv allow-style trust model, flake integration,.envloader,PATH_add
This is mima's sharpest use today. Local coding agents — Claude Code, aider, codex, cursor — run under your shell and inherit your full environ by default. If you've ever exported a prod AWS key, a personal GitHub token, or a billing-capable Stripe key, the agent can see all of it, and it can shell out. mima's per-group model is the intervention: each agent gets one group containing only what it needs.
mima group link agent-claude GITHUB_TOKEN gh-agent-readonly
mima group link agent-claude ANTHROPIC_API_KEY anthropic-key
mima run agent-claude -- claude-code
mima group link agent-aider OPENAI_API_KEY openai-key
mima run agent-aider -- aiderDelete the group when you're done with a project; the underlying secret stays in the Keychain for reuse elsewhere.
Why not
env -i?env -i cmdscrubs the inherited env — correct, but it doesn't give you any way to re-inject the scoped values the agent actually needs. mima is the "where do those values come from" half: Keychain-stored, biometric-gated, declared as a group.mima run --scrub(see Roadmap) will combine both: strip the parent env, inject only the group, keep a minimal safelist likePATH,HOME,TERM.
git clone https://github.com/klobucar/mima.git
cd mima
make install SIGNING_IDENTITY="Apple Development: Name (TEAMID)"mima needs codesigning against entitlements.plist to talk to the Keychain. SIGNING_IDENTITY must be an "Apple Development" certificate from a paid Apple Developer account — find it in Keychain Access. The free developer tier is not enough.
Why the paid account is required for Touch ID
Apple gates the biometric / .userPresence Keychain ACL behind the $99/year Developer Program. Three things have to line up for a mima secret get to fire a Touch ID prompt:
- A stable Team ID on the binary. The Keychain records the calling binary's Team ID + bundle identifier as part of the ACL identity. Ad-hoc-signed binaries have no Team ID — the OS treats every rebuild as a different identity, so a
.userPresence-gated item written by one build can't be read by the next, and biometricLAContextevaluation refuses to bind to ad-hoc code paths at all. - The keychain-access-groups entitlement in
entitlements.plist, which scopes mima to its own Keychain partition. Entitlements are only honored when they're embedded in a binary signed by a real (non-ad-hoc) identity. - A signing identity issued to a Developer Program member. Apple issues "Apple Development" / "Apple Distribution" certificates only to enrolled members. The free tier you get from signing in with an Apple ID is a "Personal Team" identity — sufficient to sideload an app to a device you own, but explicitly not allowed to enable Keychain Sharing, App Groups, or anything that touches the LocalAuthentication framework. Xcode will surface a
requires a development team with the Apple Developer Programerror if you try.
The reason Apple draws the line there: every binary that can pop a system biometric prompt is a phishing vector. Tying that capability to a verified, revocable developer identity means a compromised cert can be killed centrally — Apple revokes the cert, the binary stops working everywhere overnight. Personal Teams have no equivalent revocation path.
References: Code Signing in Depth · SecAccessControlCreateWithFlags · Keychain Services Access Control
Without a paid signing identity: plain
make installfalls back to ad-hoc signing and a Keychain item without the.userPresenceACL. Values stay encrypted at rest, but the Touch ID gate that anchors the threat model goes away — any process running as you can read them by callingSecItemCopyMatchingagainst an unlocked login Keychain. mima still beats a plaintext.envrc(an attacker needs to know mima's service name and call the Keychain API), but the headline biometric-gated property in the Threat model is not in effect on this install.
# Stash secrets in the Keychain
mima secret add openai-key # prompts for value securely
mima secret add anthropic-key
# Map env-var names to secret names, under a group
mima group link agent OPENAI_API_KEY openai-key
mima group link agent ANTHROPIC_API_KEY anthropic-key
# Run a command with just that group's env
mima run agent -- claude-codeOne Touch ID prompt. claude-code starts with OPENAI_API_KEY and ANTHROPIC_API_KEY set. Nothing else from your shell leaks in, and your parent shell's environ is never touched.
mima secret add <name> # prompt securely (getpass)
mima secret add <name> <value> # inline — avoid, goes into shell history
mima secret rm <name>
mima secret list # names only, no values
mima secret get <name> # print value; warns to stderr
mima secret get <name> --export # print `export NAME=VALUE`mima group link <group> <ENV_VAR> <secret-name>
mima group listMappings are stored in ~/.mima.yaml, written atomically:
groups:
agent:
OPENAI_API_KEY: openai-key
ANTHROPIC_API_KEY: anthropic-key
dev:
DATABASE_URL: dev-db
STRIPE_KEY: stripe-testmima run <group> -- <command> [args...]Unlocks the group's secrets behind a single LAContext, then execves the command with an explicit envp built from your current env plus the group's values. The parent mima process never calls setenv.
eval "$(mima env <group>)"Prints shell export lines. Less private than mima run because values land in your interactive shell, but useful when you need secrets for multiple ad-hoc commands. Output is POSIX-single-quote-escaped, so apostrophes and shell metacharacters cannot break the quoting.
Instead of:
# .envrc (gitignored, hopefully)
export STRIPE_KEY="sk_live_..."
export DATABASE_URL="postgres://..."…store values once in the Keychain and reference them by name in ~/.mima.yaml:
groups:
my-saas:
STRIPE_KEY: stripe-prod
DATABASE_URL: saas-db-urlFor now, run commands via mima run my-saas -- cmd. Once the shell hook ships, the group will load automatically when you cd into a project with a matching .mima.yaml.
mima run staging -- ./deploy.sh
mima run prod -- ./deploy.shmima isn't strictly better than any of these — pick the one whose tradeoffs match your workflow.
- 1Password CLI (
op run). The closest competitor.op run --env-file=.env -- cmdinjects values per invocation and is biometric-gated onceopis authenticated. Chooseopif you need shared team vaults, audit logs, RBAC, or non-macOS support. Choose mima if you want local-only storage, no subscription, no network round-trip per invocation, and a free baseline. Roughly: 1Password is to your team what the Keychain is to your machine. - direnv. direnv exports values into your interactive shell's
environoncd. Every subsequent process inherits them — includingnpm installpostinstalls, the precise transitive surface that supply-chain stealers exploit.mima runis process-scoped: secrets only land in the env of the command you named. Once the shell hook lands, wrapper mode gives you direnv-shaped ergonomics with the process scoping intact. - direnv +
op run. A common compromise:.envrccallsop runto fetch values, scoped to the current shell. Better than plaintext direnv, but you're back to shell-wideenvironexposure for any tool that auto-runs oncd. mima's wrapper mode is the same idea minus the shell-wide step. - aws-vault. Same shape — Keychain-backed, biometric-gated, exec-only — but AWS-specific. mima generalises the pattern to any env var.
- sops / age. File-encrypted at rest, decrypted on demand. Strong for team config in a repo and for CI; awkward for personal interactive use because every command needs the decryption step. mima trades portability for ergonomics on a single machine.
- Plain
.env/.envrc. Plaintext on disk, readable by any process running as you. Exactly the credential-stealer payload. Don't.
- Secrets are stored via
SecItemAddwithkSecAttrAccessControl = .userPresence, so retrieval requires biometric or passcode authentication. - One
LAContextis reused for batch reads inmima run, so a single authorisation unlocks N secrets. mima runusesexecvewith an explicit envp. The parent process never callssetenv, so secrets do not appear in the parent'senviron.mima envprints secrets to stdout and is inherently less private — use it deliberately. Output is POSIX-escaped against quoting attacks.- mima cannot protect against a malicious child process. Once the child has the env var, it has the env var. Scope groups narrowly; prefer
mima runovermima envwhen you can. - mima does not manage SSH keys. Use Paprika or an equivalent Secure-Enclave SSH agent for those. mima's scope is environment variables only.
mima's design is shaped by the class of attacks that dominates 2023–present: supply-chain credential stealers. Shai-Hulud (npm, 2025–ongoing), recurring PyPI token-stealer waves, compromised IDE extensions, malicious CLI tools from curl | sh installers, and prompt-injected agent commands. They all share a pattern: attacker code executes as you during a routine action (package install, extension load, tool invocation), and the payload almost always lifts every credential it can read — env vars, dotfiles, ~/.aws/credentials, ~/.npmrc, the lot — and posts them to a webhook before you've noticed. macOS's permission layer doesn't stop a process running as your user from reading anything else you can read. What saves you is what isn't accessible — values that aren't at rest, and values gated behind a prompt you'd notice.
- Plaintext credential files.
.env/.envrc/~/.aws/credentials/~/.npmrcare readable by any process running as you, and the common scraper pattern is to read them directly. mima stores values in the Keychain — encrypted at rest, opaque to a quickcat. With a paid Apple Developer signing identity (see Install), retrieval is also gated by.userPresence: an attacker reading values triggers a Touch ID prompt out of nowhere, which is extremely conspicuous. - Globally-exported shell env. If
.zshenvrunsexport STRIPE_KEY=…, every process you launch — including every transitive postinstall script — inherits it forever.mima runscopes values to the specific child process you named; they never enter the parent shell'senviron. - Secrets in shell history.
mima secret addusesgetpass, never command-line arguments. Values never land in.zsh_history. - Over-broad agent access. Coding agents inherit your full shell env by default.
mima run agent-claude -- claude-codehands the agent only the variables you explicitly grouped for it.
- Malicious children leaking the env they received. Once the child has the variable, it can send it anywhere. Defense is narrow groups + sandboxing (devcontainer, firejail, macOS app sandbox).
- Deceptive Touch ID prompts. An attacker who can invoke the mima binary can trigger a legitimate biometric prompt. User vigilance is the only thing stopping a reflexive approval.
- Shell-level compromise. If your
.zshrc, a shell plugin, or a supply-chain-compromised tool has injected code into your shell, mima runs inside that trust boundary and can't help. - Session agent TTL window. When the session agent ships, unlocked values will sit in memory for a configurable interval. During that window, a supply-chain attacker running as you can read them without triggering a biometric prompt — the same class of risk
ssh-agentandopaccept. Short TTLs mitigate; they do not eliminate. mima env+eval. After you eval the output, values are in the shellenvironfor the rest of the session. Same threat surface as plainexport. Use deliberately.
Supply-chain attacks shape how the shell hook works. A direnv-style auto-export on every cd would defeat mima's best property — putting group secrets into your shell environ so that any subsequent npm install or pip install inherits them. To keep secrets process-scoped by default while still offering direnv ergonomics, the hook supports three modes:
- Wrapper mode (default).
mima hook zshdefines per-group shell functions when you enter a project.devandagent-claudebecome functions that expand tomima run <group> -- "$@". Every invocation is stillexecve-scoped; secrets never enter the shellenviron. Touch ID fires per-command — amortised by the session agent once that lands. - Public-env mode.
.mima.yamlmay declare anenv:section for non-secret values (PATH additions, tool versions, nix/flake outputs). The hook auto-exports that section oncd;groups:(Keychain-backed secrets) stay behindmima run. Mirrors the nix-direnv pattern. - Compat mode (opt-in).
auto_export: truein.mima.yamlrestores direnv-classic UX: groups auto-export oncd, unset on leave. Strictly weaker than wrapper mode against supply-chain attacks — any subsequent process in the shell inherits the secrets — but still an improvement over globally-exported.zshenv, because blast radius is scoped to one project at a time. Opt-in because the tradeoff should be conscious.
mima raises the cost of credential theft; it doesn't prevent it. Not in scope: curl | sh review, phishing, MDM compromise, device theft with a weak passcode, or a persistent determined adversary with ongoing local code execution.
Short version below; ROADMAP.md has rationale, open questions, and a "considered, not planned" section.
-
Shell hook + directory discovery.
mima hook zshemits a precmd that discovers the nearest.mima.yamlby walking upward frompwd. Three loading modes:- Wrapper mode (default) — define per-group shell functions that expand to
mima run <group> -- "$@". Secrets stay process-scoped. - Public-env mode — auto-export a declared non-secret
env:section; groups stay behindmima run. - Compat mode — auto-export groups on
cd/ unset on leave, direnv-classic. Opt-in viaauto_export: truein.mima.yaml.
See Threat model for why wrapper mode is the default.
- Wrapper mode (default) — define per-group shell functions that expand to
-
Agent sandbox flag.
mima run --scrub <group> -- cmdruns the child with only the group's env plus a minimal safelist (PATH,HOME,TERM,USER,SHELL) — the companion to Agent sandboxing above. -
Trust model.
mima allow/mima denystores a content hash per config file, refusing to auto-load untrusted ones — same idea asdirenv allow. -
Session agent. A small launchd daemon holds unlocked values in memory keyed to a shell session so Touch ID amortises across a work session rather than firing on every
cd. -
Nix / flake integration.
use flakeshells out tonix print-dev-env --json, caches by flake-lock hash, merges into the exported env. -
Dotenv +
PATH_add. Cover the direnv use cases that don't need Keychain.
Paprika (github.com/klobucar/paprika) is a Secure-Enclave SSH agent by the same author. Paprika handles SSH identity via SSH_AUTH_SOCK; mima handles everything else your shell exports. Together they cover the Touch-ID-gated-secrets surface of a dev shell, but either works standalone.
MIT
