JuliusBrussee/caveman · error
cave_sandbox_side_effect_denied
cave_sandbox_side_effect_denied
Error message
cave_sandbox_side_effect_denied
What it means
The sandbox worker only executes a tool when request.allowSideEffects is exactly true, and only for tools whose effect is not "read". This guard prevents a sandboxed/subagent request from mutating state (writes, network mutations, external calls) unless the host explicitly opted in. It throws before selected.execute is ever invoked, so no side effect occurred.
Solutions
- Set allowSideEffects: true on the request if the side effect is intended and the caller is trusted
- Use a read-only alternative tool when the task should not mutate state
- Check the tool's definition.effect to know whether it requires the opt-in
- If the denial is unexpected, verify the tool definition wasn't changed from effect "read" to a mutating effect
Example fix
// before
const request = { tool: 'write_file', params };
// after
const request = { tool: 'write_file', params, allowSideEffects: true }; Defensive patterns
Strategy: validation
Validate before calling
const tool = definition.tools.find((t) => t.name === request.tool);
if (tool && tool.effect !== 'read' && request.allowSideEffects !== true) {
throw new Error('this tool has side effects; set allowSideEffects: true or use a read-only tool');
} Type guard
function canExecute(tool, request) {
return tool.effect === 'read' || request.allowSideEffects === true;
} Try / catch
try {
const result = await runSandboxTool(request);
} catch (error) {
if (error.message === 'cave_sandbox_side_effect_denied') {
// surface to the model/user that a read-only mode blocked a mutating tool
}
} Prevention
- Set allowSideEffects: true explicitly when the caller is trusted and mutation is intended
- Prefer read-only (effect: 'read') tools for subagent/sandboxed contexts
- Check definition.effect before dispatching a tool call in read-only mode
- Audit tool definitions after refactors so tools don't silently change from read to write
When it happens
Trigger: Invoking a tool whose definition has effect !== "read" (e.g. write/effectful tools) while request.allowSideEffects is undefined or false — e.g. spawning a subagent/sandbox request without the allowSideEffects flag.
Common situations: Running a subagent in read-only mode but its prompt calls a write tool; forgetting to pass allowSideEffects when constructing the sandbox request; a tool recently changed from read to write effect so previously-allowed calls now get denied.
Understand the failure class
Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.
Related errors
- cave_sandbox_tool_definition_mismatch
- AutoGen tools and workbench are mutually exclusive
- cannot persist MCP ownership journal at
- cave_claude_tool_contract_unsupported
- cave_claude_tool_result_invalid
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/04b6f85efee55f31.
Report an issue: GitHub.
Appendix: source
Thrown at packages/agent/src/tool-worker.ts:134
if (delegated.length !== 1) throw new Error("cave_sandbox_unknown_subagent");
const child = delegated[0]!.runtime!.definition as AgentDefinition;
if (!child || child.kind !== "agent") {
throw new Error("cave_sandbox_subagent_definition_invalid");
}
if (visited.has(child)) throw new Error("cave_sandbox_subagent_cycle");
visited.add(child);
definition = child;
}
const selectedTools = definition.tools.filter((item) => item.name === request.tool);
if (selectedTools.length !== 1 || selectedTools[0]!.runtime?.kind === "subagent") {
throw new Error("cave_sandbox_unknown_tool");
}
const selected = selectedTools[0]!;
if (toolDefinitionSHA256(selected) !== request.toolDefinitionSha256) {
throw new Error("cave_sandbox_tool_definition_mismatch");
}
if (selected.effect !== "read" && request.allowSideEffects !== true) {
throw new Error("cave_sandbox_side_effect_denied");
}
const value = await selected.execute(request.params as never, AbortSignal.timeout(selected.timeoutMs));
writeResult({ ok: true, value });
} catch (error) {
writeResult({ ok: false, code: failureCode(error) });
}
View on GitHub (pinned to 3ee70a1026)