Secrets policy
Why env-wins, why secrets.json is never auto-mutated, and what's encrypted at rest
Credentials in TypeClaw live in two gitignored files: .env (plain KEY=value lines, injected via --env-file at container start) and secrets.json (structured store managed by TypeClaw). The interesting question isn't where they live — it's the policy that governs them.
Env-wins, file-never-auto-mutated
When a credential's canonical env var (FIREWORKS_API_KEY, SLACK_BOT_TOKEN, etc.) is set, that value is used at runtime. TypeClaw will never auto-mutate secrets.json to capture an env value, and it will never auto-promote a secrets.json value into the environment of a subsequent process.
This policy buys three things:
- You can put a token in
.envand TypeClaw uses it without rewritingsecrets.json. Your file stays the way you wrote it. - You can put a token in
secrets.jsonand TypeClaw uses it when the env var is unset. CI pipelines override per-agent file values with CI-managed env vars without leaking the CI value back into the file. - The on-disk state is what you authored. There's no surprise diff after a single
typeclaw start.
The resolution order, applied per secret-bearing field:
process.env[secret.env]if the field was authored with an explicitenvbindingprocess.env[canonicalEnvName]— the platform's standard env var namesecret.valuefromsecrets.json- Otherwise the field is missing, and the consumer sees an absent credential
OAuth credentials are an exception to env-wins — they're stateful (refresh tokens get rotated by the upstream library), so they always live in secrets.json and are read from there.
The bridge
The runtime uses pi-coding-agent's AuthStorage under the hood, which has a flat Record<provider, AuthCredential> shape. TypeClaw's envelope is structured (providers.*, channels.*). When AuthStorage writes (e.g. OAuth refresh rotates a token), TypeClaw diffs the proposed slice against the on-disk envelope and:
- Provider unchanged — preserves the on-disk Secret bytes verbatim. Critical: this is what prevents OAuth-refresh writes from accidentally persisting env-resolved api-key values into the file.
- Provider value changed — rewraps as Secret, preserving any prior
envbinding the user authored. - Provider removed — actually removes (doesn't resurrect).
The bridge is idempotent and the idempotency is load-bearing — without it, the env-wins guarantee falls apart on the first OAuth refresh.
Encryption at rest, for one specific case
Most credentials don't need encryption at rest — the file is gitignored, the directory is mode 0700 on POSIX, and if an attacker owns your home directory you have bigger problems.
There's one exception. KakaoTalk has a hard ~7-day token TTL where both access_token and refresh_token expire on the same cycle. No inactivity keepalive solves it. The only way to keep the channel alive unattended is for TypeClaw to re-login with email and password every ~7 days, using the saved device_uuid to skip the phone-passcode confirmation step.
So the per-account record carries email (plain) and encryptedPassword (AES-256-GCM at rest). The 32-byte symmetric key lives at ~/.typeclaw/keys/<containerName>.key, file mode 0600, dir 0700, outside the agent folder. The typical leak scope (git add accident, agent-folder backup, shared mount) doesn't capture both ciphertext and key.
This is defense-in-depth, not a security boundary. It does NOT protect against full host compromise (the OAuth tokens stored next to the encrypted blob already grant equivalent capability), nor against whole-home backups that capture both ~/.typeclaw/ and the agent folder. If TYPECLAW_HOME is overridden to a path inside the agent folder, the separation collapses.
For the file shape and the legacy upgrade paths, see /reference/secrets-json.