Hmbown/CodeWhale · error · Error

The practice controls could not be read. Check…

Error message

The practice controls could not be read. Check Accessibility permission and retry.

What it means

After reading the app state, the check looks for two specific accessibility elements: a text field labeled 'Practice text' and a button labeled 'Apply'. If either is absent from state.elements the UI could not be inspected, almost always because macOS Accessibility permission was not granted to the process driving the inspection. Without AX access the element list comes back empty or incomplete.

Solutions

  1. Grant Accessibility permission (System Settings > Privacy & Security > Accessibility) to the process running the check, then restart it.
  2. Re-run the check — TCC changes only apply to newly started processes.
  3. Verify the practice app's UI still contains the 'Practice text' field and 'Apply' button (app version mismatch) if permission is already granted.

Example fix

// before
await runBackgroundCheck({ bundle }); // AX not granted

// after
// Grant Accessibility to the host process first, then:
try {
  await runBackgroundCheck({ bundle });
} catch (e) {
  if (String(e.message).includes("Accessibility")) {
    console.error("Enable Accessibility permission, then retry.");
  }
  throw e;
}
Defensive patterns

Strategy: validation

Try / catch

try {
  await runBackgroundCheck({ bundle });
} catch (e) {
  if (String(e.message).includes("Accessibility permission")) {
    console.error("Open System Settings > Privacy & Security > Accessibility, enable the host process, then rerun.");
  }
  throw e;
}

Prevention

When it happens

Trigger: runBackgroundCheck reaches get_app_state and state.elements contains no { role: 'AXTextField', label: 'Practice text' } and/or no { role: 'AXButton', label: 'Apply' } entry.

Common situations: Fresh installs where Accessibility (TCC) permission was never granted, permission granted to the wrong binary (the outer host instead of the practice app), or a localized/renamed practice UI.

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


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/c0bc70f8a568e83a. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/plugins/computer-use/app/background-check.mjs:62

  try {
    await until(() => ready);
    await call("open_application", { pid: child.pid, activate: false });
    const state = await call("get_app_state");
    if (demoDirectory) {
      fs.mkdirSync(demoDirectory, { recursive: true });
      // AX can register before WindowServer makes a new window capturable.
      // Retry only this read, before sending any input, within the check limit.
      while (true) {
        try { await call("screenshot", { app_ref: { pid: child.pid }, path: path.join(demoDirectory, "01-ready.png") }); break; }
        catch (error) {
          if (controller.signal.aborted || !error.message.includes("not capturable")) throw error;
          await delay(100, undefined, { signal: controller.signal });
        }
      }
    }
    const entry = state.elements.find(element => element.role === "AXTextField" && element.label === "Practice text");
    const apply = state.elements.find(element => element.role === "AXButton" && element.label === "Apply");
    if (!entry || !apply) throw new Error("The practice controls could not be read. Check Accessibility permission and retry.");
    // Backend targets are resolved AX records, using the exact state index.
    const focus = await call("resolve_element", { app_ref: { pid: child.pid }, windowIndex: entry.windowIndex, path: entry.path });
    if (!focus.found) throw new Error("The practice text field changed. Run the check again.");
    await call("left_click", { target: { ...focus.element, type: "element", app_ref: { pid: child.pid } } });
    const phrase = "Background check complete 🐋";
    await call("type", { text: phrase });
    if (demoDirectory) await call("screenshot", { app_ref: { pid: child.pid }, path: path.join(demoDirectory, "02-entered.png") });
    const button = await call("resolve_element", { app_ref: { pid: child.pid }, windowIndex: apply.windowIndex, path: apply.path });
    if (!button.found) throw new Error("The practice Apply button changed. Run the check again.");
    await call("left_click", { target: { ...button.element, type: "element", app_ref: { pid: child.pid } } });
    await until(() => applied);
    if (applied.value !== phrase) throw new Error("The text received by the practice window did not match. Check the app log and retry.");
    const capture = await call("screenshot", { app_ref: { pid: child.pid }, path: path.join(scratch, "practice.png") });
    if (demoDirectory) fs.copyFileSync(capture.file, path.join(demoDirectory, "03-verified.png"));
    if (capture.app_ref?.pid !== child.pid || !capture.pixels?.w || !capture.pixels?.h) throw new Error("The practice window screenshot could not be verified. Check Screen Recording permission.");
    const count = latest.samples;
    await until(() => latest.samples > count);
    const isolated = latest.samples > 0 && latest.pointerChanges === 0 && latest.foregroundChanges === 0;

View on GitHub (pinned to 73e0f67d83)