affaan-m/ECC · error · Error

Refusing to invoke an Itô CLI shim. Set

Error message

Refusing to invoke an Itô CLI shim. Set ${EXECUTABLE_OVERRIDE} to the absolute dist/bin/ito.js path.

What it means

buildInvocation re-runs isCanonicalItoEntry on the executable before constructing the spawn, as a defense-in-depth check: ECC invokes the CLI as `process.execPath <executable> ...args` and refuses to pass a non-canonical entry (for example an `ito` shim resolved from PATH) any arguments, because arguments may carry credentials. In normal flow this is unreachable — resolveItoExecutable already returns a verified canonical path — so hitting it means invokeIto/buildInvocation was called directly with an arbitrary executable string.

Solutions

  1. Always derive the executable from resolveItoExecutable(environment) and pass that exact value through to invokeIto.
  2. Set ECC_ITO_CLI_EXECUTABLE to the canonical dist/bin/ito.js path so resolution succeeds normally.
  3. In tests, create a fake path whose final segments are dist/bin/ito.js so isCanonicalItoEntry passes.

Example fix

// before
import { invokeIto } from '../scripts/ito.js';
invokeIto('/usr/local/bin/ito', ['status']); // shim -> throws

// after
import { invokeIto, resolveItoExecutable } from '../scripts/ito.js';
invokeIto(resolveItoExecutable(process.env), ['status']);
Defensive patterns

Strategy: validation

Validate before calling

// Never hand-pick an executable string; always flow the resolved value through.
import { resolveItoExecutable, invokeIto } from './ito.js';
const exe = resolveItoExecutable(process.env); // already passes all checks
invokeIto(exe, ['status']); // buildInvocation's re-check now succeeds

Try / catch

try { invokeIto(exe, args); } catch (e) {
  if (/Refusing to invoke an Itô CLI shim/.test(e.message)) { throw new Error('Caller bug: executable must come from resolveItoExecutable()'); }
  throw e;
}

Prevention

When it happens

Trigger: Another script imports scripts/ito.js internals and calls invokeIto('/usr/local/bin/ito', args) or buildInvocation with a PATH-resolved shim instead of the resolveItoExecutable() return value.

Common situations: Embedding the ECC ito wrapper as a library; refactors that bypass resolveItoExecutable; tests stubbing the executable path with a mock file.

Related errors


AI-assisted analysis of affaan-m/ECC@06c5e118c4 (2026-08-18). Data as JSON: /api/errors/3aea20625b320bda. Report an issue: GitHub.

Appendix: source

Thrown at scripts/ito.js:257

      ? segment.toLowerCase() === expected.toLowerCase()
      : segment === expected;
  });
}

function isUsableExecutable(candidate) {
  try {
    const info = fs.statSync(candidate);
    if (!info.isFile()) return false;
    fs.accessSync(candidate, fs.constants.R_OK);
    return true;
  } catch {
    return false;
  }
}

function buildInvocation(executable, args) {
  if (!isCanonicalItoEntry(executable)) {
    throw new Error(
      `Refusing to invoke an Itô CLI shim. Set ${EXECUTABLE_OVERRIDE} to the absolute dist/bin/ito.js path.`
    );
  }
  return Object.freeze({
    executable: process.execPath,
    args: Object.freeze([executable, ...args]),
  });
}

function invokeIto(executable, args, environment = process.env) {
  const invocation = buildInvocation(executable, args);
  const command = getInvocationCommand(args);
  const isNodeQualification = command === "evals";
  const isDeviceLogin = command === "login";
  const result = spawnSync(invocation.executable, invocation.args, {
    cwd: process.cwd(),
    encoding: "utf8",
    // Keep policy helpers immutable for callers, but give child-process

View on GitHub (pinned to 06c5e118c4)