JuliusBrussee/caveman · error

register requires CAVE_CONTROL_URL, CAVE_TOKEN, and…

Error message

register requires CAVE_CONTROL_URL, CAVE_TOKEN, and CAVE_PROJECT_ID

What it means

The `register` CLI command uploads a build lock to a CAVE control-plane so a build can be registered against a project. Before doing anything it reads three environment variables: CAVE_CONTROL_URL (base URL of the control plane, trailing slashes stripped), CAVE_TOKEN (or CAVE_API_TOKEN), and CAVE_PROJECT_ID. If any of the three is unset or empty, it throws this error instead of making a doomed network call.

Solutions

  1. Export all three variables before running: CAVE_CONTROL_URL, CAVE_TOKEN (or CAVE_API_TOKEN), CAVE_PROJECT_ID.
  2. Source your env file (e.g. `set -a; source cave.env; set +a;`) or use `env $(cat cave.env | xargs)` in CI.
  3. Check for typos/empty values: `printenv CAVE_CONTROL_URL CAVE_TOKEN CAVE_PROJECT_ID`.
  4. In CI, verify the secrets are actually mapped into the job's environment, not just defined as repo secrets.

Example fix

// before
npx caveman register
// after
export CAVE_CONTROL_URL=https://control.example.com
export CAVE_TOKEN=$CAVE_API_TOKEN
export CAVE_PROJECT_ID=proj_123
npx caveman register
Defensive patterns

Strategy: validation

Validate before calling

for (const v of ["CAVE_CONTROL_URL","CAVE_TOKEN","CAVE_PROJECT_ID"]) {
  if (!process.env[v]) throw new Error(`${v} is not set; cannot run caveman register`);
}

Type guard

const hasCaveEnv = (e: NodeJS.ProcessEnv): e is NodeJS.ProcessEnv &
  Record<"CAVE_CONTROL_URL"|"CAVE_TOKEN"|"CAVE_PROJECT_ID", string> =>
  Boolean(e.CAVE_CONTROL_URL && (e.CAVE_TOKEN ?? e.CAVE_API_TOKEN) && e.CAVE_PROJECT_ID);

Try / catch

try {
  await run(["caveman", "register"]);
} catch (err) {
  if (String(err?.message).includes("register requires CAVE_")) {
    console.error("Set CAVE_CONTROL_URL, CAVE_TOKEN and CAVE_PROJECT_ID first");
  }
  throw err;
}

Prevention

When it happens

Trigger: Running `caveman register` (via main) when CAVE_CONTROL_URL, CAVE_TOKEN/CAVE_API_TOKEN, or CAVE_PROJECT_ID is unset or an empty string in the process environment.

Common situations: Fresh CI job or new shell that sourced no cave.env file; CI secrets configured under the wrong names or mapped into the job environment incompletely; setting CAVE_TOKEN='' which fails the truthiness check; typos like CAVE_CONTOL_URL; forgetting per-project CAVE_PROJECT_ID when switching projects.

Understand the failure class

Background: "environment variable is not set" and "Missing keys in environment" errors: what missing required env var messages mean and how to fix them — this error's family across 28 libraries.

Related errors


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

Appendix: source

Thrown at packages/agent/src/cli.ts:469

  if (provider === "openai") {
    return { name: "OPENAI_API_KEY", available: Boolean(process.env.OPENAI_API_KEY) };
  }
  if (provider === "google" || provider === "gemini") {
    return {
      name: "GEMINI_API_KEY or GOOGLE_API_KEY",
      available: Boolean(process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY),
    };
  }
  return { name: `credential for ${provider || "unknown provider"}`, available: false };
}

async function register(_args: string[]): Promise<void> {
  const root = process.cwd();
  const controlURL = process.env.CAVE_CONTROL_URL?.replace(/\/+$/, "");
  const token = process.env.CAVE_TOKEN ?? process.env.CAVE_API_TOKEN;
  const projectID = process.env.CAVE_PROJECT_ID;
  if (!controlURL || !token || !projectID) {
    throw new Error("register requires CAVE_CONTROL_URL, CAVE_TOKEN, and CAVE_PROJECT_ID");
  }
  const lock = await readLock(root);
  const loaded = await loadBuildInputs(root, "caveman.config.ts");
  const checked = await validLockIdentity(root, loaded.config.entry);
  if (!checked || checked.build_sha256 !== lock.build_sha256) {
    throw new Error("cave_stale_lock:registration");
  }
  const response = await fetch(`${controlURL}/api/v1/projects/${encodeURIComponent(projectID)}/agent-builds`, {
    method: "POST",
    headers: {
      authorization: `Bearer ${token}`,
      "content-type": "application/json",
    },
    body: JSON.stringify({
      agent_slug: lock.agent_id,
      build_sha256: lock.build_sha256,
      plan_sha256: lock.plan_sha256,
      source_sha256: lock.source_sha256,

View on GitHub (pinned to 3ee70a1026)