Every capability falls into one of three consent tiers, and a fresh install shows an install-disclosure screen listing everything the app requests before it enters the dock. The user can turn ANY capability off at any time in the app's Permissions panel (the ⋯ menu → Settings on its row in the Apps manager) — even ones that never prompt — so treat every window.chatoss call as fallible and handle denial gracefully.
What the user experiences, per capability
- chatApi, fileDrop, webSearch, documents, sqlite, secrets, proposeTask, boards, drive: declared in the manifest, never prompt.
- driveShared: prompt-gated — it is the one drive path other apps' containers can see. Plain
drive(your own container) never prompts. - No capability at all, so nothing to declare and nothing to prompt:
manifest.get,scopedData.*,scopedTools.*,data.get/onChanged/publish,approvals.*,platform.info/platform.apis,apps.listApis,background.isHeadlessRun,documents.base64ToBytes. - fileAccess: one prompt on first use (Allow once / always / Deny). Denied dialogs return
null. - terminal: prompts per command with the exact command shown; "always" is per program. Declared
terminalCommandPrefixesare approved at install and skip the prompts; in headless/background runs only declared prefixes may run. → Terminal - webview: no per-call prompt — the manifest's
webviewAllowlistIS the boundary (shown to the user and OS-enforced), so windows can only reach the domains you declared. → Webview - notifications: prompts once on first send (Allow once / always / Deny);
send()returnsfalseif denied. - clipboardWrite / clipboardRead: NO prompt — a low-friction convenience. The user can still turn either off any time in the app's Permissions panel; a disabled call rejects.
- hostHttp: NO prompt for listed hosts — the
httpAllowlistIS the boundary (enforced in Rust + an SSRF guard). An UNDECLARED host prompts per request (scoped to the host; "Allow always" persists). Private/loopback/metadata addresses are always refused. → HTTP - globalShortcut: prompts once per accelerator on first registration (a system-wide hotkey);
register()returnsfalseif denied. - openExternal: NO prompt — restricted to the manifest's
openExternalAllowlist(http/https) +mailto:, enforced in Rust;file://and off-list hosts are refused. - background: disclosed on the install screen (with each task's schedule) and prompts once on first fire (Allow once / always / Deny; a Trusted app skips it). Bounded to the manifest's
backgroundTasks; switching it off in Permissions stops every task immediately. Background-capable apps' terminal sessions also survive ChatOSS quitting. → Background tasks - appInstall / preview / mcp: prompt-gated escalations — first use prompts (
appInstallnever auto-allows even for a Trusted app); mcp prompts per server host / command prefix. - apps.call / apps.registerApi: NO runtime prompts — the manifest's
apiRequests/apiExportsare disclosed and approved at install; the OS enforces both directions. → App-to-app APIs - data.set / tools.register: prompt quoting your manifest's "what"/"why". → Shared data & tools
Updates never widen permissions silently
A new .aip with the same id that adds ANY new capability shows the user the exact additions; denying keeps the installed version as-is. Write complete manifests from v1.
Prompts never hang forever
An unanswered permission prompt settles as denied after 5 minutes — a tool call never hangs forever. From inside your app, an unanswered prompt looks identical to a hang; the approvals API lets you show "waiting for your approval" instead.
Handle null/false results and rejections from every gated call gracefully — denial (or a turned-off capability) must not break the app.