JuliusBrussee/caveman · error

unknown directive

Error message

unknown directive '${id}'

What it means

directiveBlock() looks up a directive id in the DIRECTIVES table and fails closed when the id is absent (or only present via prototype keys, which Object.hasOwn excludes). The comment notes callers normally validate first, so this is a defensive internal error.

Solutions

  1. Validate the id against the exported DIRECTIVES keys before calling
  2. Fix the directive name typo in the invoking code/config
  3. Use Object.hasOwn(DIRECTIVES, id) checks in your own code mirroring the library's guard
  4. Upgrade/downgrade the CLI if the directive existed in a different version

Example fix

// before
directiveBlock(userInput);
// after
if (!Object.hasOwn(DIRECTIVES, userInput)) throw new Error(`unknown directive '${userInput}'`);
directiveBlock(userInput);
Defensive patterns

Strategy: validation

Validate before calling

const isValidDirective = (id: string) => Object.hasOwn(DIRECTIVES, id);
if (!isValidDirective(id)) throw new Error(`unknown directive '${id}'`);

Type guard

const isValidDirective = (id: string): id is keyof typeof DIRECTIVES => Object.hasOwn(DIRECTIVES, id);

Try / catch

try { installDirectiveNote(file, id) } catch (e) {
  if (String(e).startsWith('unknown directive')) { /* list valid ids and correct input */ }
}

Prevention

When it happens

Trigger: Calling directiveBlock/installDirectiveNote/removeDirectiveNote with an id not present as an own property of DIRECTIVES (e.g. 'constructor', '__proto__', a typo, or a directive removed in a newer version).

Common situations: Passing a user-supplied directive name straight through without validating against the known set; prototype-pollution style ids; config referencing a directive from an older CLI release.

Related errors


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

Appendix: source

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

      "the exact file or symbol.",
    ],
  },
  "deferred-tool-loading": {
    summary: "load tool descriptions on demand instead of the full catalog",
    lines: [
      "Directive: defer tool loading.",
      "Do not pre-declare every available tool on every request. Keep the task's core",
      "tool set declared, and load other tools' full descriptions through a",
      "search-then-load surface (e.g. tool search) only when a task needs them.",
    ],
  },
};
function directiveBlock(id: string): string {
  // Object.hasOwn, not a truthiness read: id is user input, and a bare
  // DIRECTIVES[id] would resolve prototype keys ('constructor', '__proto__')
  // to non-directive objects (review C8).
  const d = Object.hasOwn(DIRECTIVES, id) ? DIRECTIVES[id] : undefined;
  if (!d) throw new Error(`unknown directive '${id}'`); // callers validate first; fail closed anyway
  return [directiveBegin(id), ...d.lines, directiveEnd(id)].join("\n");
}
function installDirectiveNote(file: string, id: string): boolean {
  return installManagedBlock(file, directiveBegin(id), directiveEnd(id), directiveBlock(id));
}
function removeDirectiveNote(file: string, id: string): "removed" | "absent" | "corrupt" | "failed" {
  return removeManagedBlock(file, directiveBegin(id), directiveEnd(id));
}

// ── hard tier: opencode plugin ───────────────────────────────────────────────
// opencode exposes a real pre-exec command rewrite: a plugin's `tool.execute.before`
// hook can mutate `output.args.command` for the bash tool before it runs (the docs'
// own example does exactly this). We ship a small plugin that routes a noisy command
// through `caveman shrink` — reusing this very CLI's `shrink-hook` decision so the
// allowlist/skip rules stay in one place. Byte-safe: any failure leaves the command
// unchanged. (opencode's hook does not fire for subagent/MCP tool calls — sst/opencode
// #5894/#2319 — so the soft note remains a useful complement there.)
function opencodePluginPath(): string {

View on GitHub (pinned to 3ee70a1026)