JuliusBrussee/caveman · error

integration is degraded; run `caveman doctor --fix` before…

Error message

${profile.display_name} integration is degraded; run `caveman doctor ${agent} --fix` before changing it

What it means

This error is thrown when enabling a native integration for an agent (claude/codex/gemini) whose existing integration status reports a degraded (not clean 'installed') state. The CLI refuses to layer a new enable on top of a broken integration and instead directs the user to repair it first with `caveman doctor <agent> --fix`. A bare `caveman doctor <agent>` only prints JSON stating 'degraded' without remediation, so the message deliberately points at the `--fix` flag (issue #1049).

Solutions

  1. Run `caveman doctor <agent> --fix` to repair the degraded integration, then retry the enable.
  2. Inspect `caveman doctor <agent>` JSON output to see which component is degraded before fixing.
  3. If repair is impossible, fully disable/revert the integration (restore config from backups or the journal) and re-enable cleanly.
  4. Check the agent's settings file for leftover caveman hooks/MCP entries from an older version and remove them manually, then re-run enable.

Example fix

// before
caveman enable claude   # throws: claude integration is degraded

// after
caveman doctor claude --fix
caveman enable claude
Defensive patterns

Strategy: validation

Validate before calling

import { execFileSync } from "node:child_process";
const status = JSON.parse(execFileSync("caveman", ["doctor", "claude"], { encoding: "utf8" }));
if (status.installed && status.state !== "installed") {
  execFileSync("caveman", ["doctor", "claude", "--fix"], { stdio: "inherit" });
}
// now safe: caveman enable claude

Prevention

When it happens

Trigger: Calling `caveman enable <agent>` when `nativeIntegrationStatus(agent)` returns `existing.installed === true` and `existing.state !== 'installed'` (i.e. state is 'degraded' or similar).

Common situations: A previous partial enable/disable left the agent's config in a mixed state; a manual edit removed part of the integration; a crashed run between journal writes; upgrading caveman changed hook/MCP entry shapes so the old install no longer matches the expected canonical form.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


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

Appendix: source

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

  if (profiles.length === 0) {
    console.error(detected ? "no supported native agent detected on PATH" : `caveman enable: supported agents are claude, codex, hermes, gemini, opencode, pi, and aider (got ${target ?? ""})`);
    process.exit(1);
  }
  const gw = gatewayURL();
  for (const profile of profiles) {
    const agent = profile.id as NativeAgent;
    const outcome = withIntegrationLock(agent, () => {
      recoverPendingNativeInstallUnlocked(agent);
      if (!which(binOf(profile))) throw new Error(`${profile.display_name} not found on PATH`);
      const mcpBinary = agent === "aider" ? undefined : nativeMcpBinaryRequired();
      nativeProxyBinaryRequired(gw);
      const existing = nativeIntegrationStatus(agent);
      if (existing.installed) {
        if (existing.state === "installed") return "already" as const;
        // `--fix` on purpose: bare `caveman doctor <agent>` prints JSON that says
        // `degraded` and nothing that says how to leave that state, so pointing
        // at it alone dead-ends the user who followed this line here (#1049).
        throw new Error(`${profile.display_name} integration is degraded; run \`caveman doctor ${agent} --fix\` before changing it`);
      }
      const mutations = nativeMutationsFor(agent, gw, mcpBinary);
      const route = mutations.find((item) => typeof item.owned?.route === "string")?.owned?.route;
      process.stderr.write(`caveman enable ${agent}: planned user-scoped writes\n`);
      for (const mutation of mutations) process.stderr.write(`  ${mutation.kind}: ${mutation.file}\n`);
      if (typeof route === "string") process.stderr.write(`  routing: ${route}\n`);
      process.stderr.write(agent === "aider" ? "  recovery MCP: unavailable in Aider\n" : `  recovery MCP: ${mcpBinary}\n`);
      process.stderr.write(agent === "aider"
        ? `  Core: read-only ${aiderCorePath()}; lifecycle/tool interception unavailable; Ledger observational\n`
        : agent === "pi"
          ? `  lifecycle/Core/tool rewrite: bundled Pi extension -> ${nativeHookCommand(agent)}\n`
          // Codex gets the lifecycle line without the "tool rewrite" claim: since
          // #1037 shrink-hook declines every Codex tool event, so promising one here
          // would be the same false claim `doctor` used to report.
          : agent === "codex"
            ? `  lifecycle/Core: ${nativeHookCommand(agent)}; command-output rewrite unavailable in Codex\n`
            : `  lifecycle/Core/tool rewrite: ${nativeHookCommand(agent)}${agent === "hermes" ? " via native plugin" : ` + ${cavemanBinForHook()} shrink-hook`}\n`);
      applyNativeMutations(agent, profile, mutations);

View on GitHub (pinned to 3ee70a1026)