paperclipai/paperclip · error · Error

ACPX ${agent} runtime executable must be a real regular file

Error message

ACPX ${agent} runtime executable must be a real regular file

What it means

`openVerifiedRuntimeExecutable` in packages/paperclip-runner/src/drivers/acpx/installation-integrity.ts performs a TOCTOU-hardened verification of the ACPX runtime executable before executing it. Before opening the file, it `lstat`s the path and requires the entry to exist and be a real regular file (not a symlink, directory, fifo, etc.). If the lstat fails or the entry is anything other than a regular file, this error is thrown to refuse running a potentially attacker-controlled path.

Source

Thrown at packages/paperclip-runner/src/drivers/acpx/installation-integrity.ts:1026

  } finally {
    await handle.close();
  }
}

async function openVerifiedRuntimeExecutable(
  executablePath: string,
  expectedDigest: string,
  agent: string,
): Promise<{ handle: FileHandle; identity: VerifiedAcpxCommandIdentity }> {
  const lexicalBefore = await lstat(executablePath, { bigint: true }).catch(
    () => null,
  );
  if (
    lexicalBefore === null ||
    lexicalBefore.isSymbolicLink() ||
    !lexicalBefore.isFile()
  ) {
    throw new Error(
      `ACPX ${agent} runtime executable must be a real regular file`,
    );
  }

  let handle: FileHandle;
  try {
    handle = await open(
      executablePath,
      verifiedExecutableOpenFlags(process.platform, constants.O_NOFOLLOW),
    );
  } catch {
    throw new Error(
      `ACPX ${agent} runtime executable could not be opened as a no-follow regular file`,
    );
  }

  try {
    const before = await handle.stat({ bigint: true });

View on GitHub (pinned to 01ad858492)

Solutions

  1. Verify the configured executable path exists and is a regular file, not a symlink: `lstat` it yourself before configuring the runtime
  2. Replace symlinked bin paths with the resolved real binary path (`fs.realpathSync` then check `statSync(...).isFile()` and reject if still a symlink at lstat level)
  3. Reinstall/repair the ACPX runtime so the binary is materialized on disk at the expected path
  4. Check parent-directory permissions so the process can stat the path
  5. Point the runtime at the canonical install location managed by the installer rather than a user-provided PATH lookup

Example fix

// before
const bin = '/usr/local/bin/acpx'; // symlink into node_modules
await spawnAcppRuntime({ executablePath: bin });
// after
import { lstatSync } from 'node:fs';
const st = lstatSync('/usr/local/bin/acpx');
if (st.isSymbolicLink() || !st.isFile()) {
  throw new Error('configure the resolved real binary path, not a symlink');
}
await spawnAcppRuntime({ executablePath: '/usr/local/lib/acpx/bin/acpx' });
Defensive patterns

Strategy: validation

Validate before calling

import { lstatSync } from 'node:fs';
function isRealRegularFile(p: string): boolean {
  try {
    const st = lstatSync(p);
    return !st.isSymbolicLink() && st.isFile();
  } catch {
    return false;
  }
}
if (!isRealRegularFile(executablePath)) {
  throw new Error(`resolve acpx binary to a real file: ${executablePath}`);
}

Type guard

function isRealRegularStat(st: { isFile(): boolean; isSymbolicLink(): boolean }): boolean {
  return !st.isSymbolicLink() && st.isFile();
}

Prevention

When it happens

Trigger: Calling openVerifiedRuntimeExecutable (via the ACPX runtime spawn path) when `lstat(executablePath)` returns null (path missing/unreadable), the entry is a symlink (common with npm-installed bins in node_modules/.bin), or the entry is a directory/device/fifo rather than a regular file.

Common situations: ACPX binary path misconfigured (points at a wrapper script dir or a symlinked global install); npm/pnpm 'bin' shims that are symlinks; binary not downloaded yet so the path does not exist; permissions on a parent directory deny stat; antivirus or cleanup tool removed the file mid-session.

Understand the failure class

Background: "File not found" and ENOENT errors: why libraries can't find a file that should exist — this error's family across 50 libraries.

Related errors


AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10). Data as JSON: /api/errors/54e79e76d5f06635. Report an issue: GitHub.