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
- 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
- Fix permissions on the target: chmod u+w claude-flow.config.json (or chown to the server user)
- Create the parent directory before saving: mkdir -p config/ — the tool does not create intermediate directories
- Verify the target is a regular file, not a directory: ls -la claude-flow.config.json; remove and recreate if it is a directory
- 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
- Pre-flight ensureConfigWritable() before the first save in a session
- Watch the server console — console.error('Failed to save config:') carries the raw errno
- Persist configs in a writable directory you own; avoid read-only mounts for live config
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
- unknown game "${key}". Known: ${Object.keys(GAMES).join(', '
- unknown strategy "${name}". Available: ${roster.map((r) => r
- localCompute: no adapter for graphId=${input.graphId}
- signBacktestArtifact: privateKey must be 32 bytes (got ${pri
- SSRF guard: only HTTPS URLs are permitted, got ${parsed.prot
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/befddcca36eda7af.
Report an issue: GitHub.