{"record":{"id":"abb295de75dd49e0","repo":"Hmbown/CodeWhale","slug":"app-denied","errorCode":"app_denied","errorMessage":"the user denied access to ${desc} on this computer — do not work around it; only they can change it (consent {action:\"revoke\"}).","messagePattern":"the user denied access to (.+?) on this computer — do not work around it; only they can change it \\(consent (.+?)\\)\\.","errorType":"error_code","errorClass":"ServerError","httpStatus":null,"severity":"error","filePath":"crates/tui/plugins/computer-use/mcp/server.mjs","lineNumber":579,"sourceCode":"    }\n    if (args.state_id != null) {\n      const st = appStates.get(args.state_id);\n      if (st?.computerId === computer.id) refs.push(st.app_ref);\n    }\n    const bound = boundApps.get(computer.id);\n    if (!refs.length && bound && BOUND_TARGET_TOOLS.has(name)) refs.push(bound);\n  }\n  let grant = null;\n  for (const ref of refs) {\n    // A state or element whose backend reported no identity at all has no app\n    // to consent to — observation never named one either, so there is nothing\n    // a recorded decision could match.\n    if (!ref || !consent.appKeys(ref).length) continue;\n    const { verdict, ref: known } = await consentForRef(computer, ref);\n    const desc = known.name ?? known.bundle_id ?? (known.pid ? `pid ${known.pid}` : \"the application\");\n    const arg = known.bundle_id ?? known.name ?? (known.pid ? `pid:${known.pid}` : \"the app\");\n    if (verdict.state === \"denied\") {\n      throw new ServerError(\"app_denied\",\n        `the user denied access to ${desc} on this computer — do not work around it; only they can change it (consent {action:\"revoke\"}).`,\n        { app: known });\n    }\n    if (verdict.state === \"undecided\") {\n      throw new ServerError(\"consent_required\",\n        `Codewhale needs the user's permission to use ${desc} on this computer — ask them, then record their answer with consent {action:\"allow\"|\"deny\", app:\"${arg}\"}.`,\n        { app: known });\n    }\n    grant = { ref: known, persisted: verdict.persisted === true };\n  }\n  // Taking the shared pointer/focus is a second, separate consent: the first\n  // activate:true is the moment the agent stops being background — on every\n  // platform, not just macOS.\n  if (name === \"open_application\" && args.activate === true) {\n    const fg = consent.foregroundDecision(computer.id);\n    if (fg.state === \"denied\") {\n      throw new ServerError(\"foreground_denied\",\n        `the user denied shared-desktop (foreground) control on this computer — continue with open_application activate:false (background control) or ask them to reconsider.`,","sourceCodeStart":561,"sourceCodeEnd":597,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/plugins/computer-use/mcp/server.mjs#L561-L597","documentation":"Thrown by the computer-use MCP server when a tool call targets an application the user has explicitly recorded as denied. The server treats a user deny decision as authoritative: the agent must not retry, rename the target, or attempt a workaround — only the user can change the decision via a consent {action:\"revoke\"} call. The error carries the known app identity (name, bundle_id, or pid) in its data.","triggerScenarios":"Any MCP tool call (e.g. screenshot, get_app_state, open_application, run_actions) that resolves to an application whose consent verdict is state === \"denied\" in consentForRef() during consentCheck().","commonSituations":"The user previously answered 'deny' to a permission prompt for this app; an automation retries a task that was denied in an earlier session with a persisted decision; the agent resolves the same app via a different spelling (pid vs bundle_id) but the deny was recorded under all app keys.","solutions":["Do not retry the call; inform the user the app is blocked and that only they can lift it.","Ask the user to explicitly revoke the denial, then call the consent tool with {action:\"revoke\", app:\"<bundle_id or name>\"}.","Target a different application the user has allowed instead of the denied one."],"exampleFix":"// before: retrying after app_denied\nawait callTool({ name: \"open_application\", arguments: { name: \"Keychain Access\", activate: true } });\n\n// after: stop and ask the user to revoke, then retry\n// user: \"ok, you may use it\"\nawait callTool({ name: \"consent\", arguments: { action: \"revoke\", app: \"com.apple.keychainaccess\" } });\nawait callTool({ name: \"open_application\", arguments: { name: \"Keychain Access\" } });","handlingStrategy":"try-catch","validationCode":"// no pre-call check available; the decision lives server-side\n// defensively: track apps the user has denied in this session and skip them\nconst deniedApps = new Set();\nif (deniedApps.has(targetApp)) return skip;","typeGuard":null,"tryCatchPattern":"try {\n  await callTool({ name, arguments });\n} catch (e) {\n  if (e.code === \"app_denied\") {\n    // stop; surface e.data.app to the user and offer consent {action:\"revoke\"}\n    return { blocked: true, app: e.data?.app };\n  }\n  throw e;\n}","preventionTips":["Treat a deny as final; never retry or re-spell the app identity to dodge it.","Cache denied app identities per session and skip them proactively.","When app_denied fires, ask the user explicitly before any further attempts on that app."],"tags":["consent","permission-denied","computer-use","mcp"],"backgroundTag":"permission-denied","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-22T16:17:23.217Z"}