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

  1. Regenerate cleanly: CLAUDE_FLOW_ENCRYPTION_KEY=$(openssl rand -hex 32 | tr -d '\n') and confirm the value is exactly 64 hex characters
  2. If using base64, ensure it is standard base64 (chars A-Za-z0-9+/) 43-44 chars long — re-encode base64url keys with `tr '_-' '/+'`
  3. 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)
  4. 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

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


AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18). Data as JSON: /api/errors/661899d7cef5a21f. Report an issue: GitHub.