JuliusBrussee/caveman · warning
integration change already running for ${agent}
Error message
integration change already running for ${agent} What it means
withIntegrationLock() serializes config mutations per agent using a lock directory under ~/.caveman/integrations/.lock-<agent>. On EEXIST it reads owner.json and probes the recorded PID: the lock is only reclaimed if the owner is dead (ESRCH) or, failing that, the lock is older than 30s. This error means a live concurrent process holds the lock, or the stale-reclaim race was lost.
Source
Thrown at packages/cli/src/index.ts:6802
function withIntegrationLock<T>(agent: string, run: () => T): T {
const lock = join(cavemanHome(), "integrations", `.lock-${agent}`);
const ownerPath = join(lock, "owner.json");
const token = randomUUID();
mkdirSync(dirname(lock), { recursive: true, mode: 0o700 });
try { mkdirSync(lock, { recursive: false, mode: 0o700 }); }
catch (error) {
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
let stale = false;
try {
const owner = JSON.parse(readFileSync(ownerPath, "utf8")) as { pid?: unknown };
if (typeof owner.pid === "number" && Number.isInteger(owner.pid) && owner.pid > 1) {
try { process.kill(owner.pid, 0); }
catch (probeError) { stale = (probeError as NodeJS.ErrnoException).code === "ESRCH"; }
}
} catch {
try { stale = Date.now() - statSync(lock).mtimeMs > 30_000; } catch { /* raced; treated live */ }
}
if (!stale) throw new Error(`integration change already running for ${agent}`);
const quarantine = `${lock}.stale-${token}`;
try {
renameSync(lock, quarantine);
mkdirSync(lock, { recursive: false, mode: 0o700 });
process.stderr.write(`${mark("warn")} reclaimed stale integration lock for ${agent}\n`);
} catch {
throw new Error(`integration change already running for ${agent}`);
} finally {
try { rmSync(quarantine, { recursive: true, force: true }); } catch { /* isolated stale lock only */ }
}
}
try {
atomicWriteFile(ownerPath, Buffer.from(JSON.stringify({ pid: process.pid, token, started_at: new Date().toISOString() }) + "\n"));
} catch (error) {
try { rmSync(lock, { recursive: true, force: true }); } catch { /* original error remains authority */ }
throw error;
}
try { return run(); }View on GitHub (pinned to 27d5a3981a)
Solutions
- Check for a running caveman process for that agent (`ps aux | grep caveman`) and let it finish before retrying
- If no process is running, remove the stale lock directory: rm -rf ~/.caveman/integrations/.lock-<agent>
- Serialize your automation: run one integration change per agent at a time (the lock exists to prevent torn config writes)
Example fix
# before $ caveman native install claude & $ caveman native install claude Error: integration change already running for claude # after $ wait # or: rm -rf ~/.caveman/integrations/.lock-claude (only if no live process) $ caveman native install claude
Defensive patterns
Strategy: retry
Validate before calling
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
function integrationLockHeld(cavemanHome: string, agent: string): boolean {
return existsSync(join(cavemanHome, "integrations", `.lock-${agent}`));
} Try / catch
try { withIntegration(agent, run); } catch (e) {
if (e instanceof Error && /integration change already running/.test(e.message)) {
await waitForConcurrentRun(agent); // or: rm -rf ~/.caveman/integrations/.lock-<agent> after verifying no live owner
withIntegration(agent, run);
} else throw e;
} Prevention
- Serialize caveman integration commands per agent in scripts and CI; never run them in parallel
- After a crashed run, verify no owner process lives (ps), then delete the .lock-<agent> directory before retrying
- Treat this error as advisory: the lock prevents torn writes to agent configs, so prefer waiting over force-clearing
When it happens
Trigger: Two caveman commands mutating the same agent's integration concurrently (e.g. `caveman setup` racing `caveman native install claude`); or a stale lock whose owner.json is unreadable and younger than 30s; or the renameSync reclaim lost a race with another process.
Common situations: Scripts firing multiple caveman install commands in parallel; a cron/CI job overlapping an interactive session; a crashed process leaving a lock with a PID now reused by a live process (probe succeeds, lock looks live).
Related errors
- cave_budget_controller_in_use
- cave_subagent_concurrency_limit_invalid
- cave_subagent_concurrency_limit
- native runtime: socket already active
- memory %s changed during supersede
AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15).
Data as JSON: /api/errors/cc616426f94f0ed9.
Report an issue: GitHub.