Skip to content

Vault & Secret Broker ​

UGENT Vault is a local-first encrypted secret store and provider egress gateway. API keys live in your OS keychain or an encrypted file vault — never in config files, agent context, or logs.

Overview ​

The Vault solves a simple problem: your API keys should never appear in plaintext. Not in config files, not in environment variables that any process can read, not in agent prompts, not in logs, not in shell history.

Two layers work together:

  1. Secret Store — encrypted storage for API keys and tokens
  2. Secret Broker — an optional local daemon that injects credentials before forwarding API requests

Two Deployment Modes ​

UGENT supports two distinct deployment models. Choose based on your security posture and operational needs.

Mode 1: Native Resolution (Default) ​

Each UGENT instance resolves keys directly from the shared vault (~/.ugent/secrets/) via api_key_ref in its ugent.toml. This is the simplest mode — no broker daemon required. The decrypted key lives in each instance process memory at call time and is never written to disk or logs.

toml
[llm.instances.openai]
type = "openai"
api_key_ref = "@openai_api_key"
default_model = "gpt-5.4-mini"

When to use: Single-machine setups, development workstations, or any case where you want zero extra infrastructure.

Mode 2: Shared Broker (Hardened) ​

One machine-wide broker daemon holds all credentials. Every UGENT instance on that machine reaches the broker instead of resolving keys locally. This is the most credential-isolating mode: the decrypted key lives in exactly one process — the broker — and never enters any agent process, config file, or log.

Enable shared mode in each instance's ugent.toml:

toml
[secrets.broker]
shared = true
transparent_proxy = true          # route LLM traffic through the gateway
broker_fallback = "fail"          # fail-closed if broker unreachable

