ruvnet/ruflo · error · Error

hexToBytes: odd-length hex string

Error message

hexToBytes: odd-length hex string

What it means

handleSaveConfig() wraps its entire file operation block — backup creation, merge read, and the final fs.writeFile — in one try/catch and rethrows any underlying error prefixed with 'Failed to save configuration:'. So this message is a wrapper: the real cause (permissions, missing directory, disk full, target is a directory) is in the appended error.message and in the preceding console.error('Failed to save config:') log line on the server. Note the safePath was already validated by validateConfigPath(), so this is an I/O failure, not a path-policy rejection.

Source

Thrown at plugins/ruflo-neural-trader/src/signed-artifact.ts:171

/**
 * Canonical bytes for signing = `JSON.stringify(body)` UTF-8 encoded.
 *
 * Matches the plugin-registry signer (publish-registry.ts:127-151) and the
 * CWE-347 smoke (smoke-plugin-registry-signature.mjs:193-200): plain
 * `JSON.stringify` with NO whitespace and NO key sort. The body shape is
 * authored by us (the signer), so deterministic key order is guaranteed by
 * construction — no canonicalizer needed.
 */
function canonicalBytes(body: SignedBacktestArtifactBody): Uint8Array {
  const message = JSON.stringify(body);
  return new TextEncoder().encode(message);
}

function hexToBytes(hex: string): Uint8Array {
  const clean = hex.replace(/^0x/, '');
  if (clean.length % 2 !== 0) {
    throw new Error(`hexToBytes: odd-length hex string`);
  }
  const out = new Uint8Array(clean.length / 2);
  for (let i = 0; i < out.length; i++) {
    out[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
  }
  return out;
}

function bytesToHex(bytes: Uint8Array): string {
  let s = '';
  for (let i = 0; i < bytes.length; i++) {
    s += bytes[i].toString(16).padStart(2, '0');
  }
  return s;
}

View on GitHub (pinned to fa13ee4ad6)

Solutions

  1. Check the server console output first — console.error('Failed to save config:', error) prints the raw cause (EACCES, EISDIR, ENOENT, ...) that the wrapped message only summarizes
  2. Fix permissions on the target: chmod u+w claude-flow.config.json (or chown to the server user)
  3. Create the parent directory before saving: mkdir -p config/ — the tool does not create intermediate directories
  4. Verify the target is a regular file, not a directory: ls -la claude-flow.config.json; remove and recreate if it is a directory
  5. Free disk space or enlarge the volume if the appended message mentions ENOSPC

Example fix

// before: server run from a dir where ./config/ does not exist
await client.callTool('config_save', { path: 'config/claude-flow.config.json', config: cfg });
// -> Failed to save configuration: ENOENT: no such file or directory

// after: ensure parent dir exists, then save
import { mkdir } from 'fs/promises';
await mkdir(path.dirname(targetPath), { recursive: true });
await client.callTool('config_save', { path: 'config/claude-flow.config.json', config: cfg });
Defensive patterns

Strategy: try-catch

Validate before calling

import { stat, mkdir, access, constants } from 'fs/promises';
import { dirname } from 'path';

async function ensureConfigWritable(target: string): Promise<void> {
  await mkdir(dirname(target), { recursive: true });
  try {
    const s = await stat(target);
    if (!s.isFile()) throw new Error(`${target} is a directory`);
    await access(target, constants.W_OK);
  } catch (e: any) {
    if (e.code !== 'ENOENT') throw e; // missing file is fine, we will create it
  }
}

Try / catch

try {
  await client.callTool('config_save', { path, config, createBackup: true });
} catch (e) {
  const msg = e instanceof Error ? e.message : '';
  if (/EACCES|EPERM/.test(msg)) throw new Error(`No write permission for ${path} — fix file ownership`);
  if (/EISDIR/.test(msg)) throw new Error(`${path} is a directory — remove it`);
  if (/ENOSPC/.test(msg)) throw new Error('Disk full — free space before saving config');
  throw e;
}

Prevention

When it happens

Trigger: Target file is read-only or owned by another user (EACCES/EPERM); safePath points at an existing directory (EISDIR); the parent directory does not exist (ENOENT) — validateConfigPath never mkdirs; disk full (ENOSPC); on Windows the file is locked by another process; merge:true with a corrupt existing JSON file logs a warning but the write itself can still fail for the reasons above.

Common situations: Running the MCP server as a different user than the one owning the config file; containers with read-only mounted config dirs; CI environments with small tmpfs; config file accidentally created as a directory by an earlier bad script.

Related errors


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