ruvnet/ruflo · error · ExtractionError

archive did not contain the expected binary at its root

Error message

archive did not contain the expected binary at its root: ${binaryNameInArchive()}

What it means

After extracting the meta-proxy release archive, install checks that the expected binary (meta-proxy, or meta-proxy.exe on Windows) exists at the ROOT of the extraction directory. If it does not, it throws — meaning extraction 'succeeded' but the layout isn't what the installer expects: the release artifact nested the binary in a subfolder, the platform triple selected an archive with a different layout, or the archive was empty/not actually a binary release. A subsequent PathValidator check then guards against symlink tricks, but this error is purely 'the file isn't where it must be'.

Solutions

  1. Inspect the actual layout: extract the release archive for your platform manually (`tar -tf <asset>`) and see where the binary sits.
  2. Pin a known-good version until the layout mismatch is fixed: install with the previously working version number.
  3. Verify the detected platform triple (OS/arch) matches the asset you'd expect — wrong triple can select a differently-laid-out archive.
  4. If the release asset is simply wrong (nested/empty), report it — there is no flag to relocate the binary; the archive must contain it at the root.

Example fix

# before: release asset nests the binary
$ tar -tf meta-proxy-v9-x86_64-apple-darwin.tar.gz
meta-proxy-9.0.0/bin/meta-proxy      # NOT at root -> error 299

# after: pin the last correctly-packaged release
npx ruflo@latest proxy install --version 0.4.1   # archive has ./meta-proxy at root
Defensive patterns

Strategy: validation

Validate before calling

import { execFileSync } from 'node:child_process';
// Verify the release asset actually has the binary at its root BEFORE installing:
function archiveHasRootBinary(archivePath: string, binary: string): boolean {
  const listing = execFileSync('tar', ['-tf', archivePath], { encoding: 'utf-8', timeout: 60_000 });
  return listing.split('\n').map(l => l.trim().replace(/^\.\//, '')).includes(binary);
}
if (!archiveHasRootBinary(assetPath, process.platform === 'win32' ? 'meta-proxy.exe' : 'meta-proxy')) {
  throw new Error(`release asset layout invalid for ${version} — pin a known-good version`);
}

Type guard

function isMissingBinaryError(e: unknown): e is Error {
  return e instanceof Error && e.message.startsWith('archive did not contain the expected binary at its root');
}

Try / catch

try {
  return await installProxyBinary({ version: 'latest' });
} catch (e) {
  if (isMissingBinaryError(e)) {
    return installProxyBinary({ version: LAST_KNOWN_GOOD_VERSION }); // pin back to a correctly-packaged release
  }
  throw e;
}

Prevention

When it happens

Trigger: install downloads the asset for the detected platform triple and runs extractArchive, but the tarball's top level is e.g. 'meta-proxy-0.4.2-x86_64/bin/meta-proxy' instead of './meta-proxy'; or a release accidentally shipped sources only; or wrong-architecture asset (arm64 vs x64) with a different internal layout; or extraction silently extracting nothing (empty archive) while exiting 0.

Common situations: Upgrading to a release whose packaging changed layout without an installer update; running on an unusual platform triple whose asset is packaged differently; a release-process regression that nested binaries; proxies/mirrors serving a generic source tarball instead of the binary asset.

Related errors


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

Appendix: source

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

      assetBytes: assets.archiveBytes,
      assetFilename: assets.archiveFilename,
    });
    log(`Verified — sha256 ${sha256.slice(0, 16)}…`);

    // fetchReleaseAssets's dev (gh) path already wrote the archive to workDir
    // under archiveFilename; ensure it's there regardless of source so
    // extraction always has a real file to operate on.
    const archivePath = path.join(workDir, archiveFilename);
    if (!fs.existsSync(archivePath)) {
      fs.writeFileSync(archivePath, assets.archiveBytes);
    }

    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);

View on GitHub (pinned to 29f048fc3b)