Skip to content

Secret handling

Secrets are never written to disk where an agent could read them. They are fetched at the moment of use, held in memory or in a short-lived private file, and cleaned up when the process exits.

This is the part of the system the 2026-05-23 incident was about: an agent saved a credential to the wrong place, and three written rules failed to stop it (see the overview). The fix wasn’t a fourth written rule. It was a design where the convenient way to get a secret and the safe way are the same way — plus checks that make every other way fail.

All secrets live in a vault on a separate machine (prontera). Nothing on the main host stores them. When a service needs a credential, it presents a token to a small broker service, and the broker hands back the secret — over the network, at runtime, into memory. This is HARD RULE 10, invariant INV-024, and decision record ADR 0105 all describing the same arrangement from three angles (the rule, the test, and the reasoning).

main host prontera
───────── ────────
service needs a secret
│ presents its token
broker client ───────────────────▶ vault / broker
▲ │
└────── secret, in memory ──────────┘
on disk on the main host: the token only — short-lived,
mode-700 directory, deleted when the process exits

The one thing that does touch disk is the token itself — and only barely. Two scripts manage its whole life:

  • scripts/with-ephemeral-runtime-token.sh — fetches the token from wherever it can be found safely (an operator-supplied env file, systemd’s credentials directory, an already-running container, or typed in on stdin), writes it into a freshly created directory only the owner can read (mode 700), runs the wrapped command, and deletes the directory on exit — including on crashes, via a shell exit trap.
  • scripts/compose-up.sh — the only correct way to start the stack. Because the token file is temporary, a stopped container’s bind-mount can point at a path that no longer exists; plain docker restart then fails. This script detects those stale containers, removes them, and recreates them with a fresh token via the wrapper above.

Even GitHub access goes through the broker

Section titled “Even GitHub access goes through the broker”

scripts/gh-as-soho.sh is the required way to open a pull request. It resolves a GitHub token through the same broker (trying the host path first, then falling back to asking a running container to fetch it), checks the token actually belongs to the expected bot account by calling GitHub’s /user endpoint, and only then runs gh. If any step fails it stops with an error — it never silently falls back to whatever credentials a bare gh would find lying around. PRs therefore always carry the right identity, and no long-lived GitHub token needs to exist on the host.

The blockers — making the wrong way fail

Section titled “The blockers — making the wrong way fail”

A safe path only helps if the unsafe paths are closed. Three guards close them:

Guard What it blocks
check-agent-vault-no-local-secret-materialization.py Code that would write broker secrets to local files or export them into process environments. It greps the tree for the known dangerous patterns (the old credential-dir variable, the materialization helper, raw-secret-env flags) and fails CI on any hit.
check-secret-startup-denylist.py Startup scripts quietly going back to the legacy secret managers (Infisical, Doppler) that the broker replaced.
load-agent-vault-env.sh The legacy loader itself still exists as a file — but it has been gutted: it prints an error and exits with code 78 (“config error”) no matter how it’s called. Anything still depending on it breaks loudly instead of working unsafely.

And one audit checks reality rather than code: check-process-secret-exposure.py inspects the environment and command line of live processes and containers for secret-shaped values. It reports the location of a leak, never the value. The difference matters: the other guards prove the code can’t leak; this one proves the running system currently isn’t.