yuzu

privacy_permissions

What it doesPer-app sensitive-permission grants — camera, microphone, location, full-disk-access equivalents (read-only)
Version1.0.0
KindCollector · read-only · gathered (crossplatform.privacy_permissions.permissions)
PlatformsWindows 🟡 constrained · macOS 🟡 constrained · Linux 🟡 constrained
Actionspermissions (definition crossplatform.privacy_permissions.permissions)
Securitysecurable Forensics · operation Read · risk High · dispatch ReadOnly · approval gate AdminOrApproval
Rolesexecute: admin · author: content-author

How it works

One action, permissions, takes no parameters and reports which apps hold camera, microphone, location and full-disk-access grants, as rows permissions|<os>|<app_id>|<category>|<state>|<raw>|<last_used_start>|<last_used_stop> (the leading field is the definition’s row_kind column). Every leg is an in-process read of the operating system’s own permission store: no PowerShell and no subprocess on any OS. Every category of every source becomes a row, so a refused or failed read is a denied or unreadable row and never silence or “no grants”. The action changes nothing on the host.

The action needs the Forensics securable at admin-or-approval level, targets one device, and stays refused until an operator enables the plugin’s server-side kill switch.

flowchart LR
  OP[Operator / workflow] --> SRV[Server<br/>authz: Forensics.Read<br/>AdminOrApproval · single-target only<br/>kill switch must be enabled]
  SRV -- gRPC mTLS --> HOST[Agent plugin host] --> EX[privacy_permissions.execute]
  EX --> WIN[Windows leg<br/>ConsentStore: live HKU hive, guarded NTUSER.DAT, HKLM merge]
  EX --> MAC[macOS leg<br/>TCC.db, in-process sqlite3]
  EX --> LIN[Linux leg<br/>portal PermissionStore, session bus]
  WIN & MAC & LIN --> ROWS[rows + typed result status] --> RS[(ResponseStore)] --> API[REST /api/responses]

Reading rules in detail

macOS

Windows

Linux

What you can decide with it

QuestionWindowsmacOSLinux
Which apps may use the camera or microphone, for each user?Answered per profile and per app, including apps launched from an executable path; the machine-wide toggle is applied on topAnswered per user and per app only if the agent holds Full Disk Access; without it every source reads deniedEffectively not answerable from the service: no session bus means UNAVAILABLE
Is location sharing granted?AnsweredNever: always unsupported (macOS keeps it outside TCC)Only for the agent’s own session, if one exists
Which apps have broad file access?Settings’ “File system” toggle only, which is not Full Disk AccessThe real Full Disk Access list, with the FDA caveat aboveNever: unsupported
Was an app using the device recently?Last-used start and stop times for apps that have themNot providedNot provided

What a result does and does not prove. allowed means the store records a grant, not that the app is using the device now; absent means the store has no record, and a native app opening a device directly on Linux never appears at all. denied carries two meanings kept in one state: a decoded refusal (raw is the native value such as Deny or 0) and a refused read (raw is a <subject>:<cause> token); always check raw before treating denied as a user’s choice. A status of OK with no failure rows means every source was read or is honestly empty, and a host with no grants still reports OK.

Row states at a glance:

StateMeansAction
allowedThe store records a grantCompare with policy
deniedA decoded refusal, or a read the OS refusedRead raw to tell which; on macOS every source refused means the agent lacks Full Disk Access
prompt_undeterminedThe store holds a value the plugin does not mapRead raw; never treated as allow or deny
absentNo record for this app and categoryNot a failure
unreadableThe read itself failedraw names the token; some are retryable (see Result status)
unsupportedNo mechanism reaches this category hereUse another source

Diagrams

Windows: one profile’s hive ladder

