Skip to content

santhsecurity/envseal

envseal

envseal

Part of Santh - open source Rust security and infrastructure tooling. Follow @SanthProject on X.

crates.io crates.io docs.rs CI License: MIT OR Apache-2.0

⚠️ 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 start is 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.

Why?

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 two guarantees

  1. 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.key copied off and the passphrase brute-forced, the file does not decrypt on any other device. envseal doctor reports your active tier.
  2. 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 only Error::NoDisplay.

Install

From cargo (any platform with Rust 1.77+)

# 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 --locked

Surface 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 --locked

Pre-built binaries

Binary 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.

Optional: keyhog scanner

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 --locked

keyhog 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.

First-run setup (one minute)

# 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.fish

For AI agents (via MCP)

The 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.

Security tiers: pick how friction-heavy you want approvals

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).

Security surface: every toggle, in one place

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.

Approval pipeline

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.

Always-on hardening (no toggle, it just runs)

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.key is bound to the device's TPM 2.0 / Secure Enclave / Windows DPAPI. Stolen master.key won'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_CLOEXEC on every secret FD, PR_SET_NO_NEW_PRIVS before exec.
  • Memory protection: memfd_secret() on Linux 5.14+ unmaps the master key from kernel page tables; mlock + MADV_DONTDUMP fallback on older kernels; VirtualLock on Windows; mlock on 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.toml is 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") returns CryptoFailure unconditionally, audited as critical [ctf.flag.decrypt_attempt]. Verify-by-hash only.

Detection signals (each one fires its own audit event)

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.

Sandbox tiers (for supervised)

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.

CTF: try to break it. Publicly credited if you do.

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 tier

What'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.

MCP server setup

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 CLI (Claude Code)

claude mcp add envseal /path/to/envseal-mcp

Or edit ~/.claude.json and add to mcpServers:

{
  "mcpServers": {
    "envseal": {
      "command": "/path/to/envseal-mcp"
    }
  }
}

Gemini CLI

Edit ~/.gemini/settings.json:

{
  "mcpServers": {
    "envseal": {
      "command": "/path/to/envseal-mcp"
    }
  }
}

Gemini CLI requires absolute paths; ~ expansion is not honored.

Cursor / Antigravity / generic MCP-aware editor

Add to your editor's MCP config (typically ~/.cursor/mcp.json or ~/.antigravity/mcp.json):

{
  "mcpServers": {
    "envseal": {
      "command": "/path/to/envseal-mcp"
    }
  }
}

Verify the MCP server is wired up

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.

The .envseal File

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

Commands

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

Threat model

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

GUI Security Assessment

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 Support

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

Migrating from .env, op, doppler, or your shell history

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 shape

After migration, envseal run -- <cmd> reads the nearest .envseal file and injects every reference. (See The .envseal File above for the format.)

Desktop app (preview, untested)

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

Vulnerability reports

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.

Roadmap & contributing

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 fuzz harness 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.

License

MIT OR Apache-2.0

About

Built by Santh.

About

Secret vault for the agent era. Hardware-sealed at rest, GUI-gated at use, only injected into approved child processes.

Topics

Resources

License

Unknown, MIT licenses found

Licenses found

Unknown
LICENSE-APACHE
MIT
LICENSE-MIT

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages