May 28, 2026 — Matteo Merola

Secrets Management for AI Agents: Why .env Files Aren't Enough

How a two-tier encrypted vault keeps credentials out of the model context — and why the standard .env approach is a security liability for autonomous agents.

For a normal web application, a .env file is a perfectly reasonable way to manage secrets. The application reads STRIPE_KEY from the environment, uses it to make API calls, and no human ever sees the value in the running process. The trust boundary is the process itself.

For an AI agent, that trust model breaks down completely. The agent has tool access — it can read files, run commands, and inspect the environment. A model that can execute cat .env or printenv has access to every secret on the machine. Even without malicious intent, a secret that enters the conversation history can end up persisted in memory, included in a log, or surfaced to a different user in a group chat. The agent doesn't need to be adversarial for this to be a problem — it just needs to be helpful at the wrong moment.

The attack surface

Consider the ways secrets leak in an agent context:

Two-tier vault architecture

humux uses an encrypted vault with two tiers, each with a different trust level and unlock mechanism:

vault architecture
┌─────────────────────────────────────────────┐
│  Infrastructure Vault                        │
│  Sealed with: machine key (auto at boot)     │
│  Contains: LLM API keys, Telegram bot tokens │
│  Access: system only — never exposed to LLM  │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│  Agent Vault                                 │
│  Sealed with: admin password (manual unlock) │
│  Contains: website logins, payment keys,     │
│            email app passwords, OAuth tokens  │
│  Access: per-agent scoping — resolved at     │
│          execution time, never in LLM context │
└─────────────────────────────────────────────┘

The infrastructure vault is sealed with a machine-derived key and unlocked automatically at boot. It holds the secrets the system itself needs to function — the API key that lets the agent call an LLM provider, the Telegram bot token. These are infrastructure concerns, not agent concerns. The model never sees them because they're consumed by the Python process directly, not passed through the tool-use loop.

The agent vault uses envelope encryption with the admin password. It holds credentials the agent uses on behalf of the user — an email app password, a website login, a payment API key. These require a human to unlock (entering the admin password via the web UI), and they're scoped per-agent: the coding agent can't access the email agent's IMAP password.

Reference syntax: the model sees a placeholder, not a value

The key design decision is that secrets are referenced by name, and the resolution happens at the execution boundary — outside the model's context window. There are two reference syntaxes:

config.yml
# In config files: resolved at startup
agents:
  - slug: atlas
    telegram_token: ${vault:ATLAS_BOT_TOKEN}
    email_accounts:
      - address: [email protected]
        password: ${vault:WORK_EMAIL_PASSWORD}
skills/email.md
# In skill commands: resolved at execution time
## Send email
himalaya send \
  --account {{secret:EMAIL_ACCOUNT}} \
  --to "{recipient}" \
  --subject "{subject}" \
  --body "{body}"

# The model sees "{{secret:EMAIL_ACCOUNT}}" in the skill file.
# When it calls the tool, the executor substitutes the real value
# before passing the command to the shell.
# The model never sees the resolved password.

The ${vault:NAME} syntax is for config files — resolved once at startup by the config loader. The {{secret:NAME}} syntax is for skill commands — resolved every time the command executes, by the command executor. In both cases, the model sees the placeholder string, never the resolved value. The skill file that the model reads says {{secret:GMAIL_APP_PASSWORD}}. The command that actually runs has the real password. The model is never in the loop.

Per-agent secret scoping

If you have three agents — a work assistant, a coding agent, and a personal assistant — each should only access the secrets it needs. The work assistant needs the email password. The coding agent needs the GitHub token. Neither needs the other's credentials.

humux enforces this through the same inherit-never-widen principle used for tools and skills. Each agent has a list of secrets it can access. A subagent spawned by the work assistant can access a subset of the work assistant's secrets, but never the coding agent's GitHub token. The scoping is enforced at the vault lookup level — if an agent requests a secret it doesn't have access to, the lookup returns an error, not an empty string.

Bitwarden import

Nobody wants to manually enter fifty credentials into a new system. humux supports importing from a Bitwarden JSON export. The import process reads the export file, encrypts each credential into the agent vault, assigns it a name, and discards the export. The plaintext export exists only in memory during the import and is never written to disk by humux.

admin UI — vault import
Import source: Bitwarden (JSON export)
Imported: 47 credentials
  → 12 assigned to agent "atlas" (email, calendar)
  → 8 assigned to agent "forge" (GitHub, npm, Docker)
  → 5 assigned to agent "sage" (personal accounts)
  → 22 unassigned (available for manual assignment)

Export file consumed. Not stored on disk.

What this prevents

The vault system is designed to prevent a specific class of failures:

None of this is exotic cryptography. It's envelope encryption with AES-256, a machine key derived from the host, and an admin password for the sensitive tier. The innovation isn't the crypto — it's applying standard secret management patterns to the specific trust model of AI agents, where the process itself is not fully trusted because it takes instructions from an external model.

humux ships with an encrypted two-tier vault that keeps credentials out of the model context by design. Try it →