Part of Santh - open source Rust security and infrastructure tooling. Follow @SanthProject on X.
⚠️ Beta. Actively hardening toward 1.0. Try to break it.
Hey, thanks for trying envseal. Small solo project. Core (vault, hardware seal, signal taxonomy, sealed config) is hand-tested on Windows + Linux end-to-end with 900+ unit, regression, property, and adversarial tests, but it hasn't been stress-tested by anyone other than me. If something is awkward, wrong, or surprising, open an issue. I read all of them. The CTF in
envseal ctf startis the fastest way to attack it; I'll publicly credit anyone who finds something.
Recent (0.3.14): Encrypted audit log (AES-256-GCM events under a per-vault hardware-sealed key), every deferred audit follow-up closed (atomic Windows DACL at create, persistent TOTP rate-limit, homoglyph defense on every dialog field, full symlink-destruction sweep), and the rmcp dependency bumped to 1.6 to clear all open advisories. See CHANGELOG.md and ROADMAP.md for what's next.
Sudo can't read your keys. Hardware-sealed at rest, GUI-gated at use, copyable to no other machine.
envseal is a developer-first secret vault that turns API keys into
device-bound capabilities. The plaintext exists in exactly two places:
inside the silicon that minted it (Secure Enclave, TPM, or DPAPI-bound
user master), and inside the descendant process you authorized at the
keyboard. Nowhere else: not in cat, not in /proc/PID/mem, not in a
stolen master.key, not in your shell history.
In 2026, AI agents run with full filesystem and shell access. Every .env file is readable via cat .env. Every API key pasted into a chat window is logged to cloud AI providers. Stack the failure modes:
| Current Defense | Why It Fails |
|---|---|
.gitignore |
Prevents commit, not read. |
.cursorignore |
Prompt-based; pushed out of the context window. |
| OS Keychain | Agent calls secret-tool lookup and walks out with plaintext. |
| Password managers | Authenticate the user, not the program asking. |
.env + dotenv |
Agent runs cat .env and the rest is history. |
| Pasting into chat | Key enters context, logs, and cloud retention. |
envseal authenticates the program. Secrets can only be released to a whitelisted binary, approved via a GUI popup terminal-bound agents cannot click, and the wrap key that protects the vault never leaves your physical device.
- The key cannot leave this machine. The master key on disk is
double-wrapped: passphrase (Argon2id) on the inside, hardware-bound
seal on the outside (Windows DPAPI, macOS Secure Enclave, Linux
TPM 2.0). Even with
master.keycopied off and the passphrase brute-forced, the file does not decrypt on any other device.envseal doctorreports your active tier. - The key cannot be read without you at the keyboard. Every
release goes through a desktop popup.
sudo,ptrace,cat /proc/PID/mem, agent shells with full root: none of them can click "Allow." Headless attackers see onlyError::NoDisplay.
# CLI (binary on PATH is `envseal`). RECOMMENDED, fully tested.
cargo install envseal-cli --locked
# MCP server for AI agents (Claude Desktop, Cursor, Antigravity, Gemini CLI). RECOMMENDED, fully tested.
cargo install envseal-mcp --locked
# Native desktop app (egui, no JavaScript, no webview). UNTESTED, use at your own risk.
cargo install envseal-gui --lockedSurface status. The CLI and MCP server are the recommended, production-ready entry points; every release runs them through the full unit / integration / adversarial / property / regression / deep-security / e2e suites. The desktop GUI ships builds but the end-to-end UX flows are not yet covered by automated tests; treat it as a preview until the GUI test harness lands. CLI + MCP have parity for every security-relevant operation, so nothing is gated behind the GUI.
Or from a checkout:
git clone https://github.com/santhsecurity/envseal
cd envseal
cargo install --path cli --locked
cargo install --path mcp --locked
cargo install --path desktop-native --lockedBinary releases for Windows / macOS / Linux are attached to each
GitHub release.
Download the archive for your platform, extract, and place
envseal (and optionally envseal-mcp, envseal-gui) somewhere
on your PATH.
envseal scan and envseal init --from-history use the
keyhog scanner when
present (896 secret-pattern detectors with GPU acceleration). When
keyhog isn't on PATH, envseal falls back to a built-in regex
scanner that covers the common providers. To install:
cargo install keyhog --lockedkeyhog is intentionally NOT a hard dependency. It pulls in GPU runtimes that most envseal users don't want, and the fallback regex catches the most common cases. If you operate at scale or audit unfamiliar codebases, install keyhog separately.
# 1. Verify your platform's protection tier (DPAPI, Secure Enclave, TPM).
envseal doctor
# 2. Initialize your vault by storing your first secret. envseal will
# prompt for a passphrase via a desktop dialog. Stdin is the only
# way values enter the vault; never pass secrets as CLI args.
echo 'sk-proj-…' | envseal store openai-key
# 3. Use it. The first inject pops an approval dialog. Click
# "Allow Always" and you won't see it again for that binary+secret.
envseal inject openai-key=OPENAI_API_KEY -- python app.py
# 4. Other day-to-day operations.
envseal list # names only, never values
envseal peek openai-key # redacted preview
envseal revoke openai-key # secure zero-fill + delete
# 5. Shell completions (optional).
eval "$(envseal completions bash)" # add to .bashrc
eval "$(envseal completions zsh)" # add to .zshrc
envseal completions fish | source # add to config.fishThe agent-native workflow uses envseal_request_key. The agent proposes
a key name, a GUI popup appears, the user pastes the value. The agent
never sees the secret.
Agent: "I need your Cloudflare API token"
→ calls envseal_request_key("cloudflare-api", "Cloudflare API Token")
→ GUI popup: "Paste your Cloudflare API Token: [________] [Store]"
User: *copies from dashboard, pastes into popup, clicks Store*
→ Secret encrypted and stored. Agent receives only "stored".
Agent: "Deploying to Cloudflare..."
→ calls envseal_inject(cloudflare-api=CLOUDFLARE_API_TOKEN -- wrangler deploy)
→ GUI popup: "Allow wrangler to access cloudflare-api? [Allow Once] [Always Allow] [Deny]"
User: *clicks Allow Once*
→ wrangler deploys. Key never touched the chat.
Same effort as pasting into chat: 1 paste + 1 click vs 1 paste. The extra click IS the security.
envseal ships with three preset tiers. Switch with envseal security preset standard | hardened | lockdown (or click the tier buttons in
the desktop GUI's Settings tab). Detection signals fire the same way
in every tier; the tier decides what Action each Severity maps
to via the policy table at core/src/guard/signal.rs.
| Tier | Approval popup | Friction | Default for | Use when… |
|---|---|---|---|---|
| Standard | Allow Once / Allow Always / Deny | None, instant approval | Daily dev | You trust your shell + the binaries you run |
| Hardened | Same buttons + 2-second pre-popup delay | Anti-click-through delay, virtual keyboard on X11 | CI runners, second laptops | You want a beat to think before approving |
| Lockdown | Same buttons + 5-second delay + 4-digit numeric challenge before each batch | Maximum friction, hard-blocks any Hostile signal |
High-value vaults, root accounts | You want it to be obviously inconvenient to approve under coercion |
The tier promotes signal severities through a 5×3 policy matrix. A
Severity::Hostile signal (e.g. io.stdin.piped) at Standard
surfaces as a friction gate; at Hardened it's a friction gate with
delay; at Lockdown it's a hard block. You can override any
(severity, tier) cell or any specific SignalId via
envseal security override-tier / override-signal (or the
read-only summary in the desktop GUI's Settings panel).
These are the knobs that ship with envseal. Most are on by default; the rest you opt into. Every action is hash-chained into the audit log, so flipping a setting is itself an event you can review later.
| Knob | Command | What it does |
|---|---|---|
| Tier | envseal security preset <standard|hardened|lockdown> |
Sets the entire policy matrix in one shot. Per-cell overrides win over the preset. |
| Per-signal override | envseal security override <signal-id> <ignore|log|warn|warn_with_challenge|block> |
Pin one detector (e.g. io.stdin.piped) to a specific action regardless of tier. |
| Per-tier override | envseal security override-tier <severity>:<tier> <action> |
Override one cell of the 5×3 matrix (e.g. hostile:standard = block). |
| List overrides | envseal security overrides |
Read-only dump of every current override. |
| Approval delay | envseal security set approval_delay_secs <N> |
Anti-click-through delay before the popup accepts a click (default 2 s on Hardened, 5 s on Lockdown). |
| Numeric challenge | envseal security set challenge_required true |
Require the user to type a 4-digit code shown on screen before approval (default on Lockdown). |
| Audit logging | envseal security set audit_logging <true|false> |
Toggle the hash-chained log. Off only ever for fully-air-gapped CI. |
| TOTP 2FA | envseal security totp-setup / totp-disable |
Adds an HOTP/TOTP second factor on top of the passphrase for vault unlock. |
| Relay required | envseal security set relay_required true |
Refuse local GUI; only accept approvals via the paired-device relay. For headless / SSH workflows. |
| X11 auto-upgrade | envseal security set x11_auto_upgrade <true|false> |
When on X11 (no input isolation), auto-promote to virtual-keyboard input so passphrases can't be xdotool-replayed. |
These are not configurable; they're the floor.
- Vault root validation: refuses to open in
/tmp, world-writable dirs, or any path whose component is a symlink. Path-traversal in secret names rejected at parse time. - AES-256-GCM with
name-bound AAD: every secret is sealed with its own name as additional authenticated data, so a renamed file on disk fails to decrypt. - Argon2id KDF: passphrase wrapping uses Argon2id with parameters sized for ≥ 0.5 s on a modern laptop.
- Hardware seal: the wrap key that protects
master.keyis bound to the device's TPM 2.0 / Secure Enclave / Windows DPAPI. Stolenmaster.keywon't open on another box. - Self-preload check: refuses to run if
LD_PRELOAD,DYLD_INSERT_LIBRARIES, or equivalent is set in the inherited env. - Process hardening:
PR_SET_DUMPABLE(0),RLIMIT_CORE = 0,O_CLOEXECon every secret FD,PR_SET_NO_NEW_PRIVSbefore exec. - Memory protection:
memfd_secret()on Linux 5.14+ unmaps the master key from kernel page tables;mlock+MADV_DONTDUMPfallback on older kernels;VirtualLockon Windows;mlockon macOS. Master key zeroized on drop. - Hash-chained audit log: every event includes the SHA-256 of
the previous entry. Tampering breaks the chain; the corrupted file
is rotated to
audit.log.corrupted-<ts>and a fresh chain starts. - Policy integrity:
policy.tomlis HMAC-signed; any unsigned edit rejects subsequent operations until repaired. - Empty-secret refusal: vault refuses to store a zero-byte
value (defends against
cat /dev/null | envseal store creds). - CTF flag carve-out:
Vault::decrypt("ctf-flag")returnsCryptoFailureunconditionally, audited ascritical [ctf.flag.decrypt_attempt]. Verify-by-hash only.
| Source | Signal id | Severity | What it catches |
|---|---|---|---|
migration.preexec |
io.stdin.piped |
hostile | A secret value piped into envseal's stdin on a tty session |
migration.preexec |
io.cmdline.shell_subst |
hostile | $(…) / backtick / eval in the requesting argv |
guard.gui |
gui.input_injector |
critical | xdotool / ydotool / xte / wtype running |
guard.gui |
gui.screen_recorder |
warn | OBS / SimpleScreenRecorder / ffmpeg recording |
guard.gui |
gui.accessibility_tool |
warn | screen-reader / AT-SPI bridge active |
guard.gui |
gui.session.ssh |
critical | SSH session, no local display |
guard.gui |
gui.session.x11_forward |
critical | X11 forwarding, passphrase interceptable |
guard.gui |
gui.session.remote_desktop |
warn | RDP / VNC session detected |
guard.env |
env.loader.preload |
critical | LD_PRELOAD or peer set in inherited env |
vault.entropy |
secret.entropy.too_short |
warn | secret < 8 bytes |
vault.entropy |
secret.entropy.low_shannon |
warn | low Shannon entropy for its length |
vault.entropy |
secret.entropy.placeholder.<word> |
warn | matches todo, changeme, your-api-key, … |
audit.chain |
audit.chain.rotated_corruption |
critical | tamper-detected log was rotated |
vault.binary |
binary.tampered |
critical | approved binary's hash no longer matches |
vault.policy |
policy.tampered |
critical | policy.toml HMAC mismatch |
vault.dpapi |
vault.dpapi.device_mismatch |
critical | master.key wrap was minted on a different device |
ctf |
ctf.flag.decrypt_attempt |
critical | something tried to read the CTF flag |
ctf |
ctf.verify.incorrect |
info | wrong flag submitted |
Every signal renders to stderr at the time it fires AND is appended
to the audit log. envseal audit --json gives the full structured
stream; envseal security overrides shows the action you've pinned
for each.
envseal supervised <name>=<VAR> [--sandbox=<tier>] -- <cmd> runs
the child under live leak-detection AND, optionally, a sandbox:
| Sandbox tier | Linux | macOS | Windows |
|---|---|---|---|
none (default) |
observe only | observe only | observe only |
standard |
seccomp filter (no ptrace, no process_vm_readv) |
sandbox_init pure-computation profile |
Job Object with restricted handle access |
hardened |
+ new pid/mount/uts namespaces | + restricted-network profile | + restricted desktop |
lockdown |
+ read-only root, no network namespace | + filesystem-read-only profile | + integrity-low token |
The supervisor inspects child stdout/stderr in real time; any
substring match for a known secret value triggers
supervisor_leak_detected and terminates the child.
Once a secret enters the vault, the claim is: nobody gets it out. Not a human, not an AI agent, not both working together.
envseal ctf start # generates ENVSEAL_CTF{...}, seals it, switches the vault to Lockdown
envseal ctf rules # what's allowed, what's banned (only DMA / cold-boot / JTAG are off-limits)
envseal ctf status # check active challenge + verification hash
envseal ctf verify <flag> # submit a guess
envseal ctf reset # clean up + restore your pre-CTF tierWhat's allowed: memory reading (/proc/pid/mem, process_vm_readv,
ptrace), binary replacement, PATH poisoning, LD_PRELOAD, GUI
automation tools (xdotool, ydotool, xte, libinput replay), config
tampering, side channels (timing/cache/power), kernel modules,
sandbox escape, race conditions, TOCTOU, anything creative in
software.
What's banned: only physical hardware attacks (DMA via Thunderbolt/PCIe, cold-boot RAM transplant, JTAG). Those bypass software on every machine, not just envseal.
Reward: Hall of Fame in SECURITY.md + writeup credit. There's
no bug-bounty cash because this is a one-person project, but if you
demonstrate a Tier 1 break (extract the flag from a Lockdown vault),
the writeup will be the public record of envseal's first failure
mode, high-prestige finding.
envseal ships an MCP (Model Context Protocol) server that lets AI
agents store, list, inject, and verify secrets without ever seeing
plaintext. Each tool call surfaces a GUI approval popup that the
human authorizes; the agent only sees OK or denied.
Run which envseal-mcp to get the absolute path of the binary
installed by cargo install envseal-mcp (or by extracting the
release archive to your PATH). Every config below needs it
(replace /path/to/envseal-mcp).
claude mcp add envseal /path/to/envseal-mcpOr edit ~/.claude.json and add to mcpServers:
{
"mcpServers": {
"envseal": {
"command": "/path/to/envseal-mcp"
}
}
}Edit ~/.gemini/settings.json:
{
"mcpServers": {
"envseal": {
"command": "/path/to/envseal-mcp"
}
}
}Gemini CLI requires absolute paths; ~ expansion is not honored.
Add to your editor's MCP config (typically ~/.cursor/mcp.json or
~/.antigravity/mcp.json):
{
"mcpServers": {
"envseal": {
"command": "/path/to/envseal-mcp"
}
}
}After restarting your agent, ask it: "list the envseal tools you have".
The agent should enumerate envseal_store, envseal_inject,
envseal_list, envseal_audit, envseal_doctor, etc. If it says it has
no envseal tools, the path in the MCP config is wrong or the binary is
not executable. Try chmod +x /path/to/envseal-mcp and check the agent's
MCP startup log.
Per-agent web walkthroughs: docs/setup/ has copy-pasteable agent-specific prompts (Cursor, Antigravity, Gemini CLI, Claude CLI, manual) for the same configuration.
Drop-in .env replacement. Safe to commit to git: contains references, not values:
# .envseal: safe to commit
DATABASE_URL=database-url
REDIS_URL=redis-url
OPENAI_API_KEY=openai-key
STRIPE_SECRET_KEY=stripe-key# Generate from existing vault secrets
envseal init
# Run your app
envseal inject-file .envseal -- npm run dev| Command | Description |
|---|---|
envseal store <name> [--force] [--gui] |
Store a secret (stdin or GUI popup) |
envseal request-key <name> [--description d] |
Agent-initiated GUI key request |
envseal inject <name>=<VAR> -- <cmd> |
Inject secret into a process |
envseal inject-file <path> -- <cmd> |
Inject all secrets from .envseal file |
envseal run -- <cmd> [args...] |
Run command with explicit .envseal mappings |
envseal shell [--login] |
Open subshell with mapped secrets injected |
envseal pipe <name> -- <cmd> |
Deliver a single secret on the child's stdin (no env var) |
envseal supervised <name>=<VAR> [--sandbox=<tier>] -- <cmd> |
Run with real-time leak detection and optional sandbox tier (standard, hardened, lockdown) |
envseal scan [--deep] [--path <dir>] |
Scan dotfiles + cwd .env* (or whole $HOME with --deep); uses keyhog if installed |
envseal scan --system |
Hand off to keyhog scan-system. Every mounted drive + every git history (needs keyhog). |
envseal (no args) / envseal tui |
Open the interactive terminal UI (Welcome, Quickstart, Secrets, Doctor, Scanner, Audit, CTF) |
envseal import <.env> [--delete] |
Migrate from .env to envseal |
envseal init [path] |
Scaffold .envseal project file |
envseal doctor |
System security readiness check |
envseal status [--json] |
Show vault status (tier, secret count, etc.) |
envseal list |
List secret names (never values) |
envseal peek <name> |
Show a redacted preview of a secret |
envseal revoke <name> |
Secure delete a secret |
envseal copy <from> <to> |
Duplicate a secret |
envseal rename <from> <to> |
Rename a secret |
envseal alias <cmd...> |
Generate shell aliases that wrap envseal run |
envseal policy show |
Show whitelist rules |
envseal policy revoke-binary <path> |
Remove rules for a binary |
envseal audit [N] |
Show last N audit log entries |
envseal security show|set|preset |
Manage security configuration tiers |
envseal health [--max-age=N] |
Secret health/compliance report |
envseal git-hook <install|show|gitignore> |
Install and inspect leak-prevention hook |
envseal hook <install|uninstall|show|status> [bash|zsh|fish] |
Manage the shell preexec hook that captures typed keys |
envseal init --from-history |
Migrate every secret previously typed at the shell |
envseal op run -- <cmd> / envseal doppler run -- <cmd> / envseal vault run -- <cmd> |
Drop-in compatibility shims for 1Password / Doppler / HashiCorp Vault CLI surface |
envseal emergency-revoke |
Wipe all secrets from the vault |
envseal ctf <start|verify|rules|status> |
CTF challenge workflows |
envseal completions <bash|zsh|fish> |
Generate shell completions |
The full defense list lives under Always-on hardening above. This table is the operator-facing version: who can read a secret today, and who cannot.
Zero network connections. envseal makes none. No telemetry, no update checks, no cloud accounts.
| Attacker | Access? |
|---|---|
| AI agent with terminal access | No, cannot interact with GUI popup |
| Malicious script in build pipeline | No, binary not whitelisted |
| Root user (ptrace/proc) | No, memfd_secret + PR_SET_DUMPABLE(0) |
Stolen master.key on attacker's machine |
No, hardware seal refuses on a different device |
Brute-force passphrase against stolen master.key |
No, without the device's TPM/SEP/DPAPI key the wrap doesn't open |
| Human at the keyboard | Yes, can approve via GUI |
| Remote desktop attacker | Partial, detected via assess_gui_security() |
| Supply chain (npm token theft) | No, token never enters agent context |
envseal continuously monitors the GUI environment:
- Remote session detection: SSH, X11 forwarding, RDP/VNC
- Input injection detection: xdotool, ydotool, xte, wtype
- Screen recording detection: OBS, SimpleScreenRecorder, ffmpeg
- Threat classification: Safe / Degraded / Hostile
| Platform | GUI Backend | Hardware Seal | In-Memory | Sandbox | Status |
|---|---|---|---|---|---|
| Linux | zenity / kdialog | TPM 2.0 (via tpm2-tools, optional) | memfd_secret (5.14+) |
unshare namespaces (Hardened/Lockdown) | Supported |
| macOS | osascript | Secure Enclave (P-256, ECIES) | mlock | sandbox_init profiles |
Supported |
| Windows | PowerShell Forms | DPAPI (TPM-rooted on Win10+) | VirtualLock | Job Objects | Supported |
envseal is built to be the cheapest path off whatever you're using today. Pick the entry point that matches how the secrets are leaking:
# 1. Bulk migrate a .env file
envseal import .env --delete
# 2. Migrate everything you've already typed at the prompt
envseal init --from-history # scans bash/zsh/fish history,
# imports each detected key into the vault
# 3. Live-capture future keys without thinking about it
envseal hook install # auto-detects $SHELL,
# writes a preexec hook into .bashrc/.zshrc/.config/fish/config.fish
# Now whenever you type:
# OPENAI_API_KEY=sk-... node app.js
# envseal pops a "store this for next time?" dialog before the command runs.
# 4. Drop-in for existing scripts that called op / doppler / vault
envseal op run -- node app.js # 1Password CLI shape
envseal doppler run --config dev -- npm run dev # Doppler shape
envseal vault run -- ./deploy.sh # HashiCorp Vault shapeAfter migration, envseal run -- <cmd> reads the nearest .envseal file and injects every reference. (See The .envseal File above for the format.)
The envseal-gui binary in desktop-native/ is a pure-Rust egui/eframe app (no webview) covering the read-only views the CLI exposes. As noted in Install above, the GUI is not yet covered by the automated test suites. Tabs:
- Dashboard: Overview of vault state and recent activity
- Secrets: View and revoke stored secrets
- Store: Encrypt new secrets via the GUI
- Policy: View and revoke binary access rules
- Settings: Security tier, input protection, and threat assessment
- Audit: Session activity tracking
- Health: Secret health and compliance report
- Scanner: Scan for leaked secrets and misconfigurations
Beyond the CTF, we welcome vulnerability reports at any tier, from GUI bypass to crypto weakness. See SECURITY.md for the 3-tier disclosure program and how to submit findings.
What's in flight, what's queued, and (importantly) a list of well-scoped items we'd love outside contributors to take on. From "add a regex pattern" to "build the watchdog process that monitors envseal's heartbeats and detects spoofed popups," there's something at every experience level.
→ ROADMAP.md: the full living roadmap, with claim instructions and Hall-of-Fame credit for shipped contributions.
A few highlights currently open for community work:
- Heartbeat & watchdog process: a separate lightweight monitor that consumes signed heartbeats from envseal, observes OS popup events, and alerts on anomalies. Out-of-tree by design; a great systems-programming entry point.
- Telegram-bot relay: phone approvals via inline buttons, no envseal mobile app required.
cargo fuzzharness for the v3 envelope parser:core/fuzz/is primed for it.- Reproducible-build verifier: independently rebuild a release tag and confirm byte-for-byte equivalence.
- Documentation translation: wider reach in non-English-speaking dev communities.
See CONTRIBUTING.md for the dev setup and merge bar; ROADMAP.md has the architectural context for each item.
MIT OR Apache-2.0
Built by Santh.