flowchart TD
  PL["HKLM ProfileList: real profiles, service accounts removed"] --> LH{"Is HKU SID hive loaded?"}
  LH -- "yes" --> W["Arm change watch, walk ConsentStore"]
  LH -- "no" --> G{"Hive-file guard passes? paths, owner, sidecars, size"}
  G -- "no" --> U["Row: unreadable, hive_ token. Nothing loaded"]
  G -- "yes" --> M["RegLoadKeyW under the offline-hive lock"]
  M --> I{"File identity unchanged after load?"}
  I -- "no" --> U2["Unload unread: hive_identity_changed"]
  I -- "yes" --> W
  W --> C{"Watch reports a change?"}
  C -- "yes, first time" --> W
  C -- "yes, again or no time left" --> U3["Row: changed_during_read"]
  C -- "no" --> R["Profile rows"]
  R --> H["Merge with HKLM: a read Deny wins"]

A loaded hive is read directly; an offline mount runs the guard first, verifies the file after loading, and unloads it when anything looks wrong. Whatever is read is watched for concurrent change and walked at most twice; a profile that stays unstable or cannot be read produces an explicit row, never a guess.

macOS: reading one TCC.db safely

sequenceDiagram
  participant A as Agent
  participant K as Kernel mount table
  participant D as TCC.db and its directory
  participant S as SQLite
  A->>K: read mount table once, cached copy
  A->>A: network-backed path? refuse before touching it
  A->>D: open directory then file with O_NOFOLLOW_ANY
  A->>D: lstat journal, wal and shm names, snapshot fstat and header
  A->>S: open /dev/fd/N with immutable=1, query_only, defensive mode
  S-->>A: access rows for camera, microphone, full-disk-access
  A->>D: snapshot again and compare with the first
  A->>A: any difference, sidecar or WAL mode makes the source unreadable

The file is opened exactly once and never written, locked or followed through a link. A sidecar, a WAL-mode database or any change between the two snapshots makes the whole source unreadable, so a half-written database is never reported as grants.

How the result status is chosen

flowchart TD
  S["All rows collected"] --> A{"Any read refused?"}
  A -- "yes" --> PD["PERMISSION_DENIED, partial"]
  A -- "no" --> B{"Any failure token?"}
  B -- "yes" --> CO["CONSTRAINED, partial"]
  B -- "no" --> C{"Mechanism not reachable? Linux, no session bus or portal"}
  C -- "yes" --> UN["UNAVAILABLE, portal:unavailable"]
  C -- "no" --> OK["OK, full"]

Refusal wins over every other failure, and a plain failure wins over unavailability. The typed status is kept with the result but is not shown as a label on REST, MCP or the dashboard, so read each row’s state and raw as well.

OS capability

ActionWindowsmacOSLinux
permissions🟡 constrained · rung 1 · HKLM ProfileList enumeration, then each real profile’s SOFTWARE\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore via its loaded HKU\ hive or an offline NTUSER.DAT mount (RegLoadKeyW, SeBackup/SeRestore), plus the same HKLM ConsentStore path🟡 constrained · rung 1 · TCC.db read-only, in-process sqlite3 over one descriptor with an immutable URI (no lock, no -journal/-wal/-shm ever opened or created; a WAL-mode or journal-bearing file is refused, and a file that changes during the read is discarded): the system /Library/Application Support/com.apple.TCC/TCC.db plus each /Users/ (uid >= 500) per-user Library/Application Support/com.apple.TCC/TCC.db🟡 constrained · rung 1 · xdg-desktop-portal org.freedesktop.impl.portal.PermissionStore.Lookup over the agent process’s own session bus (sd_bus_open_user)

Declared limits per leg (descriptor fallback text, verbatim):

Privileges and prerequisites

