Что такое TCC и зачем он вообще
TCC - это Transparency, Consent and Control (прозрачность, согласие и контроль). Идея простая: ни одно приложение не должно молча лезть к твоей камере, микрофону, контактам или личным файлам. Когда программа впервые пытается дотянуться до защищённого ресурса, система перехватывает запрос и показывает тот самый диалог "Приложение X запрашивает доступ к ...". Твоё решение (разрешить/запретить) сохраняется, и больше система не спрашивает - пока ты сам не сбросишь.
Это центральный кусок модели конфиденциальности macOS (privacy mac). Управляется демоном tccd: один работает на уровне системы, другой - в твоём пользовательском сеансе. Когда процесс делает системный вызов к защищённому API, ядро и фреймворки спрашивают у tccd: "этому бинарю можно?". tccd смотрит в свою базу и либо пускает, либо инициирует диалог согласия, либо режет молча (если профилем задан запрет).
Что именно охраняет TCC mac:
- Камера и Микрофон - доступ к захвату видео/звука;
- Геолокация (Службы геолокации) - где находится Mac;
- Контакты, Календари, Напоминания - персональные данные из системных приложений;
- Фото - доступ к медиатеке Фото;
- Запись экрана (Screen Recording) - снимать экран и окна других программ;
- Универсальный доступ (Accessibility) - программно управлять интерфейсом: жать кнопки, двигать мышь, читать содержимое окон;
- Полный доступ к диску (Full Disk Access) - читать защищённые зоны: Почту, Сообщения, историю Safari, чужие ~/Library, базы самого TCC;
- Папки Документы, Рабочий стол, Загрузки - так называемый pop-up на пользовательские папки;
- Автоматизация (Automation) - управлять другими приложениями через Apple Events (AppleScript, osascript).

