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
- Export all three variables before running: CAVE_CONTROL_URL, CAVE_TOKEN (or CAVE_API_TOKEN), CAVE_PROJECT_ID.
- Source your env file (e.g. `set -a; source cave.env; set +a;`) or use `env $(cat cave.env | xargs)` in CI.
- Check for typos/empty values: `printenv CAVE_CONTROL_URL CAVE_TOKEN CAVE_PROJECT_ID`.
- 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
- Commit a cave.env.example listing all required vars and source it in CI and locally.
- Fail fast in scripts: check env vars before invoking the CLI.
- Keep names consistent; prefer CAVE_TOKEN and document the CAVE_API_TOKEN alias.
- In CI, assert secrets are injected via a preflight step.
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
- invalid CAVEMAN_LISTEN
- invalid listener
- cave_claude_model_required
- caveman build: set CAVE_MODEL when zero or multiple…
- caveman-code: no supported provider credential found; set…
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)