secretsh 0.1.2

Secure subprocess secret injection for AI agents
Documentation

secretsh

Secure subprocess secret injection for AI agents.

CI Crates.io PyPI License: MIT

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:

  1. The LLM knows the secret and can be tricked into leaking it.
  2. Shell history records it in ~/.bash_history.
  3. 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)

brew tap lthoangg/tap
brew install secretsh

From source

cargo install secretsh

Pre-built binaries

Download from GitHub Releases for:

  • x86_64-apple-darwin
  • aarch64-apple-darwin
  • x86_64-unknown-linux-gnu
  • aarch64-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.

read -rs SECRETSH_KEY && export SECRETSH_KEY

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

secretsh init

Easiest: import from an existing .env file:

secretsh import-env -f .env

Or add secrets one at a time (type the value, then press Ctrl+D on a new line to finish):

secretsh set API_PASS
secretsh set API_USER

3. Run commands with {{placeholders}}

secretsh run -- "curl -u {{API_USER}}:{{API_PASS}} https://httpbin.org/basic-auth/admin/hunter2"

The output is always scrubbed -- any vault secret (raw, base64, URL-encoded, or hex) is replaced with [REDACTED_<KEY>].

4. See what's stored

secretsh list

Values are never displayed.


Commands

Command Description
secretsh init Create a new encrypted vault
secretsh set <KEY> Store a secret (reads value from stdin)
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>/cmdline inspection (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

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
secretsh init --vault ~/.secretsh/work.vault --master-key-env WORK_KEY
secretsh import-env -f .env.work --vault ~/.secretsh/work.vault --master-key-env WORK_KEY

# Personal vault
secretsh init --vault ~/.secretsh/personal.vault --master-key-env PERSONAL_KEY
secretsh import-env -f .env.personal --vault ~/.secretsh/personal.vault --master-key-env PERSONAL_KEY

# Run from either
secretsh run --vault ~/.secretsh/work.vault --master-key-env WORK_KEY -- \
    "curl -H 'Token: {{API_TOKEN}}' https://api.example.com"

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.

import secretsh

with secretsh.Vault(master_key_env="SECRETSH_KEY") as vault:
    vault.set("API_KEY", bytearray(b"sk-live-abc123"))  # bytearray is zeroed after copy
    result = vault.run("curl -H 'Authorization: Bearer {{API_KEY}}' https://api.example.com")
    print(result.stdout)     # -> "... Authorization: Bearer [REDACTED_API_KEY] ..."
    print(result.exit_code)  # -> 0

Install from source

uv venv .venv && source .venv/bin/activate
uv sync --group dev                       # install dev deps (pytest, pytest-cov)
maturin develop --features python          # build + install into venv
python -m pytest tests/ -v                 # run tests

Requires Python 3.10+.


Development

# Build
cargo build

# Run Rust tests (188 unit tests)
cargo test

# Lint
cargo clippy -- -D warnings

# Format
cargo fmt

# Build release
cargo build --release

# Python bindings
maturin develop --features python
python -m pytest tests/ -v

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
read -rs SECRETSH_KEY && export SECRETSH_KEY
bash examples/basic_cli.sh
python examples/basic_python.py

License

MIT


Contributing

See CONTRIBUTING.md for guidelines.

Security

To report a vulnerability, see SECURITY.md.