Где это лежит: база TCC.db и две зоны ответственности
Все решения хранятся в SQLite-базах. Их две, и это принципиально:
Код: Выделить всё
~/Library/Application Support/com.apple.TCC/TCC.db # пользовательская
/Library/Application Support/com.apple.TCC/TCC.db # системная
Залезть туда напрямую через sqlite3 не выйдет, даже под root, если у твоего терминала нет Full Disk Access - сама база TCC защищена TCC, такая вот рекурсия. Глянуть структуру можно, когда доступ есть:
Код: Выделить всё
$ sudo sqlite3 "/Library/Application Support/com.apple.TCC/TCC.db" \
"SELECT service, client, auth_value FROM access LIMIT 5;"
kTCCServiceSystemPolicyAllFiles|com.apple.Terminal|2
kTCCServiceScreenCapture|us.zoom.xos|2
kTCCServiceAccessibility|com.googlecode.iterm2|2
Важно: не правь TCC.db руками через sqlite3 в проде. Apple это явно не поддерживает, формат меняется между релизами, а на системной базе SIP всё равно не даст записать. Для управления есть штатные инструменты - tccutil и профили.
Управление через GUI и сброс через tccutil
Глазами всё это живёт в Системные настройки -> Конфиденциальность и безопасность (в macOS 26 Tahoe это System Settings, не старые Preferences). Там список категорий, в каждой - тумблеры по приложениям. Это основное место, где обычный человек раздаёт разрешения mac.
Но рулить мышкой - не наш метод. Из терминала есть tccutil. У него по сути одна боевая команда - reset, она стирает уже принятое решение, чтобы система спросила заново при следующем обращении. Сам выдать разрешение tccutil НЕ может (это by design - иначе грош цена согласию пользователя).
Синтаксис: tccutil reset <Сервис> [bundle_id]. Имена сервисов чувствительны к регистру и идут без префикса kTCCService.
Код: Выделить всё
# сбросить Accessibility для всех приложений
$ sudo tccutil reset Accessibility
# сбросить только запись экрана для Zoom
$ sudo tccutil reset ScreenCapture us.zoom.xos
# сбросить Full Disk Access для конкретного приложения
$ sudo tccutil reset SystemPolicyAllFiles com.microsoft.VSCode
# снести вообще все решения для одного приложения
$ tccutil reset All com.apple.Terminal
# ядерный вариант - сбросить всё и для всех (не делай так на рабочей машине)
$ sudo tccutil reset All
Типичный сценарий: приложение глючит с разрешениями, диалог не появляется или ты случайно нажал "Запретить". Лечится так:
Код: Выделить всё
$ killall "Google Chrome" # сначала прибить процесс
$ tccutil reset Camera com.google.Chrome
$ tccutil reset Microphone com.google.Chrome
Почему скрипты и Терминал требуют Full Disk Access
Вот корень самой частой боли. Ты под sudo, ты root, но cat чужого ~/Library/Mail падает с "Operation not permitted". POSIX-права тут ни при чём - root по ним всё может. Режет именно TCC: чтение защищённых зон требует Full Disk Access, и проверка идёт не по UID, а по тому, КАКОЙ бинарь инициировал доступ.
А инициатор - это твой Терминал (или iTerm2, или VS Code, из которого ты запускаешь скрипт). Скрипт сам по себе не имеет идентичности в глазах TCC; он наследует права хост-приложения. Поэтому Full Disk Access выдаётся не скрипту, а Терминалу: Системные настройки -> Конфиденциальность и безопасность -> Полный доступ к диску -> добавить Терминал, тумблер включить. После этого любой скрипт, запущенный из этого Терминала, видит защищённые файлы.
Проверить на пальцах:
Код: Выделить всё
$ ls ~/Library/Mail
ls: : Operation not permitted # FDA нет
# ... выдаём Терминалу Full Disk Access, перезапускаем его ...
$ ls ~/Library/Mail
V10 V11 # теперь видно
Грабли автоматизации: launchd + TCC
Самое больное место - фоновые задачи. Ты делаешь LaunchAgent/LaunchDaemon (launchd, не cron - в macOS планировщик это launchd), который по расписанию скриншотит экран или дёргает другое приложение через osascript. И оно не работает, причём молча.
Почему. TCC-диалог согласия требует пользовательского интерфейса и интерактивной сессии. У LaunchDaemon её нет вообще (он в системном контексте, до логина). У LaunchAgent она есть, но если задача стрельнула, когда никто не смотрит на экран, диалог либо не покажется, либо провисит и отвалится по таймауту. Результат - доступ не выдан, задача упала.
Второй слой граблей - наследование идентичности. Когда launchd запускает /bin/zsh твой скрипт, TCC-клиентом считается процесс, который реально вызывает защищённый API. Для Apple Events это будет интерпретатор (osascript), и согласие на Automation привяжется к нему, а не к Терминалу. Поэтому разрешение, выданное вручную в интерактивной сессии, может не подхватиться в фоне - другой клиент, другая запись в базе.
Что делать на практике:
- Выдай нужные разрешения заранее, в интерактивной сессии, тому самому бинарю, который потом будет работать в фоне (например, прогони osascript из Терминала один раз, прими диалог Automation).
- Для парка машин - не надейся на диалоги вообще, раздавай разрешения профилем PPPC (см. ниже). Диалог в фоне ловить бесполезно.
- Запускай агент в правильном контексте: задаче с GUI нужен LaunchAgent в сессии залогиненного юзера (gui/<uid>), а не LaunchDaemon.
- Помни: AppleEvents/Automation чаще всего и есть та категория, которую душит фон. Проверяй журнал.
Код: Выделить всё
$ log stream --predicate 'subsystem == "com.apple.TCC"' --info
# дёрни задачу в другом окне и читай, что решает tccd:
... tccd: REQUESTING kTCCServiceAppleEvents from osascript
... tccd: Prompting policy for ... DENIED (no interactive session)
Профили PPPC для парка и роль подписи
В компании раздавать разрешения по машинам руками - тупик. Для этого есть PPPC - Privacy Preferences Policy Control. Это конфигурационный профиль (тип payload com.apple.TCC.configuration-profile-policy), который через MDM прилетает на устройство и заранее проставляет в системной TCC-базе нужные решения. Никаких диалогов - всё уже решено политикой. В 2026 это часть стандарта Declarative Device Management (DDM).
Ключевой момент про идентичность: профиль PPPC привязывает разрешение не к имени приложения, а к его подписи. В payload указываются bundle identifier и code requirement - криптографическое требование к подписи бинаря. Поэтому подпись и TCC связаны намертво: если приложение пересобрали с другим сертификатом или сломали подпись, code requirement перестаёт совпадать и профиль молча не сработает.
Получить code requirement для своего инструмента:
Код: Выделить всё
$ codesign -dr - /Applications/MyAgent.app
designated => identifier "com.acme.MyAgent" and anchor apple generic and \
certificate leaf[subject.OU] = "ABCDE12345"
Чего PPPC НЕ умеет: профилем нельзя ВЫДАТЬ доступ к Камере, Микрофону и Записи экрана. По этим трём категориям MDM может только запретить или оставить решение пользователю - финальное "да" всё равно жмёт человек. Зато Accessibility, Full Disk Access (SystemPolicyAllFiles) и Apple Events профиль раздаёт полноценно, без участия юзера, что и закрывает боль фоновой автоматизации в парке.
Мини-лаба: пощупать TCC руками
Делай на личной тестовой машине, не на проде.
- 1. Без Full Disk Access у Терминала выполни ls ~/Library/Mail и поймай Operation not permitted.
- 2. Системные настройки -> Конфиденциальность и безопасность -> Полный доступ к диску -> добавь Терминал, включи тумблер, полностью перезапусти Терминал (Cmd+Q). Повтори ls - теперь видно.
- 3. Узнай бандл-ид своего браузера: osascript -e 'id of app "Safari"'.
- 4. Запусти log stream --predicate 'subsystem == "com.apple.TCC"' --info в одном окне, в другом - osascript -e 'tell app "Finder" to get name of every window'. Прими диалог Automation и найди в логе строку REQUESTING kTCCServiceAppleEvents.
- 5. Сбрось это решение: tccutil reset AppleEvents и убедись, что при следующем запуске диалог появился снова.
- 1. Почему скрипт, запущенный под sudo, всё равно не может прочитать чужую ~/Library/Mail, и кому в итоге выдаётся Full Disk Access?
- 2. Чем системная TCC.db отличается от пользовательской, и какие категории живут в каждой?
- 3. Что делает tccutil reset ScreenCapture us.zoom.xos, и почему перед сбросом приложение надо прибить через killall?
- 4. Почему фоновая launchd-задача с Apple Events часто молча падает, и как это лечится в парке через PPPC?
TCC - это не каприз, а сознательный барьер: согласие пользователя на доступ к камере, файлам и управлению другими приложениями стоит выше прав root. Запомни три вещи. Первое: защищённые файлы открывает не sudo, а Full Disk Access у приложения-хоста твоего скрипта. Второе: в фоне диалоги не работают - либо выдавай разрешения заранее тому же бинарю, либо раздавай профилем PPPC. Третье: PPPC держится на подписи приложения через code requirement, и Камеру с Микрофоном профилем не выдашь. Держи под рукой tccutil reset для отладки и log stream по subsystem com.apple.TCC, когда что-то режется молча.