OSRuns asExtra grant neededMeasuredIf the read is refused
Windowsagent service, LocalSystem; SeBackupPrivilege/SeRestorePrivilege are enabled only inside the offline-hive arm, under the process-wide offline-hive lockNone for a loaded hive; the offline arm needs the two privileges LocalSystem already holdsCapture and both arms (live hive, guarded offline NTUSER.DAT) as LocalSystem on one standalone bare-metal host (the-rig, Windows 10.0.26200). The unit tests, including the hostile-shape, post-load refusal and sidecar cases, were also run as an elevated user on that host, and on a second host (Windows 11, native MSVC, an elevated user at High integrity holding the backup and restore privileges), not as LocalSystem there. Not measured: an unelevated token, domain-joined, Entra-joined, roaming, FSLogix or RDS profiles, Windows Server, and the production NT SERVICE\YuzuAgent identity (#1442)A refused registry read (ERROR_ACCESS_DENIED) reports that source or key denied (<profile>:access_denied, <profile>:<category>:value_access_denied), PERMISSION_DENIED/PARTIAL; a missing privilege reports <profile>:privilege_missing denied; a refused or unsafe hive file reports <profile>:<hive_* token> unreadable
macOSagent daemon, root today (LaunchDaemon carries no UserName key — docs/agent-privilege-model.md TL;DR)Every TCC.db — the system one and each per-user one — is TCC-protected regardless of privilege level; a separate Full Disk Access (FDA) grant is needed (how to grant it to the agent is not yet measured)One Mac (macOS 26.6.2), euid 501 with ambient FDA (NOT the production agent identity): the system db and the per-user db both read (per-user microphone grants visible). The same run with both TCC directories read-denied (a sandbox-exec deny profile, standing in for a non-FDA identity) reported both sources denied, PERMISSION_DENIED/PARTIAL. Not measured: the production root LaunchDaemon’s own outcome, how to grant it Full Disk Access, and the verbatim sqlite3_errmsg a real TCC refusal producesA refused open (EPERM/EACCES), SQLITE_AUTH/SQLITE_PERM, or SQLITE_CANTOPEN whose underlying syscall failed EPERM/EACCES on a file that is there, reports that source’s whole-source row denied (tcc_db:open_failed:errno_<n>, <user>:tcc_db:open_failed:errno_<n>, ...:open_failed:<msg>), PERMISSION_DENIED/PARTIAL. An agent without FDA sees this for every source
Linuxagent daemon, default (a service user with no session bus)None — the portal call is an ordinary session-bus D-Bus method call, which the agent’s own session must offerDebian 13 container, xdg-desktop-portal 1.20.3’s permission store seeded with test records: the devices and location Lookups decoded as above. Not measured on a desktop user’s real sessionA refused session-bus socket (session_bus:access_denied) or an AccessDenied-shaped D-Bus error (<category>:access_denied, on that category’s own row) reports denied, PERMISSION_DENIED/PARTIAL; a malformed reply reports unreadable with <category>:shape

Binaries/subprocesses: none — every leg is an in-process read (SQLite, the Windows registry API, sd-bus). Network: the Linux leg’s D-Bus call is local-machine IPC only, never a network socket.

Data contract

Inputs

The action takes no parameters.

Outputs

One row per app per category, led by row_kind permissions (constrained only on the one internal-error row, constrained|<os>|-|-|unreadable|internal_error|-|-, which keeps the same field count). Every category of every source is a row — its decoded grants, absent when the source cleanly holds none, unsupported when no mechanism reaches it (macOS location, Linux full_disk_access), or a failure row — or is covered by a whole-source row (category -) for a source that could not be read at all, or that holds no file (a macOS user with no per-user TCC.db, absent). denied covers two facts, kept as one state: a decoded refusal, whose raw is the native value (Deny, 0, no, NONE,<timestamp>), and a refused read, whose raw is a <subject>:<cause> failure token (the agent-log provenance carries it too, except Windows per-app failures, which reach it as the coarse <source>:<category>:<cause>; the row sanitizer writes a \ in a token as /) and whose category is - when the whole source was refused; unreadable always carries a token. app_id - means no specific app (on Windows, the capability’s own toggle; NonPackaged is the desktop-apps toggle); a row from a per-user source is qualified <user>/<app_id> (each Windows profile, each macOS per-user TCC.db); an unqualified app_id comes from a machine-wide source (the macOS system TCC.db, the Windows HKLM mirror) or, on Linux, from the agent’s own session’s portal store — that one session’s grants, never machine-wide. On Windows, last_used_start/last_used_stop read unreadable (plus a ...:last_used_start_<cause> token in the agent log) when the value exists but is not an 8-byte REG_QWORD or could not be read.

crossplatform.privacy_permissions.permissions — row_kind|os|app_id|category|state|raw|last_used_start|last_used_stop

FieldTypeValuesAvailableExampleDescription
row_kindstringpermissions constrainedWindows, Linux, macOSpermissionsRow family: permissions (every data row) or constrained (the one row the plugin writes when it fails internally: raw is internal_error, the result is CONSTRAINED).
osstringmacos windows linuxWindows, Linux, macOSmacosThe reporting OS.
app_idstring-Windows, Linux, macOScom.example.AppPer-app identifier: a TCC client id (bundle id or path, macOS), an executable path or Package Family Name (Windows), a portal-reported app id (Linux). ”-” means no specific app: a Windows capability-level toggle, a per-category absent/ unsupported row, or a failure row. On Windows NonPackaged is the capability’s “let desktop apps access” toggle; a desktop app’s own row reads absent (it stores no decision) and is governed by that toggle. A row read from a PER-USER source is qualified with that user’s name — <user>/<app_id> or <user>/- on the wire (the row sanitizer writes the separator \ as /, as it does inside every path): each Windows profile’s ConsentStore and each macOS per-user TCC.db. An unqualified app_id comes from a machine-wide source (the macOS system TCC.db, the Windows HKLM ConsentStore mirror) or, on Linux, from the portal store of the agent’s OWN session — that one session’s grants, never machine-wide.
categorystringcamera microphone location full_disk_access -Windows, Linux, macOScameraThe fixed cross-OS permission category. ”-” only on a whole-source row — one row standing for every category of a source that could not be read at all (a TCC.db, a Windows profile hive, ConsentStore root or ProfileList discovery, the session bus/portal, or what a Windows or macOS run left unread once a budget or time limit was spent), or that holds no file (a macOS user with no per-user TCC.db).
statestringallowed denied prompt_undetermined absent unreadable unsupportedWindows, Linux, macOSallowedallowed/denied: a real, decoded grant. denied also covers a READ that was refused — one state for both, told apart by raw: a decoded refusal carries the native value (Deny, 0, no, NONE,), a refused read a <subject>:<cause> token (and category ”-” when the whole source was refused). prompt_undetermined: the mechanism reported a value this plugin does not map to allowed/denied (never guessed) — a mapped Prompt/ask/macOS auth_value 1 and an unmapped value alike; raw keeps the true value. absent: no record for this app+category (or this source) — not a failure; a Windows NonPackaged app row also reads absent, WITH its last-used times: the app stores no decision of its own and its state follows the NonPackaged toggle row. unreadable: the read itself failed; raw names why. unsupported: no mechanism reaches this category on this OS/host (macOS location; Linux full_disk_access; Linux with no session bus or portal daemon).
rawstring-Windows, Linux, macOS2The mechanism-native value behind state (a TCC auth_value integer: 0 denied, 2 and 3 allowed, anything else prompt_undetermined; a ConsentStore Value string; a joined portal permission list); on a failure row (denied from a refused read, or unreadable) the failure token, with \ written / (the agent-log provenance carries it too, except a Windows per-app failure, which reaches it as the coarse <source>:<category>:<cause>); ”-” when nothing meaningful beyond the state itself.
last_used_startstring-Windows1700000000000Windows ConsentStore LastUsedTimeStart, epoch milliseconds; ”-” when never set or on any other OS; unreadable when the value exists but is not an 8-byte REG_QWORD or could not be read (a ...:last_used_start_<cause> token names it).
last_used_stopstring-Windows1700000100000Windows ConsentStore LastUsedTimeStop, epoch milliseconds; ”-” when never set (a stored 0 also reads ”-”: conventionally in use now, unmeasured) or on any other OS; unreadable when the value exists but is not an 8-byte REG_QWORD or could not be read (a ...:last_used_stop_<cause> token names it).

Result status

The typed status is recorded with the result (and appears unlabelled in agent-transition events) but is not shown as a label on REST, MCP or the dashboard, so read each row’s state and raw; completeness and provenance reach only the agent log.

StatusCompletenessProvenanceWhen
OKFULL—No read was refused and no failure token exists: every category was read or is absent/unsupported. Completeness is in the rows: any unreadable/denied row, or a category - row, means a source was not fully read. A host with no grants recorded for any mapped category still reports OK.
UNAVAILABLEFULLportal:unavailableLinux only: the agent’s own account has no session bus, or no portal backend answered any lookup (ServiceUnknown on every table). One whole-source row, state unsupported (app_id and category both -). Says nothing about interactive users’ grants.
CONSTRAINEDPARTIALsee belowA read failed for a reason other than a refusal; each failed read is its own unreadable row naming the token (bar the two agent-log-only cases below).
PERMISSION_DENIEDPARTIALsee belowAt least one read was refused — a TCC-protected TCC.db without Full Disk Access, a refused registry read on Windows, a refused session-bus socket or a portal AccessDenied on Linux. Wins over CONSTRAINED when both occur.

Every token is <subject>:<cause>; on a failure row it is also the row’s raw. The complete set this plugin emits today, by leg, with what it means and what to do:

Where the data goes

Sample output

Windows — captured: windows Windows 10.0.26200 x86_64 · bare-metal · 2026-10-02 · LocalSystem (elevated; account names replaced with jsmith) · leg-hash 6f22f62118ed

== action=permissions
permissions|windows|jsmith/-|camera|allowed|Allow|-|-
permissions|windows|jsmith/-|full_disk_access|allowed|Allow|-|-
permissions|windows|jsmith/-|location|allowed|Allow|-|-
permissions|windows|jsmith/-|microphone|allowed|Allow|-|-
permissions|windows|jsmith/5319275A.WhatsAppDesktop_cv1g1gvanyjgm|camera|denied|Deny|-|-
permissions|windows|jsmith/5319275A.WhatsAppDesktop_cv1g1gvanyjgm|location|allowed|Allow|-|-
permissions|windows|jsmith/C:/PROGRA~2/Citrix/ICACLI~1/HdxRtcEngine.exe|camera|absent|-|1699446591317|1699448586363
permissions|windows|jsmith/C:/PROGRA~2/Citrix/ICACLI~1/HdxRtcEngine.exe|microphone|absent|-|1699446611274|1699448585475
permissions|windows|jsmith/C:/PROGRA~2/Citrix/ICACLI~1/wfica32.exe|microphone|absent|-|1727429693627|1727429699770
permissions|windows|jsmith/C:/Program Files (x86)/Steam/bin/cef/cef.win7x64/steamwebhelper.exe|microphone|absent|-|1672318772321|1672318774367
permissions|windows|jsmith/C:/Program Files (x86)/Steam/steam.exe|microphone|absent|-|1737300284911|1737312589346
permissions|windows|jsmith/C:/Program Files (x86)/ZoomCitrixHDXMediaPlugin/Zoom.exe|camera|absent|-|1732527131754|1732528329384
… 12 of 121 rows shown
[result_status] OK / FULL

macOS — captured: macos macOS 26.6.2 arm64 · bare-metal · 2026-09-29 · euid 501 (ambient FDA, NOT the production agent identity — see leg banner; account names replaced with jsmith) · leg-hash 6f22f62118ed

== action=permissions
permissions|macos|-|camera|absent|-|-|-
permissions|macos|-|microphone|absent|-|-|-
permissions|macos|/Library/PrivilegedHelperTools/com.microsoft.autoupdate.helper|full_disk_access|denied|0|-|-
permissions|macos|/usr/libexec/sshd-keygen-wrapper|full_disk_access|allowed|2|-|-
permissions|macos|com.microsoft.VSCode|full_disk_access|allowed|2|-|-
permissions|macos|com.nordvpn.macos|full_disk_access|denied|0|-|-
permissions|macos|com.spotify.client|full_disk_access|denied|0|-|-
permissions|macos|net.whatsapp.WhatsApp|full_disk_access|denied|0|-|-
permissions|macos|jsmith/com.microsoft.teams2|camera|allowed|2|-|-
permissions|macos|jsmith/org.whispersystems.signal-desktop|camera|allowed|2|-|-
permissions|macos|jsmith/com.microsoft.teams2|microphone|allowed|2|-|-
permissions|macos|jsmith/org.whispersystems.signal-desktop|microphone|allowed|2|-|-
… 12 of 14 rows shown
[result_status] OK / FULL

Linux — captured: linux Debian GNU/Linux 13 (trixie) aarch64 · container · 2026-09-25 · euid 0 · leg-hash 6f22f62118ed

== action=permissions
permissions|linux|org.example.CamApp|camera|allowed|yes|-|-
permissions|linux|org.example.MicApp|microphone|denied|no|-|-
permissions|linux|org.example.MapApp|location|allowed|EXACT,0|-|-
permissions|linux|org.example.NoLocApp|location|denied|NONE,0|-|-
permissions|linux|-|full_disk_access|unsupported|-|-|-
[result_status] OK / FULL

Caveats and known gaps

  1. macOS needs Full Disk Access, and the production identity is unmeasured. Every TCC.db is TCC-protected, so the root LaunchDaemon without Full Disk Access is expected to read every source denied; that identity’s real outcome, how to grant it Full Disk Access (a grant that is agent-wide, so it widens what the whole agent can read) and the verbatim sqlite3_errmsg of a real TCC refusal have not been measured.
  2. macOS reads an immutable snapshot of files the user owns, and coverage is /Users only. Per-user rows are what that user’s own, user-writable file says, so they do not attest what tccd enforces and system-database (MDM) precedence is not modelled; a relocated or network home is not read. Reaching the files is not time-bounded: a network mount is refused up front, but a stacked filesystem over one reports its own local type, a mount that appears after the snapshot is not seen, and a hung open, read or sidecar lstat can block the run with every row lost (rows are buffered to the end), which sample <agent pid> shows under privacy_permissions and restarting the agent is expected to recover.
  3. Windows residuals. RegLoadKeyW loads by path, so a swap between the hop-by-hop checks and the load is caught only by the final-path comparison and the identity re-check, and RegNotifyChangeKeyValue does not report a RegRestoreKey-style whole-key replacement; a busy profile can legitimately read changed_during_read, and the 15 s deadline is cooperative, so the offline-hive lock wait and RegLoadKeyW/RegUnLoadKeyW are not interrupted. The guard’s decisions, tokens and limits live in agents/shared/win_hive_file_guard.hpp and its Win32 shell in agents/shared/win_hive_file_probe.hpp, outside this plugin’s src/: change a token or a limit together with this README.
  4. Windows coverage and data caveats. On a fleet whose profile roots are FSLogix or UPD containers, junctions or other reparse points, or reached through an 8.3 ProfileImagePath, a logged-off profile reads unreadable (hive_path_reparse_ancestor or hive_path_redirected): a fail-closed coverage gap, while a loaded hive is unaffected. Last-used times are usage-class behavioural data and a presence proxy, not a statement of what is currently permitted, and a Windows app_id folds \ to / so a literal # in an executable path is indistinguishable from the encoded separator (issue #5208).
  5. Linux reads only the agent’s own session, and the portal reply has no size bound (#5057). In every shipped deployment (a service user with no session bus) the leg reports unsupported/UNAVAILABLE, which says nothing about interactive users; data appears only inside a desktop session and only for portal-mediated grants, so absent never means no access, and full_disk_access is unsupported. The PermissionStore.Lookup reply is decoded with no row, byte or entry cap, mitigated by default-off, AdminOrApproval, single-target and the 5-second call budget.

Source and tests