paperclipai/paperclip · error · Error

Materialized OpenCode executable has unsafe permissions

Error message

Materialized OpenCode executable has unsafe permissions

What it means

After verifying the digest, the script checks the target's permission bits: it must be a regular file, executable (any of 0o111 set), and not group/world writable (no 0o022 bits). Anything else is a tampering/correctness risk and throws.

Source

Thrown at packages/paperclip-runner/scripts/materialize-opencode-binary.mjs:87

    linkSync(source, target);
  } catch (error) {
    const code = error?.code;
    if (!new Set(["EACCES", "EMLINK", "EPERM", "EXDEV"]).has(code)) {
      throw error;
    }
    copyFileSync(source, target);
  }
  chmodSync(target, 0o755);

  const sourceDigest = sha256(source);
  const targetDigest = sha256(target);
  if (sourceDigest !== targetDigest) {
    throw new Error("Materialized OpenCode executable digest mismatch");
  }
  const targetStat = lstatSync(target);
  const mode = targetStat.mode & 0o777;
  if (!targetStat.isFile() || (mode & 0o111) === 0 || mode & 0o022) {
    throw new Error("Materialized OpenCode executable has unsafe permissions");
  }

  const version = spawnSync(target, ["--version"], {
    encoding: "utf8",
    timeout: 30_000,
    windowsHide: true,
  });
  if (version.status !== 0 || version.stdout.trim() !== OPENCODE_VERSION) {
    throw new Error(
      `Materialized OpenCode executable did not report ${OPENCODE_VERSION}`,
    );
  }
  return { sourceDigest, target, version: OPENCODE_VERSION };
}

const invokedPath = process.argv[1]
  ? pathToFileURL(realpathSync(process.argv[1])).href
  : null;

View on GitHub (pinned to 01ad858492)

Solutions

  1. Confirm the filesystem honors chmod ('chmod 755 bin/opencode.exe && stat -c %a bin/opencode.exe'); build on a local ext4/overlayfs volume if not.
  2. Remove the target and re-run the materialization to re-apply 0o755 cleanly.
  3. Check for tools modifying permissions on node_modules (ACLs, sync tools) and exclude the directory.
  4. If the file is no longer a regular file, treat it as tampering: delete it, reinstall dependencies, re-run.

Example fix

# before
$ stat -c %a bin/opencode.exe  # e.g. 775 (group-writable)
# after
$ chmod 755 bin/opencode.exe
$ node scripts/materialize-opencode-binary.mjs
Defensive patterns

Strategy: validation

Validate before calling

import { lstatSync, chmodSync } from 'node:fs';
const st = lstatSync(target);
const mode = st.mode & 0o777;
if (!st.isFile() || (mode & 0o111) === 0 || (mode & 0o022) !== 0) {
  chmodSync(target, 0o755);
}

Try / catch

try {
  materializePinnedOpenCodeBinary();
} catch (err) {
  if (err.message === 'Materialized OpenCode executable has unsafe permissions') {
    chmodSync(target, 0o755);
    materializePinnedOpenCodeBinary();
  } else throw err;
}

Prevention

When it happens

Trigger: The target after chmodSync(0o755) still has an unsafe mode — e.g. the filesystem ignored the chmod (some mounts), a umask/ACL forced group-write bits, or the target was replaced by a non-file between link and stat (TOCTOU).

Common situations: Building on a mounted volume that does not honor chmod; an over-permissive umask; ACLs or sync tools adding write bits to node_modules; another process swapping the binary.

Understand the failure class

Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.

Related errors


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