ruvnet/ruflo · error · Error

Invalid ${ENV_KEY_VAR}: expected 32-byte key as 64-char hex

Error message

Invalid ${ENV_KEY_VAR}: expected 32-byte key as 64-char hex or 44-char base64

What it means

Thrown by decodeKey() when the supplied CLAUDE_FLOW_ENCRYPTION_KEY matches neither the strict 64-char hex regex nor the 43/44-char base64 regex yielding a 32-byte buffer. The vault would rather fail loudly than encrypt with a truncated key (which would silently produce ciphertext no correct key can decrypt).

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 6b01dc5a68)

Solutions

  1. Regenerate the key cleanly: `node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"` and copy the full 64 hex chars.
  2. Verify length and charset before deploying: hex must be exactly 64 chars of [0-9a-f].
  3. If using base64, ensure it is exactly 44 chars with padding (or 43 without) and decodes to 32 bytes.
  4. Do NOT pass a human passphrase — wait for the ADR-096 passphrase/keychain support.

Example fix

// before
CLAUDE_FLOW_ENCRYPTION_KEY=my-password   // wrong shape
// after
CLAUDE_FLOW_ENCRYPTION_KEY=9f3c...64 hex chars...a1   // 64-char hex of 32 random bytes
Defensive patterns

Strategy: validation

Validate before calling

function validateKeyShape(raw: string): void {
  const t = raw.trim();
  if (/^[0-9a-fA-F]{64}$/.test(t)) return;
  if (/^[A-Za-z0-9+/]{43}=?$/.test(t) && Buffer.from(t, 'base64').length === 32) return;
  throw new Error('CLAUDE_FLOW_ENCRYPTION_KEY must be 64-char hex or 44-char base64 (32 bytes)');
}

Type guard

const isVaultKey = (v: unknown): v is string =>
  typeof v === 'string' &&
  (/^[0-9a-fA-F]{64}$/.test(v.trim()) ||
   (/^[A-Za-z0-9+/]{43}=?$/.test(v.trim()) && Buffer.from(v.trim(), 'base64').length === 32));

Try / catch

try {
  decodeKey(raw);
} catch (e) {
  const msg = e instanceof Error ? e.message : String(e);
  if (msg.startsWith('Invalid CLAUDE_FLOW_ENCRYPTION_KEY')) {
    console.error('Regenerate: node -e "console.log(crypto.randomBytes(32).toString(\'hex\'))"');
    process.exit(2);
  }
  throw e;
}

Prevention

When it happens

Trigger: Key with trailing whitespace/newline that survives into a non-64 length, a hex string with one character too few/many, a base64 string without padding, a raw 32-byte string passed as text, or a key copied with a missing character.

Common situations: Editor/copy adding a trailing newline that is not trimmed (note decodeKey does .trim(), so this is usually fine), shell variable expansion dropping a character, hand-pasting a partial hash, or using a passphrase string directly instead of a derived key.

Related errors


AI-assisted analysis of ruvnet/ruflo@6b01dc5a68 (2026-08-12). Data as JSON: /api/errors/743792c2ad92a102. Report an issue: GitHub.