JuliusBrussee/caveman · error

usage: ${invokedCommand("receipts")} verify <bundle.json> [-

Error message

usage: ${invokedCommand("receipts")} verify <bundle.json> [--pubkey <file>]

What it means

receiptsVerify needs a positional bundle path; positionalAfterOptions scans argv after the subcommand, skipping only --pubkey and its value. If no positional remains, the CLI prints the usage line and exits non-zero.

Source

Thrown at packages/cli/src/index.ts:17451

  try { parsed = JSON.parse(source); } catch { throw new Error("--pubkey JSON is malformed"); }
  const infos = Array.isArray(parsed.public_keys) ? parsed.public_keys : parsed.public_key ? [parsed.public_key] : [];
  if (infos.length === 0) throw new Error("--pubkey JSON must contain public_key or public_keys");
  const keys = decodeUniqueKeyring(infos, "--pubkey public_keys");
  const pinnedCurrent = keys.get(current.info.key_id);
  if (!pinnedCurrent || !pinnedCurrent.raw.equals(current.raw)) throw new Error("trusted --pubkey keyring does not contain the bundle public key");
  return { keys, trust: "pinned_keyring" };
}

// receiptsVerify validates a signed receipt bundle offline (no network). A raw
// --pubkey pins the current key; JSON may independently pin a full rotation
// keyring. Without either, embedded keys prove self-consistency, not publisher
// authenticity. Exits non-zero on any included content, signature, or
// scope-chain break. Tail/scope omission needs separately trusted head manifest;
// bundle output states completeness is not attested.
//   caveman receipts verify <bundle.json> [--pubkey <file>]
async function receiptsVerify(argv: string[]) {
  const file = positionalAfterOptions(argv.slice(1), new Set(["--pubkey"]));
  if (!file) throw new Error(`usage: ${invokedCommand("receipts")} verify <bundle.json> [--pubkey <file>]`);
  let bundle: ReceiptBundle;
  try { bundle = JSON.parse(await readFile(file, "utf8")) as ReceiptBundle; } catch (e) { return fail(`invalid bundle JSON: ${(e as Error).message}`); }
  if (!Array.isArray(bundle.receipts)) return fail("bundle receipts must be an array");

  try {
    const embedded = embeddedReceiptKeys(bundle);
    const pubkeyFile = flagFrom(argv, "--pubkey", "");
    const pinned = pubkeyFile ? await pinnedReceiptKeys(pubkeyFile, embedded.current) : null;
    const keys = pinned?.keys ?? embedded.keys;
    for (const receipt of bundle.receipts) {
      const embeddedKey = embedded.keys.get(receipt.signature?.key_id);
      if (!embeddedKey) return fail(`seq ${receipt.seq}: no embedded public key for key_id ${String(receipt.signature?.key_id)}`);
      if (pinned) {
        const trusted = keys.get(receipt.signature?.key_id);
        if (!trusted) return fail(`seq ${receipt.seq}: key_id ${String(receipt.signature?.key_id)} is not present in trusted --pubkey material`);
        if (!trusted.raw.equals(embeddedKey.raw)) return fail(`seq ${receipt.seq}: embedded key ${receipt.signature.key_id} does not match trusted --pubkey material`);
      }
    }

View on GitHub (pinned to 5184b3d11a)

Solutions

  1. Add the bundle path: caveman receipts verify <bundle.json> [--pubkey <file>]
  2. In scripts, guard with [ -n "$BUNDLE" ] or set -u so unset variables fail loudly
  3. Put the bundle path before the flags to avoid it being swallowed as a flag value

Example fix

# before
caveman receipts verify --pubkey keys.json

# after
caveman receipts verify bundle.json --pubkey keys.json
Defensive patterns

Strategy: validation

Validate before calling

const args = ["receipts", "verify", bundlePath];
if (pubkeyPath) args.push("--pubkey", pubkeyPath);
if (!bundlePath || !existsSync(bundlePath)) {
  throw new Error(`bundle path missing or not found: ${bundlePath}`);
}

Type guard

function hasBundlePositional(argv: string[]): boolean {
  const rest = argv.slice(1);
  for (let i = 0; i < rest.length; i++) {
    if (rest[i] === "--pubkey") { i++; continue; }
    return true;
  }
  return false;
}

Try / catch

try { execFileSync("caveman", args); }
catch (e) {
  if (/^usage: caveman receipts verify/.test(String((e as Error).message))) fail("missing bundle path argument");
  throw e;
}

Prevention

When it happens

Trigger: Running `caveman receipts verify` with no file, or `caveman receipts verify --pubkey keys.json` where the bundle path was forgotten (or was consumed as the --pubkey value).

Common situations: Shell scripts that pass empty variables ("$BUNDLE" unset expands to nothing); flag-order mistakes; copy-pasting a command minus the path.

Understand the failure class

Background: "no subcommand specified" and "... is required": CLI errors when a required argument is missing — this error's family across 13 libraries.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@5184b3d11a (2026-08-18). Data as JSON: /api/errors/90337d2aa9be018d. Report an issue: GitHub.