can1357/oh-my-pi · error · ToolError
Tool permission response used unknown option ID: ${outcome.o
Error message
Tool permission response used unknown option ID: ${outcome.optionId} What it means
The permission client responded with an optionId that is not present in PERMISSION_OPTIONS_BY_ID (the registry of valid allow/reject options offered with the prompt). SessionTools throws ToolError because an unrecognized option ID means the responder is out of sync with the prompt contract — proceeding would silently guess the user's intent.
Source
Thrown at packages/coding-agent/src/session/session-tools.ts:830
),
},
PERMISSION_OPTIONS,
signal,
).then(outcome => ({ kind: "permission" as const, outcome }));
raced = await Promise.race([permissionPromise, abortPromise]);
} finally {
signal?.removeEventListener("abort", onAbort);
}
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 ||View on GitHub (pinned to 9690622007)
Solutions
- Return only option IDs exactly as delivered in the permission request's options list — echo back a chosen option verbatim.
- Update the permission client/UI so its option IDs match the current protocol version.
- Fall back to a default reject outcome if the client cannot match any offered option, instead of inventing an ID.
Example fix
// before: inventing an id
return { outcome: { outcome: 'selected', optionId: 'yes' } };
// after: echo a real offered option
const allow = options.find(o => o.kind === 'allow_once');
return { outcome: { outcome: 'selected', optionId: allow.id } }; Defensive patterns
Strategy: validation
Validate before calling
const VALID = new Set(options.map(o => o.id));
if (!VALID.has(chosenOptionId)) {
throw new Error(`optionId ${chosenOptionId} not offered by this permission prompt`);
} Type guard
function isKnownOptionId(id: string, offered: { id: string }[]): boolean {
return offered.some(o => o.id === id);
} Try / catch
try {
await runTool();
} catch (err) {
if (err instanceof ToolError && /unknown option ID/.test(err.message)) {
logger.error('permission client out of sync', { err });
// fall back to a safe default: reject the tool call
return;
}
throw err;
} Prevention
- Always echo option IDs verbatim from the permission request — never construct them by hand.
- Keep RPC/ACP client and host versions aligned.
- Test custom permission UIs against every option kind the prompt can offer.
- Do not persist raw option IDs across sessions; re-resolve against the current prompt.
When it happens
Trigger: The client bridge's requestPermission handler returned `outcome.optionId` that isn't one of the IDs registered for this prompt (session-tools.ts:828-830) — e.g. stale/cached option IDs, a custom handler inventing IDs, or a protocol mismatch between host and client versions.
Common situations: Custom permission UI sending hand-written option IDs; mismatched versions of the RPC/ACP protocol where option ID formats changed; persisting and replaying an old decision option that no longer exists.
Related errors
- Replacement text is not valid UTF-8: {err}
- invalid glob `{pattern}`: {error}
- RPC chunk received before protocol negotiation
- RPC protocol v2 negotiation failed
- RPC chunk exceeded the transport limit
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/9a4252a70e2233ea.
Report an issue: GitHub.