When shared = true, the broker:

  • Binds to the machine-wide socket ~/.ugent/secret-broker.sock (not a per-workspace path)
  • Reads its provider profiles and lease policy from the canonical ~/.ugent/broker.toml (not any workspace's ugent.toml)
  • Uses the loopback port (127.0.0.1:18443) as a single-broker arbiter: if a second broker starts while one is running, it detects the bound port and exits cleanly

When to use: Production servers, multi-workspace machines, shared development servers, or any environment where credential isolation across instances matters.

Comparison

Native ModeShared Broker Mode
Decrypted key lives inEach instance processBroker daemon only
Extra infrastructureNoneOne daemon process
Config sourceugent.toml per workspace~/.ugent/broker.toml (canonical)
Socket pathPer-workspaceMachine-wide ~/.ugent/secret-broker.sock
Best forDevelopment, single-userProduction, multi-workspace

CLI Commands ​

The ugent secret CLI path boots only the vault — no agent, LLM, tools, MCP, or plugins are initialized. This means it works even when the rest of your workspace config is broken.

All secret values are accepted only via stdin. There is no --value flag — this prevents shell history and process listing leaks. Use clipboard piping to keep secrets out of your terminal history entirely.

--backend keyring|file-vault selects the store for one command; without it, UGENT_SECRETS_BACKEND decides, and the OS keychain is used when that is unset. --kind is one of api-key, token, service-account-json or generic.

Initialize ​

bash
ugent secret init

Creates the metadata database and (for file-vault) the encrypted store directory.

Add a Secret ​

Pipe the value from your clipboard so it never appears in shell history:

bash
pbpaste | ugent secret add @openai_api_key --provider openai --kind api-key --stdin
bash
xclip -selection clipboard -o | ugent secret add @openai_api_key --provider openai --kind api-key --stdin
bash
wl-paste | ugent secret add @openai_api_key --provider openai --kind api-key --stdin
bash
powershell.exe -Command Get-Clipboard | ugent secret add @openai_api_key --provider openai --kind api-key --stdin

Copy your API key to the clipboard first, then run the command. The key travels through the pipe directly into encrypted storage — it never touches disk in plaintext, never appears in ~/.bash_history, and never shows up in ps output.

If add finds the ref already exists, it refuses with an error. Use rotate to overwrite.

List and Inspect ​

bash
ugent secret list
ugent secret inspect @openai_api_key

These commands never print the secret value — only metadata (provider, kind, fingerprint, timestamps, value length).

Rotate ​

bash
# Copy the new key to your clipboard, then:
pbpaste | ugent secret rotate @openai_api_key --stdin

Delete ​

bash
ugent secret delete @openai_api_key

Audit ​

bash
ugent secret audit @openai_api_key

Shows the full audit trail — every add, rotate, lease issuance, and denial — from ~/.ugent/secrets/audit.jsonl.

Change the File-Vault Passphrase ​

Rotate the passphrase whenever it may have been seen. The master key is derived from it, so rekey re-encrypts every entry:

bash
# Stop the broker and the daemon first; both hold the old passphrase.
export NEW_VAULT_PASSPHRASE="$(openssl rand -base64 48 | tr -d '\n')"
ugent secret --backend file-vault rekey --new-passphrase-env NEW_VAULT_PASSPHRASE
unset NEW_VAULT_PASSPHRASE        # once every environment file has the new one

The new passphrase is read from the environment variable you name, never from the command line. It must be at least 32 characters and differ from the current one, and rekey refuses while a secret broker or a running UGENT answers. The old vault is kept as ~/.ugent/secrets/vault.retired-<time> and still opens with the old passphrase. Put the new passphrase into every environment file that sets the old one, start the holders again, and delete the retired vault once everything runs on the new one.

Storage Backends ​

OS Keychain (Default) ​

Uses the OS-native secret service:

  • macOS: Keychain
  • Windows: Credential Manager
  • Linux: Secret Service (GNOME Keyring, KWallet)

No passphrase needed — the OS manages encryption.

Encrypted File Vault ​

For headless servers or environments without an OS keychain:

bash
UGENT_SECRETS_BACKEND=file-vault ugent secret init

Uses XChaCha20-Poly1305 authenticated encryption with an Argon2id-derived master key. Set the passphrase via environment variable:

bash
export UGENT_SECRETS_PASSPHRASE="your-strong-passphrase"

WARNING

For system services (systemd), the OS keychain is usually unavailable. Use file-vault with a passphrase stored in a mode-0600 environment file.

The passphrase stays with the processes that decrypt the vault — the daemon, the broker and a ugent secret command — and nothing they start inherits it:

  • No child process receives it: not the model's shell (sandboxed or not), MCP and LSP servers, skills, plugins, or anything else UGENT spawns, whatever the sandbox settings. A plugin's env_refs and secret_env_refs cannot name it. The broker UGENT starts for itself is the one exception.
  • On Linux, other processes of the same user cannot read it from a holder. Each holder marks itself non-dumpable, which makes its /proc/<pid> files root-only. The cost: holders produce no core dumps, and same-user gdb, lsof and ss -p against them need root.
  • Confined processes see ~/.ugent/secrets empty on Linux, where bubblewrap mounts an empty directory over it. Seatbelt on macOS denies it.
  • Redaction knows it, as a backstop for stored transcripts and logs.

Because the agent's own shell does not have the passphrase, ugent secret ... run from inside the agent does not work with the file vault. Run it from your own terminal.

Referencing Secrets in Config ​

Instead of plaintext values, use @secret_ref handles:

toml
[llm.instances.openai]
type = "openai"
api_key_ref = "@openai_api_key"
default_model = "gpt-5.4-mini"

[llm.instances.anthropic]
type = "anthropic"
api_key_ref = "@anthropic_api_key"
default_model = "claude-opus-5-5"

When api_key_ref is set, it takes priority over api_key or $ENV_VAR. The decrypted value is resolved only at the moment of use and never written to any file.

Linux Daemon Mode ​

For production Linux servers, run the broker as a systemd service so it starts automatically on boot and restarts on failure. Two unit files ship in packaging/systemd/.

System Service (Shared Broker) ​

The standalone secret-broker binary defaults to shared mode: it serves the machine-wide socket and reads config from ~/.ugent/broker.toml.

bash
# Install the binary
install -m0755 target/release/secret-broker /usr/local/bin/secret-broker

# Install the service unit
install -m0644 packaging/systemd/ugent-secret-broker.service /etc/systemd/system/

# Create the environment file (for file-vault passphrase)
cat > /etc/ugent/secret-broker.env << 'EOF'
UGENT_SECRETS_BACKEND=file-vault
UGENT_SECRETS_PASSPHRASE=your-strong-passphrase
EOF
chmod 0600 /etc/ugent/secret-broker.env

# Enable and start
systemctl daemon-reload
systemctl enable --now ugent-secret-broker.service

The service is ordered Before=ugent.service so the credential proxy is up before UGENT makes its first brokered call. It is Type=notify and reports ready only once every listener is bound, so whatever is ordered after it never asks before the socket exists. It runs with hardened systemd protections: NoNewPrivileges, PrivateTmp, ProtectSystem=full, and UMask=0077.

Per-User Service ​

For desktop Linux where the OS keychain (Secret Service) is available, use the user-level unit instead:

bash
install -m0755 target/release/secret-broker ~/.local/bin/secret-broker
install -m0644 packaging/systemd/ugent-secret-broker-user.service ~/.config/systemd/user/
systemctl --user enable --now ugent-secret-broker.service

This runs under your login session so it can reach GNOME Keyring or KWallet.

Verify the Daemon ​

bash
systemctl status ugent-secret-broker.service
# Or check the socket directly:
ls -la ~/.ugent/secret-broker.sock

systemd Credentials ​

Another service on the host can receive a secret from the vault without the passphrase, without opening the vault, and without speaking the broker's API. The shared broker answers systemd LoadCredential= requests, and a root-owned file grants each unit its secrets. This is the way to give a unit its keys: the service gets the secrets granted to it, never the key to all of them.

  1. Turn on the credential socket in ~/.ugent/broker.toml. Only a shared broker serves credentials; the setting is ignored in a workspace's ugent.toml.

    toml
    [broker]
    credential_socket = "/run/ugent-secret-broker/credentials.sock"

    The shipped broker unit has the RuntimeDirectory=ugent-secret-broker this path needs. Restart the broker.

  2. Grant, as root, in /etc/ugent/credentials.toml:

    toml
    [[credential]]
    unit = "my-service.service"     # the unit asking; a template is granted per instance
    id = "api_key"                  # the file name in its $CREDENTIALS_DIRECTORY
    secret_ref = "@my_service_api_key"

    The broker trusts the file only when it and every directory above it are owned by root and writable by no one else. It reads the file on every request, so changing a grant needs no restart.

  3. Check the grants as the service user. This validates the file as the broker will, confirms each named secret exists (metadata only, no passphrase needed), and prints the drop-in each unit needs:

    bash
    ugent secret credentials check
  4. Install the printed drop-in on the consuming unit, then systemctl daemon-reload and restart it. The service reads its value from $CREDENTIALS_DIRECTORY/api_key:

    ini
    # /etc/systemd/system/my-service.service.d/ugent-credentials.conf
    [Unit]
    After=ugent-secret-broker.service
    Wants=ugent-secret-broker.service
    
    [Service]
    LoadCredential=api_key:/run/ugent-secret-broker/credentials.sock
    ExecStartPre=/usr/bin/test -s %d/api_key

A unit that is not granted, and any process not running as root, receives an empty credential, and the ExecStartPre= guard turns that into a failed start rather than a service running without its key. Served and refused credentials are audited (CredentialServed, CredentialDenied) and logged by the broker. A unit reads its credential when it starts, so a rotated value, or a removed grant, takes effect at its next start.

Who to grant

The socket sits in the broker's runtime directory, so a process running as the broker's user could replace it and answer instead. Grant only to units that run as that user, or that receive outbound keys, and never give a unit running as another user a secret it trusts others with, such as an inbound token. Configure an admin named client as well, so that no other caller can use the management API to rotate a secret you grant, and check that the broker's broker client identities resolved log line lists it as loaded: when no configured client loads, the API is closed to every caller, you included.

Starting the Broker Manually ​

Without systemd, start the broker in the foreground:

bash
# Per-workspace broker (native mode companion):
ugent secret broker start

# Shared broker (reads ~/.ugent/broker.toml):
ugent secret broker start --config ~/.ugent/ugent.toml   # with [secrets.broker] shared = true

# Or use the standalone binary directly:
secret-broker

The standalone secret-broker binary exists for process supervisors that want their own PID. It defaults to shared mode.

Provider Egress Gateway ​

The gateway lets any client call a provider API without ever holding the provider key. The client sends the request to the broker (authenticating with the bootstrap token), and the broker injects the real credential before forwarding upstream.

Transports ​

  1. Unix domain socket: ~/.ugent/instances/<workspace_hash>/secret-broker.sock (per-workspace), or ~/.ugent/secret-broker.sock (shared mode). No authentication needed — protected by directory permissions.
  2. Loopback HTTP: 127.0.0.1:18443 (configurable via loopback_bind). Requires a bootstrap bearer token from ~/.ugent/secrets/broker.token (mode 0600) on every request.

Calling the Gateway ​

bash
TOKEN=$(cat ~/.ugent/secrets/broker.token)
curl -N http://127.0.0.1:18443/proxy/openai/v1/chat/completions \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"model": "gpt-5.4-mini", "stream": true, "messages": [{"role": "user", "content": "hello"}]}'

The broker validates the token, injects the real API key, forwards to the upstream provider, and streams the response back. Request and response bodies are never logged.

Built-in Providers ​

11 providers ship out of the box, covering every LLM UGENT itself supports:

ProviderAuth InjectionSecret RefKey Routes
OpenAIAuthorization: Bearer@openai_api_keychat, responses, embeddings, images, audio, files
Anthropicx-api-key header@anthropic_api_keymessages, count_tokens
Googlex-goog-api-key@google_gemini_api_keygenerateContent, streamGenerateContent, embedContent
DeepSeekAuthorization: Bearer@deepseek_api_keychat/completions
DashScope (Alibaba)Authorization: Bearer@dashscope_api_keycompatible-mode chat, embeddings
MoonshotAuthorization: Bearer@moonshot_api_keychat/completions
GLM (Zhipu)Authorization: Bearer@glm_api_keyapi/paas/v4 chat/completions
MiniMaxAuthorization: Bearer@minimax_api_keytext/chatcompletion, chat/completions
OpenRouterAuthorization: Bearer@openrouter_api_keyapi/v1 chat/completions
JinaAuthorization: Bearer@jina_api_keyembeddings, rerank
VoyageAuthorization: Bearer@voyage_api_keyembeddings, rerank

Provider-specific headers are forwarded automatically (e.g. anthropic-version, anthropic-beta for Anthropic; openai-beta, openai-organization for OpenAI). Streaming responses pass through without buffering.

Custom Providers ​

Add any provider in ugent.toml (native mode) or ~/.ugent/broker.toml (shared mode) — no code changes needed:

toml
[secrets.broker.providers.customai]
upstream_base_url = "https://api.customai.example"
secret_ref = "@customai_api_key"
auth = { type = "header", name = "x-api-key" }   # or { type = "bearer" } | { type = "query", name = "key" }
forward_request_headers = ["x-customai-version"]
routes = [
  { method = "POST", path = "/api/v2/generate", streaming = true },
  { method = "GET", path = "/api/v2/jobs/{id}" },
]

An entry whose name matches a built-in replaces it wholesale. Disable a built-in entirely with enabled = false.

Transparent Routing (UGENT's Own Traffic) ​

Enable UGENT to route its own LLM calls through the broker automatically, so provider keys never enter the agent process:

toml
[secrets.broker]
transparent_proxy = true
broker_fallback = "fail"          # "fail" (default) | "native"

At client build time, UGENT resolves the broker socket, probes it, and rewrites the provider base URL to the gateway. The real key lives only in the broker. If the broker is unreachable after connect-retry, broker_fallback decides: fail (fail-closed) or native (fall back to local key resolution).

Transparent routing covers: OpenAI, Anthropic, DashScope, OpenRouter (when using their default base URLs). Instances with custom base URLs (e.g. Azure/Foundry) fall through to native calls.

3rd-Party Application Support ​

The broker is not limited to UGENT. Any application that can speak HTTP can use it as a credential-injecting proxy. Three integration patterns cover virtually any use case.

Pattern 1: Any OpenAI-Compatible SDK ​

Point any SDK's base URL at the broker gateway and use the bootstrap token as the API key. The broker validates the token and swaps in the real credential. Your code never sees the provider key.

python
import openai

client = openai.OpenAI(
    base_url="http://127.0.0.1:18443/proxy/openai/v1",
    api_key=open("/home/user/.ugent/secrets/broker.token").read().strip(),
)

response = client.chat.completions.create(
    model="gpt-5.4-mini",
    messages=[{"role": "user", "content": "hello"}],
)
javascript
import OpenAI from "openai";
import { readFileSync } from "fs";

const token = readFileSync("/home/user/.ugent/secrets/broker.token", "utf-8").trim();

const client = new OpenAI({
  baseURL: "http://127.0.0.1:18443/proxy/openai/v1",
  apiKey: token,
});

const response = await client.chat.completions.create({
  model: "gpt-5.4-mini",
  messages: [{ role: "user", content: "hello" }],
});
bash
TOKEN=$(cat ~/.ugent/secrets/broker.token)
curl http://127.0.0.1:18443/proxy/openai/v1/chat/completions \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"model":"gpt-5.4-mini","messages":[{"role":"user","content":"hi"}]}'

Pattern 2: Environment Projection ​

Run any command with secrets in its environment — never the current shell, never logged. Works for any language, any tool, zero code changes.

bash
ugent secret exec \
  --env SERVICE_TOKENS=@context_engine_service_tokens \
  --env OPENAI_API_KEY=@openai_api_key \
  -- ugent-context-engine run --config ~/.ugent/context-engine/server.toml

The --env VAR=@secret_ref form is the only supported syntax. exec checks every target first — no vault passphrase variable, no LD_* or DYLD_* loader variable, no variable given twice — and resolves every value before anything runs, so a typo or a missing secret never starts the program with half its credentials. On Unix it then replaces itself with the program, which keeps the process id: under systemd the program is the unit's main process, receives the stop signal itself and reports its own exit status, and no wrapper stays behind holding the passphrase. A program that cannot be found exits 127; one that cannot be run exits 126.

Values are resolved once, at start, so a rotated secret reaches the program at its next start. For a systemd unit, prefer systemd credentials: the unit then never needs the vault passphrase in its environment file.

Pattern 3: Sibling Service Configuration ​

Point a sibling service's embedder or API client at the broker gateway in its own config file. The provider key never enters that service's process.

Context Engine:

toml
# ~/.ugent/context-engine/server.toml
[embedder]
openai_compatible_endpoint = "http://127.0.0.1:18443/proxy/openai/v1"
openai_compatible_api_key = "$BROKER_TOKEN"

Any application reading from environment:

bash
# Launch your app with secrets projected from the vault:
ugent secret exec \
  --env OPENAI_API_KEY=@openai_api_key \
  --env ANTHROPIC_API_KEY=@anthropic_api_key \
  --env GOOGLE_API_KEY=@google_gemini_api_key \
  -- your-application --config /etc/yourapp/config.yaml

Pattern 4: HTTP API (Any Language) ​

The management and lease API speaks plain HTTP/JSON over the Unix socket or loopback HTTP. Any language that can make HTTP requests can manage secrets programmatically.

text
GET    /v1/secrets                       -> list metadata (never values)
POST   /v1/secrets                       -> create (value in body, stored encrypted)
GET    /v1/secrets/{ref}                 -> metadata for one ref
DELETE /v1/secrets/{ref}                 -> delete
POST   /v1/secrets/{ref}/rotate          -> overwrite value
GET    /v1/secrets/{ref}/audit           -> audit trail
POST   /v1/leases                        -> request a lease (deny-by-default)

All responses contain metadata only. Decrypted secret values are never returned through any endpoint.

Grants and Leases ​

Access is deny by default. Configure grants in ~/.ugent/secrets.grants.toml:

toml
# UGENT core resolving the OpenAI key natively for chat and embeddings.
[[grants]]
secret_ref = "@openai_api_key"
consumer = "ugent-core"
purposes = ["llm.chat", "llm.embeddings"]
delivery = ["native"]
allowed_hosts = ["api.openai.com"]
ttl_secs = 300

# The broker's provider egress gateway forwarding /proxy/openai/... requests.
[[grants]]
secret_ref = "@openai_api_key"
consumer = "secret-broker.proxy"
purposes = ["llm.chat", "llm.embeddings"]
delivery = ["http_header_inject"]
allowed_hosts = ["api.openai.com"]
ttl_secs = 300

A missing file denies everything. Every issue and denial is audited to ~/.ugent/secrets/audit.jsonl. Edits to the grants file take effect on the next lease request — no broker restart needed.

The gateway reads its own grants (consumer = "secret-broker.proxy") only when enforce_gateway_grants = true under [secrets.policy] ([policy] in broker.toml); it defaults to false, and the gateway then resolves each provider's secret from its profile. Turn it on after the grants exist, or the gateway forwards nothing.

Fields:

  • secret_ref — vault handle (e.g. @openai_api_key)
  • consumer — identity of the requester (e.g. ugent-core, secret-broker.proxy, or your own name). The caller chooses this string, so it separates components, not callers
  • client — optional named client id, matched against the caller the broker authenticated from its token. A grant that sets it refuses the shared bootstrap token
  • purposes — optional allowlist of free-form purpose strings
  • delivery — optional allowlist: native, http_header_inject, query_param_inject, child_process_env, temp_file, human_reveal
  • allowed_hosts — optional upstream host allowlist
  • ttl_secs — per-grant TTL cap (clamped to policy max, default 3600)

Shared Broker Configuration ​

In shared mode, the broker reads from ~/.ugent/broker.toml instead of any workspace's ugent.toml. The schema mirrors [secrets.broker] / [secrets.policy] minus the secrets. prefix:

toml
# ~/.ugent/broker.toml -- canonical config for the shared broker.

[broker]
loopback_bind = "127.0.0.1:18443"
allow_loopback_http = true

# Optional: override or add provider profiles.
[broker.providers.foundry-claude]
upstream_base_url = "https://YOUR-RESOURCE.services.ai.azure.com"
secret_ref = "@foundry_claude_api_key"
auth = { type = "header", name = "api-key" }
forward_request_headers = ["anthropic-version", "anthropic-beta"]
routes = [{ method = "POST", path = "/anthropic/v1/messages", streaming = true }]

[policy]
lease_max_ttl_secs = 3600
allow_human_reveal = false
require_approval_for_new_grant = true

An absent broker.toml means embedded provider defaults plus default policy — the broker works out of the box. A broker.toml that contains a [secrets...] table is refused at startup: write [broker], [[broker.clients]] and [policy] here, without the secrets. prefix ugent.toml uses. credential_socket under [broker] turns on systemd credentials.

Trust Domain

All UGENT instances of the same OS user authenticate to the shared broker with the same bootstrap token and can read the same vault, forming a single trust domain. The grants system gates which secrets may be leased; on its own it cannot tell which instance is asking.

Named clients give the broker that distinction. Each has its own bearer token, kept in the vault:

toml
# ~/.ugent/broker.toml
[[broker.clients]]
id = "finance"
token_ref = "@broker_client_finance"

[[broker.clients]]
id = "ops"
token_ref = "@broker_client_ops"
admin = true                      # may use the secret management API

An instance presents its client's token with client_token_ref = "@broker_client_finance" under [secrets.broker] in its ugent.toml. A grant's client and a provider profile's allowed_clients are matched against the client the broker authenticated, so a caller cannot claim another's. Once any client is configured, the secret management API (/v1/secrets*) is closed to everyone but the admin clients that loaded; with none configured, nothing changes and the bootstrap token keeps working as before. The lease routes stay open to every authenticated caller, decided by grants. A client whose token_ref does not resolve, whose token is the bootstrap token, or that shares a token with another client (every client sharing it) is dropped at startup. If every client is dropped, the API stays closed to every caller, and the broker logs no configured broker client resolved; the secret management API is closed to every caller until the broker restarts with one that does. Clients are loaded when the broker starts, so after storing a missing token, restart the broker. Its broker client identities resolved log line names the clients it loaded and those it dropped.

Plugin Secret Injection ​

Map environment variables to secret handles in your plugin configuration. The supervisor resolves and injects values only into the plugin child process at spawn time:

toml
[[plugins]]
id = "my-plugin"
secret_env_refs = { "MY_API_TOKEN" = "@my_service_token" }

A plugin is never given the vault passphrase: an env_refs or secret_env_refs entry that names UGENT_SECRETS_PASSPHRASE is dropped with a warning.

Threat Model ​

Protected against:

  • Provider keys in config files, agent context, or logs
  • Prompt injection revealing keys (the LLM never sees decrypted values)
  • Other local processes calling the loopback API without the bootstrap token
  • Shell history and process listing leaks (secrets enter only via stdin/pipe)
  • Upstream credential redirect attacks (the broker never follows redirects)
  • Child processes inheriting the vault passphrase — the model's shell, MCP and LSP servers, skills, plugins
  • On Linux, same-user processes reading the passphrase from a running holder's environment

Not protected against:

  • Root access
  • Same-user malware that can read the bootstrap token file or OS keychain
  • Kernel-level attacks
  • Same-user processes reading the values a secret exec program or a plugin worker holds — those processes are not hardened like the holders
  • A same-user process that opens a holder's environment in the moment between its start and its hardening
  • On Linux, a confined process reading other credential files such as ~/.ssh: bubblewrap enforces no read denial except over the vault directory
  • On macOS, a same-user process reading a holder's launch environment (ps -E): there is no equivalent of Linux's non-dumpable flag, so prefer the OS keychain there

Separate service users are the only complete answer to same-user access.

Released under the Private Beta License.