JuliusBrussee/caveman · error · Error

does not point to an executable

Error message

${envVar} does not point to an executable: ${explicit}

What it means

`ensureBinary({ name, envVar })` first honors an explicit override: if `process.env[envVar]` is set, that path must exist and be executable (checked by `executable()`). If it isn't, the installer throws immediately instead of silently ignoring the override and downloading. This catches misconfigured overrides early with the offending path in the message.

Solutions

  1. Run `ls -l "$CAVEMAN_<NAME>_BIN"` and confirm the file exists and has the x bit.
  2. Fix the env var to the real binary path, or `unset` it so ensureBinary falls back to PATH lookup/download.
  3. If the file exists but isn't executable: `chmod +x <path>`.
  4. If the binary was removed by an upgrade, reinstall it or let the installer download it by unsetting the override.

Example fix

// before
export CAVEMAN_RIPGREP_BIN=/opt/tools/rg-14/bin   # directory, not executable
// after
export CAVEMAN_RIPGREP_BIN=/opt/tools/rg-14/bin/rg && chmod +x "$CAVEMAN_RIPGREP_BIN"
Defensive patterns

Strategy: validation

Validate before calling

import { accessSync, constants } from "node:fs";
function isExecutable(p) {
  try { accessSync(p, constants.F_OK | constants.X_OK); return true; } catch { return false; }
}
const v = process.env.CAVEMAN_RIPGREP_BIN;
if (v && !isExecutable(v)) throw new Error(`CAVEMAN_RIPGREP_BIN not executable: ${v}`);

Try / catch

try {
  await ensureBinary({ name, envVar });
} catch (e) {
  if (String(e.message).includes("does not point to an executable")) {
    delete process.env[envVar]; // fall back to PATH lookup / download
    return ensureBinary({ name, envVar });
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling `ensureBinary({ name, envVar })` when `process.env[envVar]` is set to a path that is missing, a directory, or lacks the execute bit — e.g. `CAVEMAN_RIPGREP_BIN=/opt/rg/bin` (directory), a deleted binary, a Windows-style path on Linux, or a file that was never `chmod +x`.

Common situations: Hand-edited shell profile pointing at a path that moved after an upgrade; copied binary without preserving permissions; Docker image copied binaries with `COPY` losing exec bits; pointing at a wrapper script that isn't executable.

Understand the failure class

Background: "is not a valid" / "Invalid ... value" environment variable errors: how libraries validate env vars and what to do when they reject yours — this error's family across 48 libraries.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/003e8979182af076. Report an issue: GitHub.

Appendix: source

Thrown at packages/shared/binary-installer/installer.mjs:178

// exits within a second or two of being asked to.
async function replaceWithRetry(part, target, attempts = 6) {
  for (let attempt = 0; ; attempt++) {
    try {
      renameSync(part, target);
      return;
    } catch (error) {
      const locked = error.code === "EPERM" || error.code === "EBUSY" ||
                     error.code === "EACCES" || error.code === "ETXTBSY";
      if (!locked || attempt >= attempts - 1) throw error;
      await new Promise((done) => setTimeout(done, 100 * 2 ** attempt));
    }
  }
}

export async function ensureBinary({ name, envVar }) {
  const explicit = process.env[envVar];
  if (explicit) {
    if (!executable(explicit)) throw new Error(`${envVar} does not point to an executable: ${explicit}`);
    return explicit;
  }
  const found = onPath(name);
  if (found) return found;
  const binDir = join(process.env.CAVEMAN_HOME ?? join(homedir(), ".caveman"), "bin");
  const target = join(binDir, binaryInstallFilename(name));
  if (executable(target)) return target;

  const { os, arch } = targetPlatform();
  const artifact = `${name}_${os}_${arch}`;
  const base = (process.env.CAVE_BINARY_RELEASE_BASE ?? BINARY_RELEASE_BASE_DEFAULT).replace(/\/+$/, "");
  const release = `${base}/${BINARY_RELEASE}`;
  const timeout = timeoutMs();
  const [checksumsResponse, signatureResponse] = await Promise.all([
    asset(`${release}/checksums.txt`, timeout),
    asset(`${release}/checksums.txt.keysig`, timeout),
  ]);
  const [checksums, signature] = await Promise.all([checksumsResponse.text(), signatureResponse.text()]);

View on GitHub (pinned to 3ee70a1026)