# secretsh
**Secure subprocess secret injection for AI agents.**
[](https://github.com/lthoangg/secretsh/actions/workflows/ci.yml)
[](https://crates.io/crates/secretsh)
[](https://pypi.org/project/secretsh/)
[](LICENSE)
> **Beta.** Core functionality is stable and tested. Read [What it does NOT](#what-it-does-and-does-not) before deploying in sensitive environments.
AI agents write commands with `{{PLACEHOLDER}}` tokens. secretsh resolves them from an encrypted vault at exec time and scrubs any secrets that leak back through output.
```
Agent writes: curl -u admin:{{API_PASS}} https://internal/api
Child runs: curl -u admin:hunter2 https://internal/api
Agent sees: curl -u admin:[REDACTED_API_PASS] https://internal/api
```
---
## What it does and does NOT
### Does
| **Keeps secrets out of LLM context** | Agent only ever sees `{{PLACEHOLDER}}`, never the value |
| **Keeps secrets out of shell history** | secretsh reads from an encrypted vault, not the command line |
| **Keeps secrets out of spawn errors** | `command not found: "[REDACTED]"` — never the raw value |
| **Scrubs output (best effort)** | Aho-Corasick substring redaction on stdout/stderr — raw, base64, URL-encoded, hex |
| **Blocks shell oracle attacks** | `--no-shell` rejects `sh`/`bash`/`zsh`/etc. before any child runs |
| **Encrypts at rest** | AES-256-GCM + Argon2id + HKDF — key names and values both encrypted |
### Does NOT
| **Stop prompt injection** | If the agent is tricked into running a malicious command, secretsh executes it |
| **Stop a child reading its own argv** | Secret is in the process's argv for its lifetime — visible in `/proc/<pid>/cmdline` |
| **Handle common-value false positives** | If your secret is `123456`, every `123456` in output is redacted — including unrelated content |
| **Fully close the redaction oracle** | `echo {{KEY}}==guess` leaks one bit per probe — if `==guess` is also redacted, the guess matched |
| **Replace a secrets manager** | No access control, no audit trail beyond local stderr JSON, no rotation |
| **Protect against a compromised passphrase** | If `SECRETSH_KEY` is stolen, the vault is open |
> **In short:** secretsh gives your AI agent the ability to use credentials without the credentials appearing in its context, history, or output — it does not stop a sufficiently adversarial agent from probing or exfiltrating. Use `--no-shell` to raise the bar.
---
## Install
```bash
# Homebrew
brew tap lthoangg/tap && brew install secretsh
# PyPI
uv add secretsh
# From source
cargo install secretsh
```
Pre-built binaries for `x86_64`/`aarch64` on macOS and Linux: [GitHub Releases](https://github.com/lthoangg/secretsh/releases).
---
## Quick Start
```bash
# 1. Set passphrase (silent, not saved to history)
read -rs SECRETSH_KEY && export SECRETSH_KEY
# 2. Create vault and import secrets
secretsh init
secretsh import-env -f .env
# 3. Run commands — secrets injected and scrubbed
secretsh run --no-shell -- curl -u "{{API_USER}}:{{API_PASS}}" https://api.example.com
# 4. List what's stored (values never shown)
secretsh list
```
---
## Commands
| `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 with secret injection + output redaction |
| `secretsh export --out <path>` | Export vault to encrypted backup |
| `secretsh import --in <path>` | Import entries from a backup |
| `secretsh import-env -f <path>` | Bulk import from a `.env` file |
All commands read the passphrase from `SECRETSH_KEY` by default. Use `--master-key-env <VAR>` to override.
### Key `run` flags
| `--no-shell` | off | Block `sh`/`bash`/`zsh`/`dash`/`fish`/`ksh`/`tcsh`/`csh` as argv[0]. **Recommended for AI agents.** |
| `--timeout` | 300s | Kill child after N seconds |
| `--max-output` | 50 MiB | Kill child if stdout exceeds this |
| `--quiet` | off | Suppress audit JSON on stderr |
---
## Python API
```python
import secretsh
with secretsh.Vault(master_key_env="SECRETSH_KEY") as vault:
result = vault.run("curl -H 'Authorization: Bearer {{API_KEY}}' https://api.example.com")
print(result.stdout) # Bearer [REDACTED_API_KEY]
print(result.exit_code) # 0
```
See [docs/python-api.md](docs/python-api.md) for the full API reference.
---
## Documentation
| [docs/cli.md](docs/cli.md) | All flags, exit codes, vault location |
| [docs/threat-model.md](docs/threat-model.md) | Full security model, oracle attacks, known limitations |
| [docs/architecture.md](docs/architecture.md) | Execution pipeline, crypto, memory hardening |
| [docs/testing.md](docs/testing.md) | Test inventory, known gaps |
| [examples/](examples/) | Runnable CLI and Python examples |
---
## Exit Codes
| 0 | Success |
| 1–125 | Child exit code (passthrough) |
| 124 | Timeout or output limit exceeded |
| 125 | secretsh error (vault, tokenization, shell blocked) |
| 126 | Command not executable |
| 127 | Command not found |
| 128+N | Child killed by signal N |
---
## Development
```bash
cargo test # 233 tests (220 unit + 13 integration)
cargo clippy -- -D warnings # must be zero warnings
cargo fmt --check
# Python bindings
maturin develop --features python
python -m pytest tests/ -v
```
---
## License
[MIT](LICENSE) · [Contributing](CONTRIBUTING.md) · [Security](SECURITY.md)