TypeClawTypeClaw
Concepts

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 .env and TypeClaw uses it without rewriting secrets.json. Your file stays the way you wrote it.
  • You can put a token in secrets.json and 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:

  1. process.env[secret.env] if the field was authored with an explicit env binding
  2. process.env[canonicalEnvName] — the platform's standard env var name
  3. secret.value from secrets.json
  4. 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 env binding 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.

On this page