ruvnet/ruflo · error · ExtractionError

extracted binary path failed validation

Error message

extracted binary path failed validation: ${validation.errors.join('; ') || 'unknown'}

What it means

Thrown by ruflo proxy install after archive extraction, when the extracted meta-proxy binary's resolved path fails PathValidator validation against the single allowed prefix extractDir. It is a defense-in-depth check that runs even though only one hard-coded expected relative path is ever read — its purpose is to catch a symlink swap or any case where the path about to be copied does not genuinely resolve inside the extraction directory. The install aborts before the binary is copied to its final location.

Solutions

  1. Re-run `ruflo proxy install` — a one-off tmpdir resolution glitch or race often clears on retry
  2. Set TMPDIR to a non-symlinked directory and retry: `mkdir -p ~/.tmp && TMPDIR=~/.tmp ruflo proxy install`
  3. Manually verify the downloaded archive against the release's SHA256SUMS — a repacked archive that changes symlink behavior may indicate tampering
  4. If reproducible, capture the validation.errors content from the message and report upstream with OS, TMPDIR, and Node version

Example fix

# before (extractDir behind a symlink, e.g. macOS /tmp)
ruflo proxy install   # extracted binary path failed validation
# after (normalize TMPDIR to a real path)
mkdir -p ~/.tmp && TMPDIR=~/.tmp ruflo proxy install
Defensive patterns

Strategy: try-catch

Validate before calling

import { realpathSync } from 'node:fs';
// Normalize a symlinked TMPDIR before scripting installs
const tmp = process.env.TMPDIR ?? '/tmp';
if (realpathSync(tmp) !== tmp) process.env.TMPDIR = realpathSync(tmp);

Type guard

const isExtractionError = (e: unknown): e is Error & { name: 'ExtractionError' } =>
  e instanceof Error && e.name === 'ExtractionError';

Try / catch

try {
  await installProxy({ version });
} catch (e) {
  if (isExtractionError(e)) {
    // Security-relevant abort: report with the validation errors; do not silently retry
    console.error('install aborted:', e.message);
    process.exitCode = 1;
  } else throw e;
}

Prevention

When it happens

Trigger: Calling installProxy() / `ruflo proxy install` where the resolved real path of extractedBinaryPath escapes extractDir: a symlink inside the archive replacing the expected binary entry, or the extraction directory itself resolving through a symlink so the validated path no longer shares the literal allowedPrefixes prefix.

Common situations: macOS where extractDir derives from os.tmpdir() and /tmp is a symlink to /private/tmp (literal prefix vs resolved path mismatch); a repacked or tampered release archive whose binary entry is a symlink; unusual filesystems (network mounts, case-insensitive volumes) where path resolution differs from the constructed path.

Related errors


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

Appendix: source

Thrown at v3/@claude-flow/cli/src/proxy/install.ts:165

    const extractDir = path.join(workDir, 'extracted');
    await extractArchive(archivePath, extractDir, releaseArchiveExtension(triple));

    const extractedBinaryPath = path.join(extractDir, binaryNameInArchive());
    if (!fs.existsSync(extractedBinaryPath)) {
      throw new ExtractionError(`archive did not contain the expected binary at its root: ${binaryNameInArchive()}`);
    }

    // Defense in depth: confirm the extracted binary genuinely resolves
    // inside extractDir (catches a symlink swap or similar), even though
    // we only ever read one specific expected relative path, never an
    // archive-listed one (so "zip slip" via arbitrary archive paths isn't
    // reachable here in the first place).
    const { PathValidator } = await import('@claude-flow/security');
    const validator = new PathValidator({ allowedPrefixes: [extractDir] });
    const validation = await validator.validate(extractedBinaryPath);
    if (!validation.isValid) {
      throw new ExtractionError(`extracted binary path failed validation: ${validation.errors.join('; ') || 'unknown'}`);
    }

    const finalPath = proxyBinaryPath();
    fs.mkdirSync(path.dirname(finalPath), { recursive: true, mode: 0o700 });
    const tmp = `${finalPath}.tmp`;
    fs.copyFileSync(extractedBinaryPath, tmp);
    fs.chmodSync(tmp, 0o755);
    // The activation transaction has already stopped and backed up the old
    // daemon. Windows rename does not replace an existing executable.
    fs.rmSync(finalPath, { force: true });
    fs.renameSync(tmp, finalPath);

    const liveSha = sha256Hex(fs.readFileSync(finalPath));
    const manifest: InstallManifest = {
      version: opts.version,
      sha256: liveSha,
      verifiedAt: new Date().toISOString(),
      pubkeyFingerprint: sha256Hex(Buffer.from(PROXY_RELEASE_PUBKEY_PEM)).slice(0, 16),

View on GitHub (pinned to 29f048fc3b)