v0.1.0 · AES-256-GCM · X25519 sealed responses · machin/MFL

veil

A secret manager where agents never see raw secrets —
not in stdout, not in stderr, not in logs, not in MCP responses.
Output scrubbing is mandatory, not optional.

The fix Phoenix needed. Phoenix Secrets had AES-256-GCM, X25519 sealed responses, ACLs, attestation — but its exec command used syscall.Exec with no output interception. A child process printing key=$SECRET leaked the raw value straight to the model. Veil is a clean-room reimplementation in machin/MFL that fixes all three critical flaws: mandatory scrubbing, no raw output by default, and sealed mode as the only mode.

How it works

Two paths for agents. Neither exposes the raw secret.

veil exec (env injection + scrubbing)

Resolve secrets internally, inject as env vars, run the child, capture stdout + stderr, scrub every known secret to ***, emit only the scrubbed output. The agent runs the command but never sees the values.

veil resolve (sealed token)

Returns an opaque VEIL_SEALED:... blob encrypted with X25519 + AES-256-GCM, bound to the secret path and ref. The agent gets a token it can pass around but cannot decrypt. Only the holder of the seal private key can open it.

# Agent wants to run a command that needs a secret:
veil exec --env API_KEY=veil://myapp/api-key -- python app.py

# Agent needs a reference to a secret (not the value):
veil resolve veil://myapp/api-key
→ {"sealed":"VEIL_SEALED:eyJ...","ok":"true"}

# Human retrieves a secret to a file (never stdout by default):
veil get myapp/api-key -o /tmp/secret

The 3 fixes vs Phoenix

Phoenix flawVeil fix
exec uses syscall.Exec — child stdout/stderr leaks secrets directly to the model → exec captures output via machin's exec(), scrubs both streams, emits only ***-redacted text
resolve and get print raw secrets to stdout → get refuses stdout by default (requires -o <file> or --raw); resolve returns opaque sealed tokens
MCP plaintext is the default unless sealed mode is explicitly configured → Sealed mode is the only mode — resolve always returns VEIL_SEALED:... tokens

Usage

veil init                                    # Initialize ~/.veil (generates admin token)
veil set myapp/api-key -v "sk-xxx"           # Store a secret (warns: use --stdin)
veil set myapp/api-key --stdin               # Store from stdin (no argv leak)
veil get myapp/api-key -o /tmp/secret --token <admin>  # Retrieve to file (human only)
veil list                                    # List secret paths
veil delete myapp/api-key --token <admin>    # Delete a secret (human only)
veil exec --env KEY=veil://myapp/api-key -- cmd   # Run cmd with env + scrubbing (agent)
veil resolve veil://myapp/api-key            # Get sealed token (agent)
veil agent create ci-bot <token> --token <admin>  # Create an agent (human only)
veil backup push --remote http://lume:8788   # Backup to lume
veil guide                                   # Embedded mental model
veil help-json                               # Machine-readable catalog

Trust model

Both humans and agents can use veil, but only humans can see raw secrets.

CommandAgentHumanWhy
setyesyesAgent already has the value -- no new leak. Human can rotate later.
execyesyesScrubbed output -- agent never sees values.
resolveyesyesSealed token -- agent can't decrypt.
listyesyesPaths only -- no values.
getnoyesRaw value retrieval -- requires admin token.
deletenoyesDestructive -- requires admin token.
agentnoyesAdmin operations -- requires admin token.

The admin token is generated during veil init and shown once. Humans store it in a password manager or export VEIL_ADMIN_TOKEN. Agents don't have it, so admin-gated commands are human-only in practice.

Cryptography

AES-256-GCM envelope encryption

Master KEK wraps per-secret DEKs. Compromising one DEK exposes only one secret. Rotating the KEK only requires re-wrapping DEKs, not re-encrypting all secrets.

X25519 sealed responses

Sealed tokens are encrypted with X25519 ECDH + HKDF-SHA256 derived keys. Path and ref are bound into the envelope to prevent relabeling attacks.

Output scrubbing

Every secret value is replaced with *** in child stdout and stderr before emitting. Catches exact, base64, hex, URL-encoded, reversed, space-separated, and partial substring (>=8 chars) variants. The agent sees the command output but not the secrets.

Deny-by-default ACL

Path-based permissions with glob matching. Token hashing (SHA-256). Per-agent access control. admin implicitly grants all actions.

No custom cryptography. All primitives are machin builtins: aes_gcm_encrypt/decrypt, x25519_pub/shared, hkdf_sha256, rand_bytes, sha256_bytes.

Known limitations

Veil prevents secrets from entering the LLM context window via command output. It does not protect against a malicious same-user process that reads files directly.

LimitationWhyMitigation
master.key/seal.key readable by same-user processesFiles are 0600 but same user can read themDedicated user account; container isolation
acl.json token hashes readableSHA-256 hashes, not raw tokensUse long random tokens; rotate if compromised
Child /proc/PID/environ readableInherent to env injectionUse veil resolve (sealed tokens) instead
Arbitrary transforms (rot13, XOR) bypass scrubberInfinite possible encodingsRestrict child commands to trusted code
set -v puts secret in argvVisible in ps for process lifetimeUse set --stdin instead

The threat model is well-behaved agents using the CLI as intended, not malicious same-user processes. A malicious agent could always cat ~/.veil/master.key and write a decryption script. Veil's job is to ensure that normal agent workflows -- exec, resolve, list, set -- never expose raw secrets to the model.

Install

# from a release (no build needed)
curl -sL https://github.com/javimosch/veil/releases/latest/download/veil-linux-amd64 -o veil
chmod +x veil
./veil install     # copies to ~/.local/bin

# or build from source
git clone https://github.com/javimosch/veil.git
cd veil
./build.sh          # machin encode + machin build → ./veil
./veil guide        # read the mental model
./veil help-json    # machine-readable command catalog

Requires machin installed to build from source. Single ~90 KB binary, no runtime, no dependencies. Self-update: veil update.

Agent-first CLI specs

Veil implements four of the six agent-first CLI specs:

SpecStatusImplementation
cli-output-specyesJSON stdout, typed errors, semantic exit codes, help-json
cli-guide-specyesveil guide — embedded mental model
cli-update-specyesveil update, install, uninstall
cli-feedback-specyesveil feedback — relay-only dual-write
cli-telemetry-specnoDeliberately skipped — a secret manager making network calls is a bad look
cli-daemon-specn/aVeil is CLI-only, no daemon