# Secrets Infisical

> Use when secrets reach an OpenClaw instance through an injection wrapper and something about that is in question — a feature silently off while the config looks right, an instance receiving far fewer keys than its siblings, a plaintext env file inside the state tree, a token or client id sitting in a backup or identity copy, a key that has to be added, rotated, or proven delivered, a machine identity or its project binding, a secret reference in the config, or a restart whose safety depends on every referenced key still resolving.

- Skill: `agents-store/secrets-infisical` (Agent Skill, multi-file: 4 files)
- Install (CLI): `npx skillmds@latest add agents-store/secrets-infisical`
- Raw SKILL.md: https://api.skillmd.com/api/skills/agents-store/secrets-infisical/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: agents-store (https://skillmd.com/u/agents-store)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/agents-store/secrets-infisical

---


# 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 `exec` bypassing the wrapper sees no secrets and returns a false "key missing" verdict — which
  is why `ocexec.py` `hot` mode 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_names` return names and fingerprints, never values.
- `redact.structure_only` renders 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 `exec` without 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>`.

