ruvnet/ruflo · error · Error
Invalid CLAUDE_FLOW_ENCRYPTION_KEY: expected 32-byte key as
Error message
Invalid CLAUDE_FLOW_ENCRYPTION_KEY: expected 32-byte key as 64-char hex or 44-char base64
What it means
Thrown by decodeKey() when CLAUDE_FLOW_ENCRYPTION_KEY does not match the accepted encodings: exactly 64 hex chars [0-9a-fA-F], or base64 of 43 chars plus optional '=' padding (44 total) that decodes to exactly 32 bytes. The vault is strict by design — a truncated or wrongly-encoded key would silently weaken AES-256-GCM, so anything else is rejected loudly. Note base64url characters '-' and '_' are NOT accepted, only '+' and '/'.
Source
Thrown at v3/@claude-flow/cli/src/encryption/vault.ts:108
}
/**
* Decode a key string. Exposed for testing and for the future passphrase
* resolver, which will scrypt-derive a Buffer and hand it back through here
* to share the same length-check.
*/
export function decodeKey(raw: string): Buffer {
const trimmed = raw.trim();
// Hex first — strict 64 chars [0-9a-fA-F]
if (/^[0-9a-fA-F]{64}$/.test(trimmed)) {
return Buffer.from(trimmed, 'hex');
}
// Base64 — accept padded 44-char or unpadded 43-char forms
if (/^[A-Za-z0-9+/]{43}=?$/.test(trimmed)) {
const buf = Buffer.from(trimmed, 'base64');
if (buf.length === KEY_LEN) return buf;
}
throw new Error(
`Invalid ${ENV_KEY_VAR}: expected 32-byte key as 64-char hex or 44-char base64`,
);
}
/**
* Encrypt a plaintext Buffer with AES-256-GCM. Returns the wire-format
* blob: magic(4) || iv(12) || ciphertext(N) || tag(16).
*
* The IV is freshly randomized per call. Reusing a (key, iv) pair under
* GCM is catastrophic — every call MUST produce a different IV. Node's
* randomBytes is csprng-backed so this is automatic; the function takes
* no IV input deliberately.
*/
export function encryptBuffer(plaintext: Buffer, key: Buffer): Buffer {
if (!Buffer.isBuffer(plaintext)) {
throw new TypeError('encryptBuffer: plaintext must be a Buffer');
}
if (!Buffer.isBuffer(key) || key.length !== KEY_LEN) {View on GitHub (pinned to fa13ee4ad6)
Solutions
- Regenerate cleanly: CLAUDE_FLOW_ENCRYPTION_KEY=$(openssl rand -hex 32 | tr -d '\n') and confirm the value is exactly 64 hex characters
- If using base64, ensure it is standard base64 (chars A-Za-z0-9+/) 43-44 chars long — re-encode base64url keys with `tr '_-' '/+'`
- Check the value actually delivered to the process: `node -e "console.log(process.env.CLAUDE_FLOW_ENCRYPTION_KEY.length)"` must print 64 (hex) or 43/44 (base64)
- Remove surrounding quotes/newlines in docker-compose/Kubernetes YAML and verify no `0x` prefix on hex keys
Example fix
# before — passphrase + base64url chars, decodeKey() throws export CLAUDE_FLOW_ENCRYPTION_KEY="my-team-secret-2024" # after — exactly 64-char hex (32 bytes) export CLAUDE_FLOW_ENCRYPTION_KEY="3f2b8c1e9a4d67f0c5e8b2a19d4f7c6e0b3a58d29f1c47e6b0a3d59c8f21e740"
Defensive patterns
Strategy: validation
Validate before calling
function isValidKeyEncoding(raw: string): boolean {
const t = raw.trim();
return /^[0-9a-fA-F]{64}$/.test(t) || /^[A-Za-z0-9+/]{43}=?$/.test(t);
}
if (!isValidKeyEncoding(process.env.CLAUDE_FLOW_ENCRYPTION_KEY ?? '')) {
throw new Error('CLAUDE_FLOW_ENCRYPTION_KEY must be 64-char hex or 44-char base64');
} Type guard
const isHexKey = (v: string) => /^[0-9a-fA-F]{64}$/.test(v.trim());
const isBase64Key = (v: string) => /^[A-Za-z0-9+/]{43}=?$/.test(v.trim()); Prevention
- Generate keys only with openssl rand -hex 32 / -base64 32 and never hand-edit them
- Assert key length at startup (64 or 43/44 chars) before the first vault call
- Beware base64url (- and _) encodings from other tools — re-encode before use
- Strip trailing newlines when reading keys from files or mounted secrets
When it happens
Trigger: A key value with a trailing newline (e.g. `openssl rand -base64 32 > keyfile` then `$(cat keyfile)` — actually trimmed here, but quotes from compose files are not), surrounding quotes injected by YAML/compose interpolation, a base64url-encoded key using - and _, a raw passphrase like 'my-secret-key', a 32-character ASCII string, or a hex key with an 0x prefix (66 chars). decodeKey() trims whitespace first, so the usual culprit is charset or length, not spaces.
Common situations: Kubernetes/docker secrets that append a trailing newline or wrap the value in quotes; developers passing a human-chosen passphrase instead of random bytes; copying only part of the key across terminals; using a UUID or `head -c 16` output (wrong byte count); regenerating the key in a different encoding between environments so one env boots and the other throws.
Related errors
- Invalid completion type
- Router multimodal is enabled but LLM_ROUTER_MULTIMODAL_MODEL
- Routes config must be a flat array of routes
- Invalid route entry: ${JSON.stringify(r)}
- CLAUDE_FLOW_ENCRYPT_AT_REST is set but CLAUDE_FLOW_ENCRYPTIO
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/661899d7cef5a21f.
Report an issue: GitHub.