OpenBao secrets
Code name:
openbao-env
Loads secrets from OpenBao (KV v2) into the environment of every process in the container: VS Code and its extensions, terminals, the lifecycle hooks of other features, AI agents (Claude Code, Remote Control sessions). The container fetches its own secrets, so they no longer depend on which client attached and what it had in its localEnv.
Also included automatically: repairs broken host paths in your git config and restores your SSH commit-signing key on every attach — see
helpers4-common.
Each key of each path becomes an environment variable of the same name: store keys under their final in-container names (GH_TOKEN, GEMINI_API_KEY, …). On a duplicate key, the later path wins (above: orgs/my-org over shared/*).
Then give the container credentials — one of the three setups below.
Options
Section titled “Options”| Option | Type | Default | Description |
|---|---|---|---|
paths | string | "" | Comma-separated KV v2 paths, later ones override earlier ones |
mount | string | kv | Mount path of the KV v2 engine |
address | string | "" | Server address. BAO_ADDR in the container’s environment wins when set |
installCli | boolean | true | Also install the bao CLI (GitHub releases, checksum verified). The loader doesn’t need it |
version | string | latest | bao CLI version when installCli is on, e.g. 2.7.2 |
Runtime environment overrides: BAO_ADDR, BAO_CACERT (custom CA bundle), OPENBAO_PATHS, OPENBAO_MOUNT, OPENBAO_APPROLE_MOUNT (default approle), OPENBAO_ENV_TIMEOUT (seconds for one fetch, default 5), OPENBAO_ENV_DISABLE=1 (never fetch from a shell).
Credentials
Section titled “Credentials”First match wins:
BAO_TOKENin the environment.- Files dropped by the host before the lifecycle hooks:
/dev/shm/openbao/role-id+/dev/shm/openbao/wrapped-secret-id(a response-wrapped AppRolesecret_id). The loader unwraps it (sys/wrapping/unwrap), logs in, and deleteswrapped-secret-idat once: it is single-use. - A token cached by an earlier login, in
/dev/shm/openbao/token, until its TTL runs out — soopenbao-env refreshworks without new credentials meanwhile. BAO_ROLE_ID+BAO_SECRET_IDin the environment (a reusablesecret_id).
a. A host tool with a two-phase devcontainer up
Section titled “a. A host tool with a two-phase devcontainer up”The host mints a short-lived wrapped secret_id and drops it in the container between creating it and running the lifecycle hooks:
Write the files as the remote user (-u), mode 600: the directory must be theirs (if it isn’t, the loader tries sudo chown). /dev/shm is memory only and emptied when the container stops, so the next start needs a new wrapped secret_id.
b. GitHub Codespaces
Section titled “b. GitHub Codespaces”Add BAO_ROLE_ID and BAO_SECRET_ID (and BAO_ADDR if not in the address option) as Codespaces secrets for the repository. If the server is only reachable on a private network (e.g. a tailnet), add the Tailscale feature and an auth key, so the container can reach it. Codespaces secrets that already hold a value (e.g. a GH_TOKEN secret) are kept over the OpenBao ones.
c. A token
Section titled “c. A token”BAO_TOKEN in containerEnv / remoteEnv / a Codespaces secret. Simplest, but the token lives as long as you let it.
⚠️ Remove secret entries from remoteEnv
Section titled “⚠️ Remove secret entries from remoteEnv”remoteEnv is applied on top of the shell environment: when the attaching client doesn’t have the variable, ${localEnv:…} resolves to an empty string, and that empty value replaces the one loaded from OpenBao. Keep secrets out of remoteEnv and containerEnv once they are in OpenBao.
How it works
Section titled “How it works”- A loader in every shell.
/usr/local/share/openbao-env/loader.sh(POSIX sh) is sourced from/etc/profile.d/,/etc/bash.bashrcand zsh’s globalzshrc. VS Code and the devcontainer CLI probe the user’s environment with a login interactive shell (userEnvProbe) before anyonCreate/postCreate/postStarthook of any feature, and hand that environment to the server, extensions, terminals and every lifecycle hook. So e.g.org-workspace(cloning withGH_TOKEN) andgraphite-dev(GRAPHITE_TOKENon start) already see the secrets. - Two pieces.
loader.shis sourced: only a sourced file can export variables into a shell. It just reads the cache and exports. Everything else (login, network, cache write) is in theopenbao-envcommand, a separate process, so nothing it does can leak into your shell. - A memory-only cache. The first shell fetches every path and writes
/dev/shm/openbao/env(directory 700, files 600, owned by the remote user). Later shells only read it: no network./dev/shmis a tmpfs, never on disk, emptied when the container stops. - Bounded and quiet. A fetch takes at most ~5 s, one at a time (
flock; concurrent shells wait and reuse the result). On failure: one line on stderr naming the cause (no credentials, no answer, HTTP 403 on a path, …), never a value; the shell starts normally; new shells don’t retry for 5 minutes unless the host drops new credentials. The loader never sets shell options or exits. - Existing variables win. A variable already set to a non-empty value (Codespaces secret,
containerEnv, your ownexport) is left alone; an empty one counts as unset. Variables the loader set itself are replaced after arefresh. - Secrets never in argv or logs. Tokens and
secret_ids reachcurlthrough 600 files in/dev/shm/openbao, never on the command line (readable by any process in/proc). Status and errors print key names, paths and HTTP statuses only. - Only
curl+jqat runtime. ThebaoCLI is optional. postStartCommandrunsopenbao-env check: a one-line warning if nothing is loaded.
openbao-env
Section titled “openbao-env”After refresh, open a new terminal, or run Developer: Reload Window for VS Code and its extensions to get the new values (in the current shell: . /usr/local/share/openbao-env/loader.sh).
Version History
Section titled “Version History”- v1.0.1: Remove every temp file (curl bodies, token headers) when
openbao-envexits, is interrupted or killed; 1.0.0 left them in/dev/shm/openbao. Leftovers older than 2 minutes are purged. - v1.0.0: Initial release.
