Build1 publisher3 min readPublished
macOS refused Full Disk Access because the app's seal recorded a different App.framework
The switch read on and every protected path stayed refused, because macOS validates a signature before it honours a grant, and this bundle's Flutter framework had been replaced after the app was signed.
The Engineer · Build desk

What happened
- Full Disk Access was switched on in System Settings and the app went on reporting limited access, with its own banner advising a relaunch that changed nothing.
- Running codesign --verify --deep --strict on the installed app returned "nested code is modified or invalid" and named Contents/Frameworks/App.framework as the modified file.
- The bundle's own seal in Contents/_CodeSignature/CodeResources recorded one hash for the nested App.framework, and the copy on disk had a different one.
- The state was reproduced once after the Xcode build system crashed: the build that followed ran Flutter's embed step, skipped the app's signing, and reported success.
- Re-signing the bundle and granting access again made the app report Full Disk Access on, and the grant survived a restart.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- constraint Because TCC re-evaluates a code requirement against the running app, the Privacy & Security pane cannot show this fault at all, and every hour spent in Settings is spent on the wrong layer.
- capability One codesign --verify --strict on the installed bundle separates a permissions problem from a signing one before any toggling starts, and it does not need --deep to catch this case.
- decision Nothing in the build output distinguishes the bad artifact from a good one, so anyone shipping Flutter on macOS has to decide whether verification runs on every release build before it leaves the machine.
- exposure TCC sits behind every switch in Privacy & Security, so a bundle that fails validation loses whatever a user granted it, not only disk access.
TCC stores a code requirement and evaluates it against the running app every time [8]. There is no row saying this app is allowed. The evaluation has two halves: the code has to validate, and it has to match the requirement. A bundle whose nested code disagrees with its own seal fails the first half, and code that does not validate cannot satisfy any requirement [10]. The grant attaches to nothing, and every protected path stays refused while the switch still reads on [10].
Two things write a Flutter macOS bundle. A script phase runs `macos_assemble.sh embed`, which hands off to Flutter's `xcode_backend.dart`; that copies the freshly compiled App.framework into Contents/Frameworks and signs it. Xcode then signs the app, and that step writes the seal [12]. In two good builds the framework and the seal were stamped a second apart, framework first: 23:19:31 and 23:19:32 on a clean build, then 23:20:05 and 23:20:06 on a rebuild after one Dart change [13]. The shipped bundle had its seal and its main executable at 15:08:47 and its App.framework binary at 15:30:38, 21 minutes and 51 seconds later [6][7]. The framework verified correctly on its own, as did the other four frameworks in the bundle [4].
It took two wrong explanations to get there. Incremental builds went into a changelog before being tested, and a clean build seals correctly, as does a Dart-only rebuild straight after it [14]. The other theory was that only `--deep` compares nested code against the outer seal, so a plain verify would have passed the bundle [22]. The check that caught the reproduced case ran as `codesign --verify --strict`, with no `--deep` [16].
A crash reproduced it. On a copy of the project a Swift change built fine, and the next build, a one-line Dart change, died with "error: unexpected service error: The Xcode build system has crashed. Build again to continue." [15]. Building again with another Dart-only change produced "Built build/macos/Build/Products/Release/Helm.app (47.1MB)" and a verify failure, the seal recording dda6dabc96dd against afaf15310e15 on disk [16]. The seal was stamped 23:22:42 and the framework 23:23:28, a gap of 46 seconds [17][18].
Whether the shipped build went through a crash is not established. The developer kept only the last few lines of each build log, and a crash in an earlier build is exactly what that discards. The developer calls an interrupted build the obvious suspect and has not tested it, and which other failures leave the same state is unknown [20]. The reproduction ran on Flutter 3.47.3, Xcode 26.6 and macOS 26.6.2, and the shipped build was on Flutter 3.47.1 [21].
For this to be your failure, something other than the step that writes the outer seal has to sign nested code inside the bundle, which is how Flutter's macOS build is arranged [12]. The build that leaves the bundle in that state also has to exit reporting success [19]. What the requirement contains depends on how you sign: a Developer ID signature makes it name the developer's team, and an ad-hoc signature, which is what you have without one, makes it effectively the hash of the code [9].
What to watch
- Whether an interrupted or cancelled build, which the developer has not tested, leaves the same seal-and-framework mismatch.
- Whether a full build log from a shipped artifact ever ties this state to an Xcode build system crash.
- Whether Flutter's embed step or Xcode's build system changes so that a skipped app signing no longer exits reporting success.