AI 사용기
AI 활용기: Homebrew 파이썬을 올렸더니 무인 자동화 권한이 조용히 풀렸다
macOS 예약 자동화(launchd)에 준 화면기록·손쉬운 사용 권한이 Homebrew 파이썬 업그레이드 한 번에 풀린 이유와, 서명된 런처 앱을 자식 프로세스로 끼워 넣어 고친 방법. Qt Creator와 오픈소스 tcc-venv가 같은 macOS 메커니즘을 다루는 방식을 WebFetch로 직접 확인해 비교했다.
launchd·cron 같은 macOS 무인 자동화에 화면기록·손쉬운 사용·로컬 네트워크 권한을 준 사람에게, 그 권한이 어떤 프로세스에 붙는지와 왜 조용히 풀리는지를 실제 사례로 보여준다.
macOS의 TCC 권한은 실행 파일 자체가 아니라 그 실행을 시작한 '책임 프로세스(responsible process)'의 코드 서명 신원에 붙는다. launchd가 Homebrew 파이썬을 직접 띄우면 책임 프로세스가 파이썬 바이너리가 되고, 파이썬 버전을 올리면(2026-09-28 3.14.5→3.14.7) 그 신원이 바뀌어 권한이 조용히 풀린다. 해결은 서명된 런처 앱을 launchd가 띄우고, 그 앱이 파이썬을 자식 프로세스로 실행하는 것 — exec로 바꿔치기하면 책임이 다시 파이썬으로 넘어가므로 반드시 자식으로 남겨야 한다. 이 '책임을 유지하는' 방향은 오픈소스 도구 tcc-venv가 쓰는 것과 같은 구조(고정 신원의 서명된 C 런처)다. 반대로 '책임을 떼어내는' 비공개 API `responsibility_spawnattrs_setdisclaim()`은 Qt Creator가 2022년 블로그에서 밝힌 것과 같은 함수이며, 이번 자동화에서는 로컬 네트워크 호출 헬퍼에 이 반대 방향을 따로 적용했다.
launchd·cron으로 macOS 자동화를 돌리면서 화면기록·손쉬운 사용·자동화 권한을 주고 있다면, 그 권한을 요청하는 게 실제로 어느 프로세스인지(파이썬/노드 같은 인터프리터 바이너리인지, 서명된 래퍼 앱인지) `codesign -dv <실행 파일 경로>`로 서명 여부·서명 신원을 먼저 확인하고, macOS '개인정보 보호 및 보안' 목록에 어떤 이름으로 항목이 뜨는지 대조한다.
먼저 답하면
launchd로 무인 자동화를 돌리다가 화면기록·손쉬운 사용 권한이 어느 날 갑자기 풀렸다. 원인은 Homebrew 파이썬 업그레이드였다 — macOS는 권한을 파이썬 바이너리 자체에 붙이고 있었고, 버전이 바뀌자 그 신원도 바뀌었다. 서명된 런처 앱을 하나 끼워 넣어 고쳤는데, 오픈소스 도구 tcc-venv도 같은 구조를 쓰고 있었고, Qt Creator는 정반대 문제(자신이 남의 책임 프로세스가 되는 것)를 같은 비공개 API로 풀고 있었다.
재현 방법
사람이 지켜보지 않는 예약 자동화를 launchd로 돌리고 있었다. 이 자동화는 화면 기록, 손쉬운 사용(접근성), Chrome 자동화, 로컬 네트워크 접근이 전부 필요한 작업이라 macOS 설정에서 해당 권한을 파이썬 실행 파일에 허용해뒀다. 문제는 Homebrew가 파이썬을 3.14.5에서 3.14.7로 올린 날(2026-09-28) 그 권한들이 아무 안내 없이 사라졌다는 것이다. 원인을 되짚어보면, launchd가 직접 띄우는 프로세스가 파이썬이면 macOS의 TCC(Transparency, Consent, and Control)는 그 파이썬 바이너리의 경로와 코드 서명을 권한의 기준으로 삼는다. Homebrew 파이썬은 버전이 바뀌면 실행 파일 경로와 서명 신원도 함께 바뀌므로, TCC 입장에서는 “다른 앱”이 요청하는 것과 같아져 기존 허용이 더 이상 적용되지 않았다.
검증 결과
고친 방법은 launchd가 직접 파이썬을 띄우지 않고, 서명된 런처 앱을 대신 띄우게 하는 것이다. 이 런처는 posix_spawn으로 파이썬을 자식 프로세스로 실행하고 종료까지 기다린다 — exec로 파이썬을 덮어씌우지 않는 게 핵심이다. exec를 쓰면 프로세스 이미지 자체가 파이썬 코드로 바뀌면서 책임 프로세스가 다시 파이썬이 돼버린다. posix_spawn으로 자식을 띄운 채 부모가 살아있으면, macOS는 이 요청 체인의 책임을 부모(런처)에게 돌린다. 런처는 Apple Development 인증서로 서명했고, 서명 신원이 같으면 다시 빌드해도 macOS가 부여한 권한이 유지된다는 것도 확인했다.
로컬 네트워크 권한은 별도로 걸렸다. 이 권한은 시스템 설정 목록에 수동으로 추가할 수 없고, 허용 여부를 묻는 팝업을 한 번 놓치면 다시 뜨지 않는다는 걸 실측으로 확인했다. 이 때문에 LAN 호출(로컬 curl 요청)만 별도로 처리하는 헬퍼를 따로 만들었는데, 여기서는 반대로 “책임을 떼어내는” 비공개 API responsibility_spawnattrs_setdisclaim()을 써서 헬퍼가 아니라 그 헬퍼가 띄우는 Apple 서명 도구(/usr/bin/curl) 자신의 권한으로 요청이 나가도록 만들었다. 자식에게 책임을 넘길 때와 자식의 책임을 떼어낼 때 쓰는 도구가 다르다는 걸 이 과정에서 알게 됐다.
한계와 다음 개선
같은 macOS 메커니즘을 다루는 사례 두 개를 찾았는데, 방향이 정확히 반대였다.
첫 번째는 macOS에서 uv/venv 파이썬 인터프리터를 쓰는 문제를 다루는 오픈소스 도구 tcc-venv다(저장소 생성일 2026-06-05, 만든 지 몇 달 안 된 도구다). README는 이 문제를 이렇게 요약한다: “Grant that binary Full Disk Access, then run uv python upgrade…and the path changes. To TCC it’s now a different app: the grant silently stops applying.” 겪은 것과 완전히 같은 증상(경로가 바뀌는 인터프리터, 조용히 풀리는 권한)이고, 해법도 “tcc-venv fixes both by interposing a tiny signed C launcher with a stable identity that you grant once“다 — 이번에 만든 메인 런처 앱과 같이 책임을 유지하는 방향이다.
두 번째는 Qt 블로그의 “The Curious Case of the Responsible Process”(2022-02-04 게시)로, 반대 문제를 다룬다. Qt Creator가 사용자 애플리케이션을 실행하면 Qt Creator 자신이 책임 프로세스가 되어, 사용자 앱이 요청해야 할 권한이 엉뚱하게 Qt Creator 이름으로 뜨는 문제였다. 이 글은 “권한은 하위 프로세스에 상속되며, 보호된 자원에 접근할 때 TCC는 책임 있는 프로세스를 파악하고 그를 기반으로 결과를 저장한다”고 설명하고, 해법으로 LLVM/LLDB가 이미 쓰던 비공개 API responsibility_spawnattrs_setdisclaim()을 가져와 Qt Creator 7에 넣었다고 밝힌다. 이건 이번에 만든 메인 런처가 아니라, 로컬 네트워크 호출 헬퍼(hub-disclaim)에 쓴 것과 같은 함수이자 같은 방향(책임을 떼어내는 쪽)이다.
정리하면 같은 API 계열을 놓고 두 가지 반대 용도가 있다 — 자식에게 책임을 유지시켜 권한을 한 곳에 고정하고 싶으면(메인 런처, tcc-venv) 그냥 posix_spawn으로 자식을 띄우면 되고, 자식이 부모의 책임을 물려받지 않게 하고 싶으면(로컬 curl 헬퍼, Qt Creator) responsibility_spawnattrs_setdisclaim()으로 명시적으로 떼어내야 한다. 로컬 네트워크 권한 팝업을 한 번 놓치면 다시 뜨지 않는 문제는 이 떼어내기 방향으로 우회했다 — 헬퍼가 아니라 헬퍼가 띄우는 Apple 서명 도구(/usr/bin/curl) 자신의 이름으로 권한을 요청하게 만들어, curl 자체가 이미 가진(또는 별도로 받는) 권한을 쓰게 한 것이다. 다만 이 방식이 macOS 버전이 바뀌어도 계속 통할지, 그리고 posix_spawn 자식이 아니라 애초에 팝업을 완전히 재노출시키는 방법이 따로 있는지는 이번 조사 범위 밖이다.