can1357/oh-my-pi · info · ToolError

Tool call rejected by user (${target.name})

Error message

Tool call rejected by user (${target.name})

What it means

The permission outcome selected a 'reject_once' or 'reject_always' option, so SessionTools refuses to execute the tool and throws ToolError with the tool's name. This is the library's representation of an explicit user denial — the model's tool call is blocked and the rejection (for reject_always) is cached for future calls with the same permission cache key.

Source

Thrown at packages/coding-agent/src/session/session-tools.ts:838

					}
					if (raced.kind === "aborted" || signal?.aborted) {
						throw new ToolAbortError("Permission request cancelled");
					}
					const outcome = raced.outcome;
					if (outcome.outcome === "cancelled") {
						throw new ToolAbortError("Permission request cancelled");
					}
					const selectedOption = PERMISSION_OPTIONS_BY_ID.get(outcome.optionId);
					if (!selectedOption) {
						throw new ToolError(`Tool permission response used unknown option ID: ${outcome.optionId}`);
					}
					if (selectedOption.kind === "allow_always") {
						this.#acpPermissionDecisions.set(permissionIntent.cacheKey, "allow_always");
					} else if (selectedOption.kind === "reject_always") {
						this.#acpPermissionDecisions.set(permissionIntent.cacheKey, "reject_always");
					}
					if (selectedOption.kind === "reject_once" || selectedOption.kind === "reject_always") {
						throw new ToolError(`Tool call rejected by user (${target.name})`);
					}
					return await target.execute(toolCallId, args as never, signal, onUpdate, ctx);
				};
			},
		}) as T;
	}

	#isExplicitAutoApproveMode(): boolean {
		return (
			this.#autoApprove ||
			(this.#host.settings.isConfigured("tools.approvalMode") &&
				this.#host.settings.get("tools.approvalMode") === "yolo")
		);
	}

	/** Applies an enabled tool set and reconciles its `xd://` partition. */
	applyActiveToolsByName(toolNames: string[], forcePromptRefresh = false, signal?: AbortSignal): Promise<void> {
		return this.runToolRegistryMutation(

View on GitHub (pinned to 9690622007)

Solutions

  1. This is intended behavior — the agent loop should relay the rejection to the model as a tool error, not treat it as a crash.
  2. To stop seeing rejections, remove the reject-always preference or re-approve the tool in permission settings.
  3. If building an automated responder, return an allow option for trusted tools instead of reject.
Defensive patterns

Strategy: try-catch

Try / catch

try {
  await runTool();
} catch (err) {
  if (err instanceof ToolError && err.message.startsWith('Tool call rejected by user')) {
    // relay denial to the model as a normal tool result
    return { blocked: true, reason: err.message };
  }
  throw err;
}

Prevention

When it happens

Trigger: The permission prompt response mapped to an option with kind 'reject_once' or 'reject_always' (session-tools.ts:836-838); or an earlier reject_always decision was cached under `#acpPermissionDecisions` and replayed via the 'Tool call rejected by user (preference)' path.

Common situations: User clicked Deny/Reject on a tool permission dialog; an automated policy handler configured to reject certain tools; a previously saved 'always reject' preference auto-rejecting later calls.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/3873734764717696. Report an issue: GitHub.