Acceso total al disco activado y macOS seguía negándolo: era la firma del bundle
Un desarrollador activa el permiso, la app de macOS sigue sin leer el disco y relanzar no sirve de nada. El sello de firma del bundle apuntaba a un App.framework que ya no estaba.

Helm es una app de macOS que mide el uso de disco, y para eso necesita Acceso total al disco. Su autor activó el permiso en Ajustes ▸ Privacidad y seguridad y la aplicación siguió mostrando el aviso de que le faltaba. Relanzarla no cambiaba nada, y no podía cambiarlo: el problema no estaba en el interruptor ni en TCC, sino en que el bundle llevaba un sello de firma que ya no correspondía con su contenido.
El diagnóstico arranca con una pregunta sencilla: ¿puede macOS saber qué es esta app? Un codesign --verify --deep --strict --verbose=2 sobre el paquete responde que el código anidado está modificado. App.framework, que es donde Flutter deja el Dart compilado, valida por su cuenta, igual que los otros cuatro frameworks del bundle. Lo que falla es la app que los contiene. Contents/_CodeSignature/CodeResources guarda el hash del código que debería haber en cada framework anidado, y ahí figuraba uno, 4ba5cc60… El que había realmente era otro, c7a45621…. El sello y el ejecutable salían de la misma compilación; el framework venía de otra veintidós minutos posterior, que dejó un App.framework nuevo sin volver a sellar la app alrededor.
Por qué el interruptor no hacía nada
TCC no almacena «esta app tiene permiso». Almacena un requisito de código y compara contra él el proceso en marcha. Con una firma de Developer ID ese requisito nombra al equipo del desarrollador; con una firma ad-hoc, que es lo que hay cuando no existe ese certificado, es en la práctica el hash del código. Un bundle cuyo código anidado no encaja con su sello no valida, y el código que no valida no satisface ningún requisito, así que la concesión no queda asociada a nada. El interruptor se ve activado, todas las rutas protegidas siguen denegadas y ningún relanzamiento toca eso. Volver a firmar el paquete y conceder el acceso otra vez lo arregló, y el permiso aguantó un reinicio.
Cómo se queda obsoleto el sello
En un build de Flutter para macOS escriben dos cosas. Una fase de script ejecuta macos_assemble.sh embed, que delega en xcode_backend.dart de Flutter: copia el App.framework recién compilado a Contents/Frameworks y lo firma. Después Xcode firma la app, que es lo que escribe el sello. En una compilación limpia los timestamps caen con un segundo de diferencia, primero el framework. No es, por tanto, lo que hace un build incremental, que es lo que el autor supuso al principio y anotó en un changelog. Lo que reprodujo el fallo fue un crash: tras un cambio en Swift, la siguiente compilación murió con «unexpected service error: The Xcode build system has crashed. Build again to continue.»; al repetirla con otro cambio de Dart, el build terminó con éxito, ejecutó el embed de Flutter y se saltó la firma de la app. El mismo patrón del build que se publicó.
El autor avisa de que no puede demostrar que la versión distribuida pasara por un crash: solo conservaba las últimas líneas de cada log, justo lo que un crash anterior se lleva por delante. Un build interrumpido es el sospechoso obvio y no lo ha probado. Reproducido en Flutter 3.47.3, Xcode 26.6 y macOS 26.6.2; el build publicado iba con Flutter 3.47.1.
Por qué no lo detectó nada
El script de publicación no verificaba nada en la ruta que sí se ejecutaba. La firma y la comprobación vivían en la misma rama, la que solo entra si hay un $SIGN_ID con certificado de Developer ID. La app es gratuita, así que esa rama nunca se daba. El camino que siempre corría no firmaba ni comprobaba, y el DMG se llevaba lo que flutter build hubiera dejado. Tampoco hacía falta --deep para cazarlo: cualquier verificación del paquete lo detecta, con flags o sin ellos, y el código que devuelve Security es -67021.
La corrección pasa por firmar y verificar en todas las rutas, de dentro hacia fuera (primero el código anidado, luego el bundle que lo contiene), y por hacer que una verificación fallida tumbe la publicación en lugar de dejarla pasar. Para quien distribuya apps de macOS, sobre todo con firma ad-hoc, la lección es barata: un codesign --verify en el pipeline y que su salida decida si el artefacto sale.


