Secret injection and the store
Secrets are not in the config. The config holds references; the wrapper resolves them at start. Everything here follows from that, including the failure that looks like a broken feature and is really a missing key name.
Mechanics
The gateway command is wrapped: the wrapper mints a short-lived machine-identity token, exchanges it for the project's secret set, and execs the real process with those values in its environment only. Nothing lands on disk, so:
- Reading the config tells you what is required. Only running the wrapper tells you what is delivered.
- An
execbypassing the wrapper sees no secrets and returns a false "key missing" verdict — which is whyocexec.pyhotmode goes through the wrapper by default. - The identity is per instance, one file, mode 0600, owned by root
(
fleet.secrets.identity-file-mode). A wrong project or environment binding shows up as short delivery, never as an error. - Store values are not encrypted at rest — a recorded decision
(
fleet.secrets.shared-store-plaintext), not something to fix here.
Never a value
Presence, fingerprint (fp:0000aaaa — an obviously synthetic stub; a real one is eight hex digits of a
digest), size bucket, key class, expiry date and full key name answer every operational question. The
value answers none of them.
redact.read_env_file/env_namesreturn names and fingerprints, never values.redact.structure_onlyrenders a credential document by shape — safe fields verbatim, identifying fields as fingerprints. That is also the token-sink detector: one account fingerprint across several credential directories.- Files that are never read whole: any env file, the auth-secret tree, auth profiles, credential directories, identity files, logs. Use a filter that structurally cannot return a value.
- When a provider answers unauthorized the value is useless as evidence; presence, fingerprint parity with what is injected, and expiry are the whole answer.
The delivery audit
Two sets, compared by name:
- required — every secret reference in the effective config, plus the names the compose file passes through.
- delivered — the key names the wrapper actually produces inside the container. Names only; count the characters of a value if you need "non-empty", never print it.
required − delivered non-empty is critical: the feature reading that key is dead and nothing
logs an error (fleet.secrets.delivery-short). A count far below the siblings' is a wrong project or
environment binding (fleet.secrets.wrong-project); a set of almost nothing is a broken identity
(fleet.secrets.identity-broken). Compare against secrets.expected_key_count and against the other
instances — a fleet of clones makes the outlier obvious.
Restart precondition
compose restart is R2, but on this deployment it carries an R3 precondition: a running instance
holds its secrets in memory; a restart re-resolves them. Restarting while a reference no longer
resolves converts a working instance into a dead one, and the cause is invisible afterwards.
Run the delivery audit before any restart, show the resolved-name list in PRECHECK, and refuse
while required − delivered is non-empty.
Writing a secret
Three leak paths, all closed at once, plus the one this plugin refuses outright.
| Path | Why it leaks | Rule |
|---|---|---|
| command output | the write command prints the full KEY=VALUE table even in quiet mode |
always discard stdout and stderr of the write |
| process table | a value in argv is visible to every user on the box | pass by stdin or environment, never as an argument |
| history and transcript | anything printed is on disk and in the model's context, permanently | the plugin does not accept a key in chat |
The plugin prints the operator a line that reads the value with echo disabled, pipes it to the write,
and then verifies by fingerprint alone. Overwriting a key whose fingerprint differs is red line
secret-overwrite-different-fingerprint — a rotation, not an edit. Writes get zero retries
(gate.RETRY_RULES["secret-write"]): a repeat risks a partial write and one more appearance in a
process table.
Identity login: capture stdout only
The store CLI writes an update-check banner to stderr. A login that merges the streams glues that banner onto the token, every later call fails, and the error blames the credentials. Capture stdout alone — a token that "works by hand and fails in the script" is almost always this.
Plaintext env files: parity decides, not existence
A plaintext env file inside the state tree is either redundant (every key matches the store by fingerprint) or load-bearing (a key the store does not deliver). They look identical.
Deleting a load-bearing file takes the instance down at the next restart, silently. Classify by
fingerprint parity first, close the gap for load-bearing keys, then neutralise the file by moving
it to a quarantine path outside every container volume — never rm. Red line
delete-plaintext-env, finding fleet.secrets.plaintext-env; six-step procedure with its stop
points and canary restart in references/playbook.md.
A leaked value is not fixed by deleting the file
If a token, client id or key ever sat in a file that left the box — a backup, a copy, a paste — the
value is compromised, and removing the file removes the evidence rather than the exposure
(fleet.secrets.leaked-in-backup, critical, R4). Rotation is mandatory, not advisable. Do not let
the easy action stand in for the undone hard one: "removed the file" while the old value still
authenticates closes the ticket without closing the exposure.
Common mistakes
- Reading the config and calling it delivery.
- Running
execwithout the wrapper and concluding the key is missing. - Restarting before the delivery precheck.
- Printing a value "just once" to compare it — compare fingerprints.
- Deleting a plaintext env file because the store also has those keys, without checking every key.
- Merging stderr into the login capture.
- Treating a leak as filed once the file is gone.
Procedures: references/playbook.md. Symptom to cause to check: references/troubleshooting.md.
Repairs go through /openclaw-ops:repair <selector> --issue <finding-id>.