The local-only secret manager for the AI-coding era. Encrypted with SOPS + age. No cloud, no telemetry, no account. CLI ¡ Web UI ¡ MCP ¡ TUI.
đ Documentation ¡ đ Report a bug ¡ â Star this repo if it helps you!
see CHANGELOG
concealer is a thin, auditable wrapper around two battleâtested tools â it does not implement its own cryptography. Everything is encrypted by sops/age; concealer only adds the UX: typed secrets, scoping, tags, a professional web UI, tamperâevident audit logs, and an MCP server so AI agents can use secrets without ever seeing them.
Agents use a secret (token) without ever seeing it:
Modern coding assistants â Claude Code, Codex, Gemini CLI, opencode, Cursor â are wonderful, but they read your project files. That means the moment an API key lands in a .env, a credentials.json, or gets pasted into a chat (unfortunately, we've all done this at least once), it can be:
- read by the agent and sent to a model provider,
- captured in logs/telemetry of whatever tool you're using,
- committed to git by accident,
- copied across dozens of repos with the same
OPENAI_API_KEYname and no way to tell them apart.
I didn't want a cloud secret manager (Doppler, Infisical, Vault, 1Password) because:
- Localâonly, provably offline. Secrets never leave my machine. No account, no sync server, no telemetry â you can verify with a firewall that nothing phones home.
- Portable, not machineâbound. The vault is decryptable on any machine with just the master password (age passphrase) â not tied to this laptop's Keychain/TPM. Copy the files, type the password, done.
- Gitâfriendly & inspectable. The encrypted vault is a plain SOPS file you can commit; the tool is a single readable script, not a black box.
- AIâsafe by design. Agents get an MCP server that can list names and inject secrets into a command's environment â but the plaintext values are redacted from output. The agent runs
psql/curlwith the credentials without the key ever appearing in the transcript. - One place, many projects. The same value used across many repos is disambiguated by
tenant / project / environment / repodimensions instead of a single ambiguous name.
If you've ever pasted a secret into a chat window and immediately regretted it â that's the itch this scratches.
One searchable, scoped home for every credential â typed, tagged, masked, and disambiguated by tenant / project / environment / repo.
concealer is not a crypto project. It delegates 100% of encryption to:
| Tool | Repo | Role in concealer |
|---|---|---|
| SOPS | https://github.com/getsops/sops | Encrypts/decrypts the vault (secrets.enc.yaml). Perâvalue AESâ256âGCM, gitâfriendly. |
| age | https://github.com/FiloSottile/age | The encryption backend (X25519). The private key is itself passphraseâwrapped (scrypt) for a portable backup. |
Why this stack: SOPS is a CNCF project used by thousands of teams; age is a modern, audited, boringâonâpurpose encryption tool by Filippo Valsorda. Reusing them means the securityâcritical code is the part that has already been reviewed by the world â concealer is just glue. This is deliberate: the laziest secure design is the one where you write the least security code.
âââââââââââââââ CLI / Web UI / MCP ââââââââââââââââââââââââ
â concealer â ââââââââââââââââââââââââââś â secrets.enc.yaml â
â (1 script) â load â dict â save â (SOPS + age, on disk)â
âââââââŹââââââââ ââââââââââââââââââââââââ
â delegates all crypto; key stays in memory (SOPS_AGE_KEY)
âź
sops ââ age ââ keys/age-key.txt.age (age key, master-password wrapped â the ONLY key at rest)
keys/master.json (scrypt verifier for the UI)
keys/recovery.json (recovery-code hashes + code-wrapped key)
keys/agents.json (unlock-token hashes + token-wrapped key)
keys/audit.log (HMAC-chained + seq) ¡ keys/audit.head (tail anchor)
- Vault: one encrypted JSON (stored as YAML by SOPS). Each secret is a typed record with dimensions, tags, url, notes.
- Keyâatârest: the age private key is never written to disk in plaintext. It exists only wrapped â by the master password (
age-key.txt.age), by recovery codes, and by unlock tokens â and is handed tosopsin memory. Copying the vault folder gets you nothing without the master password or a token. - Unlock tokens: humans run
concealer unlock(TTL token), agents get a revocable token viaconcealer agent register. The token value lives only in your environment (CONCEALER_TOKEN); the disk holds just its hash + a tokenâwrapped key. No password reâprompts. - Recovery codes:
initprints 8 oneâtime codes â store them elsewhere. Any code recovers the vault if you forget the master password, andpasswdrequires one as a 2nd factor. - Audit: every access (CLI/Web/MCP) is appended to an HMACâchained log with a monotonic
seq; altering, deleting, reordering, or truncating the tail breaks verification (anchored byaudit.head).
Runs natively on macOS, Linux, and Windows â all four interfaces (CLI ¡ Web ¡ MCP ¡ TUI) are verified on each in CI.
# Homebrew (recommended, macOS/Linux) â pulls in sops, age and expect automatically:
brew install fxerkan/tap/concealer
# or pipx (all platforms, incl. Windows):
pipx install concealer # + pywinpty/windows-curses on Windows
scoop install sops age # the binaries it wraps (Windows); brew/apt elsewhereOn Windows see the Windows guide (Scoop/pip install, environment variables, security caveats, screenshots).
Or run the single script directly (needs python3, sops, age, expect on PATH):
# prerequisites (only for the manual method)
brew install sops age # macOS (or your package manager)
# get concealer
git clone https://github.com/fxerkan/concealer.git
cd concealer
ln -sf "$PWD/concealer" ~/bin/concealer # optional: put on PATH
ln -sf "$PWD/concealer" ~/bin/cer # optional: short alias â `cer web`, `cer add`, `cer run`
# first-time setup â generates keys, asks for a master password
concealer init # or: cer init
# â prints 8 one-time RECOVERY CODES (save them elsewhere!) and a starter
# `export CONCEALER_TOKEN=âŚ` line. The plaintext age key is then removed.
# unlock the CLI for your shell session (or paste the token init printed):
eval "$(concealer unlock)" # asks master password, exports CONCEALER_TOKEN (~8h)Requires Python 3 (stdlib only â no pip install), plus sops, age, and expect (macOS/Linux; on Windows pywinpty replaces expect, installed automatically by pipx).
concealer set --name OPENAI_API_KEY --project proj-a --env prod 'sk-...' --tags ai
concealer set --name MAIN_DB --type database --tenant acme --project billing --env prod \
host=db.acme.io port=5432 database=billing username=svc password=secret auth_type=password
concealer list --type database --tenant acme
concealer search OPENAI
concealer get --name OPENAI_API_KEY --project proj-a --env prod
concealer rotate --name OPENAI_API_KEY --project proj-a # 32-byte random if no value
concealer rm --name OLD_KEY --project proj-a
concealer run --project proj-a --env prod claude # inject into env, run tool
concealer audit # recent audit entries
concealer audit verify # chain + tail-anchor integrityeval "$(concealer unlock)" # human: master password â CONCEALER_TOKEN (TTL, ~8h) in your shell
concealer agent register claude # agent: master password â long-lived, revocable token for MCP env
concealer agent list # show tokens (label, source, expiry/revoked)
concealer agent revoke claude # (or `all`) revoke a token
concealer harden # migrate an old plaintext-key vault to key-at-rest
concealer passwd # change master password â needs current pw + a recovery code
concealer recover # forgot the master password? recover with a recovery code
concealer recovery # regenerate the recovery-code set (needs master password)The token value is only ever in your environment (CONCEALER_TOKEN); the vault stores just its hash and a tokenâwrapped key. Revoke a token and that copy is dead.
Every secret carries tenant / project / environment / repo. Empty = wildcard (a default). On run, the mostâspecific match wins: acme/proj-a/prod overrides proj-a overrides global. Unspecified dimensions on run are autoâdetected from the current git repo.
Each type has its own typeâaware form so you only enter the fields that make sense, and secretâish fields (password/value/token/pin/âŚ) are stored masked and revealed only on demand (audited). Any field name works too via custom.
| Type | Fields |
|---|---|
api_key |
value |
access_token |
token, refresh_token, expires, scopes |
oauth |
client_id, client_secret, auth_url, token_url, scopes |
jwt |
token, issuer, audience, expires |
ssh_key |
private_key, public_key, passphrase, host, user |
certificate |
certificate, private_key, chain, expires |
database |
host, port, database, schema, username, password, auth_type, jdbc_url |
server |
host, port, username, password, ssh_key |
website |
web_url, username, password |
login |
web_url, username, password, totp |
pin |
pin, label (phone / door PINs) |
wifi |
ssid, password, security |
membership |
provider, member_id, password |
secure_note |
note |
custom |
any key/value you define |
No PII by design. There are deliberately no creditâcard / passport / nationalâID types â this vault is for machine & account credentials, not identity documents.
Each type renders exactly the inputs it needs â an API key is a single value, a cloud credential carries its client/secret/URLs, a database its host/port/user/password, a website its URL/login:
Typeâaware entry: cloud tokens ¡ database connections ¡ website logins ¡ freeâform custom fields. Secret fields are masked; plain fields (host, url, username) stay readable and become optional table columns.
Opens http://127.0.0.1:8787 (localhost only). Features:
- TR / EN interface toggle (topâright)
- Full CRUD with typeâaware forms ¡ responsive (phone/tablet) layout
- Search + searchable, multiâselect type/tenant/project/environment/repo/tags filters
- Sortable, reorderable columns â including any custom field (web_url, host, âŚ) as its own column
- Perâsecret Deploy: render the exact CLI/manifest to push a secret to
export/docker/k8s/aws-secrets/aws-ssm/github/⌠- Copy to clipboard with autoâclear (20s) ¡ password show/hide toggle
- Metadata: url, tags, notes
- Autoâlock on idle (default 300s,
CONCEALER_IDLE=âŚto change) - Audit Log viewer: filter by action/source/key/date, pagination, row detail, chain verification, CSV/JSON export
Audit Logs â every read/write/copy/inject is HMACâchained; verify integrity or export to CSV/JSON.
Risks â finds the same value reused across projects and scores the blast radius.
Scan folder â sweep a directory (or shell history) for stray secrets and import them, tagged by origin.
A companion Chrome extension opens your vault and copies secret values straight from the toolbar â
without typing cer web. It talks only to your local server on 127.0.0.1; no cloud, no accounts.
cer chrome-extension # one-time: register the native helper (macOS/Linux/Windows)Then click the toolbar icon â unlock â copy. Highlights:
- Perâfield copy â multiâfield secrets expand into child rows; copy or reveal exactly the field you need.
- Onâdemand â starts
concealer webwhen you open the popup and selfâexits after 15 min idle (nothing lingers). - Autoâlock countdown, đ˛ password generator, search, and three themes (Dark ¡ White ¡ Matrix).
- Clipboard autoâclears; values are never logged.
Chrome extension â pick the exact field to copy, right from the toolbar.
Install & full guide â Chrome Extension docs.
The native host is built into concealer (concealer native-host) â no separate program to install.
Add to Chrome â on the Chrome Web Store.
Register once, available in every session. On a hardened (keyâatârest) vault the MCP server unlocks with a token, so give it an agent token instead of your password â it never prompts and you can revoke it anytime:
concealer agent register claude # prints a CONCEALER_TOKEN for this agent
claude mcp add --scope user concealer \
--env CONCEALER_TOKEN=<token-from-above> \
-- /path/to/concealer/concealer mcpWithout a valid token the server fails closed â no secret ever leaks. Revoke with concealer agent revoke claude.
Tools exposed to the agent:
list_secrets/search_secretsâ names, types, scopes, tags (never values)run_with_secretsâ runs a command with secrets injected into env; values are redacted from the returned output
The agent can use a DB password to run a query, but the password never appears in its context. Every MCP access is written to the audit log with source=mcp.
Agents list secret names â values stay hidden:
# copy: secrets.enc.yaml + .sops.yaml + keys/ (age-key.txt.age, master.json,
# recovery.json, audit.*) â but NOT any CONCEALER_TOKEN (that stays per-machine)
eval "$(concealer unlock)" # asks the master password on the new machine, mints a fresh token
concealer list # works â machine-independentThe vault is bound to a password (or a recovery code), not to this machine's hardware. Tokens are perâmachine on purpose: a copied folder is inert until someone types the master password.
- Encryption: AESâ256âGCM (SOPS) over age X25519. Key derivation for the wrapped key backups + UI verifier: scrypt / ageâscrypt.
- Keyâatârest: on a hardened vault the age private key is never on disk in plaintext â only masterâpasswordâ, recoveryâcodeâ, and tokenâwrapped copies exist. It reaches
sopsin memory (SOPS_AGE_KEY). Old vaults with a0600 keys/age-key.txtstill work; runconcealer hardento migrate. - Unlock tokens are held clientâside (
CONCEALER_TOKEN); the vault stores only a scrypt hash + a tokenâwrapped key, and every token is revocable with expiry support. Agents get their own token â no shared password. - Recovery codes wrap the key too: any one recovers the vault, and
passwdconsumes one as a 2nd factor so a stolen master password alone can't rotate the key. - The audit log holds key names and actions, not values â tamperâevident via an HMAC chain plus a
seqand anaudit.headanchor that catches tailâtruncation. Honest ceiling:keys/audit.keyis local, so a filesystemâroot attacker with full access can still reâforge the chain; true immutability needs an offâmachine key/anchor. - The web UI binds to
127.0.0.1only and is singleâuser; treat it as a local convenience, not a hardened multiâuser server. - Nothing in
keys/,secrets.enc.yaml, or.sops.yamlis committed â see.gitignore. This repo ships the tool, never a vault.
concealer passwd # asks the CURRENT password + a RECOVERY CODE (consumed), then sets the new oneRequiring a recovery code means whoever learns your master password still can't take the vault over without one of the codes you stored elsewhere. Out of codes? concealer recovery mints a fresh set.
concealer recover # asks for a recovery code, restores access, sets a new master passwordMIT.
concealer is glue over SOPS and age. All the hard cryptography is theirs; the laziness is mine.
- SOPS
- age
- secretctl
Developed by FXerkan - Code more, worry less.


