JuliusBrussee/caveman · error

cave_sandbox_tool_failed

cave_sandbox_tool_failed

Error message

cave_sandbox_tool_failed

What it means

The sandboxed tool worker answered with a result frame { ok: false } carrying no structured code, so the executor throws the generic fallback (result.code ?? 'cave_sandbox_tool_failed'). Something inside the worker failed - the tool threw a plain unstructured error, the worker process died, or the call was aborted by the timeout/parent signal - and no specific code survived the fd 3 length-prefixed result frame.

Source

Thrown at packages/agent/src/runtime.ts:4866

          reject(new Error(`cave_sandbox_failed:${redactSandboxError(Buffer.concat(stderr).toString("utf8"))}`));
          return;
        }
        reject(new Error("cave_sandbox_invalid_output"));
      });
      combined.addEventListener("abort", onAbort, { once: true });
      if (combined.aborted) onAbort();
      child.stdin.end(JSON.stringify({
        entry: pathToFileURL(entryPath).href,
        agentPath: executionContext.agentPath,
        rootDefinitionSha256: executionContext.rootDefinitionSha256,
        toolDefinitionSha256,
        tool: toolName,
        params,
        allowSideEffects,
        allowNetwork: profile?.network === true,
      }));
    });
    if (!result.ok) throw new Error(result.code ?? "cave_sandbox_tool_failed");
    return result.value;
  } finally {
    await rm(workspace, { recursive: true, force: true });
  }
}

/**
 * Above this many per-file `--allow-fs-read` flags, collapse the staged source
 * files to their common ancestor directory. A large project would
 * otherwise blow the OS argument limit (E2BIG) and the tool could not spawn at
 * all. The collapse is safe here: `sourceFiles` are paths inside the per-run
 * STAGED COPY, which already contains only the reachable source graph — never
 * the real project root with its .env and credentials.
 */
const SANDBOX_FS_READ_FLAG_THRESHOLD = 1024;

function commonAncestorDir(paths: readonly string[]): string {
  const dirs = paths.map((path) => resolve(dirname(path)));

View on GitHub (pinned to 766dce6b13)

Solutions

  1. Reproduce the tool outside the sandbox with the same params to see the underlying error
  2. Raise timeoutMs if the abort path is the cause
  3. Make the tool throw errors that carry a machine-readable code so result.code is present and specific
  4. Validate params before dispatch and check the tool's staged entry/dependency graph imports cleanly

Example fix

// before: tool throws a plain error -> generic code
run: async (params) => { throw new Error('boom'); }

// after: throw with a code the worker frame preserves
run: async (params) => { const e = new Error('boom'); (e as any).code = 'my_tool_parse_failed'; throw e; }
Defensive patterns

Strategy: try-catch

Try / catch

try {
  return await runSandboxedTool(params);
} catch (error) {
  if (error instanceof Error && error.message === 'cave_sandbox_tool_failed') {
    // generic worker failure: reproduce outside the sandbox to get the real error
    const real = await reproduceUnsandboxed(params);
    throw new Error(`sandboxed tool failed; unsandboxed repro says: ${real?.message ?? 'unknown'}`);
  }
  throw error;
}

Prevention

When it happens

Trigger: Tool code throwing a plain Error (no cave_ code) inside the sandbox worker; worker killed by AbortSignal.timeout(timeoutMs) or the parent signal (combined.aborted); worker crash (OOM, import failure of the entry module); malformed/truncated result frame from the worker.

Common situations: Long-running tools hitting a tight timeoutMs; tool entry modules that fail to import inside the sandbox (missing staged dependency); errors from third-party libraries bubbling unstructured; OOM on large inputs.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@766dce6b13 (2026-08-18). Data as JSON: /api/errors/8c582e4f9cdffcf7. Report an issue: GitHub.