Skip to content

Commit 2eaeabf

Browse files
committed
[install] Stop resetting TCC grants on a Developer ID rebuild
A changed cdhash only invalidates a grant when the cdhash IS the designated requirement, which is the ad-hoc case the check was written for. A Developer ID signature's requirement is identifier plus Team ID and is stable across rebuilds, so the grant survives one. Resetting regardless meant re-granting Accessibility after every single install — the exact cost that signing properly is supposed to remove. Observed doing it twice within an hour of the switch to a Developer ID build, each time telling the user that ad-hoc signing was the reason while the bundle carried a full chain and a secure timestamp. Solves: Accessibility re-prompt on every reinstall of a signed build Tests: the ad-hoc check must precede and return before the tccutil call; verified live — a forced cdhash mismatch on the signed bundle now reports the grants carry over instead of clearing them
1 parent 93be7cf commit 2eaeabf

2 files changed

Lines changed: 42 additions & 0 deletions

File tree

install-client.sh

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,20 @@ reset_stale_grants_on_identity_change() {
328328
# First install, or the same binary reinstalled: nothing to invalidate.
329329
[[ -n "$old_hash" && "$old_hash" != "$new_hash" ]] || return 0
330330

331+
# A changed cdhash only invalidates the grant when the DESIGNATED REQUIREMENT
332+
# is the cdhash — which is the ad-hoc case. A Developer ID signature's
333+
# requirement is identifier + Team ID, so it is stable across rebuilds and the
334+
# grant survives one. Resetting anyway makes the user re-grant Accessibility
335+
# after every single install, which is precisely the cost that signing
336+
# properly is supposed to remove.
337+
local sig_info
338+
sig_info="$(codesign -dvvv "$APP_DST" 2>&1 || true)"
339+
if ! grep -q '^Signature=adhoc' <<<"$sig_info"; then
340+
log "the agent binary changed, but it is signed with a Developer ID —"
341+
log "its designated requirement is stable, so the grants carry over."
342+
return 0
343+
fi
344+
331345
warn "the agent binary changed (${old_hash:0:12}… -> ${new_hash:0:12}…)."
332346
warn "Ad-hoc signing ties TCC grants to that hash, so the Accessibility grant"
333347
warn "no longer applies — and macOS would still show its toggle switched ON."

tests/test_app_dir_resolution.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,3 +109,31 @@ def test_uninstall_does_not_delete_a_brew_owned_bundle(self):
109109
guard = uninstall.index('"$APP_MANAGED" -eq 1')
110110
removal = uninstall.index('rm -rf "$APP_DST"')
111111
assert guard < removal, "the removal must be guarded by the managed check"
112+
113+
114+
class TestGrantResetIsSignatureAware:
115+
"""A cdhash change only invalidates TCC when the cdhash IS the requirement.
116+
117+
That is the ad-hoc case. A Developer ID signature's designated requirement
118+
is identifier + Team ID, stable across rebuilds, so the grant survives one.
119+
Resetting regardless made the user re-grant Accessibility after every
120+
install — exactly the cost that signing properly exists to remove, and it
121+
was observed doing so on 2026-08-05 immediately after the switch to a
122+
Developer ID build.
123+
"""
124+
125+
def test_the_reset_is_skipped_for_a_developer_id_signature(self):
126+
code = CLIENT.read_text()
127+
# Match the DEFINITION, not the earlier comment that names the
128+
# function — indexing on the bare name lands in a comment and slices
129+
# a completely different function's body.
130+
fn = code[code.index("reset_stale_grants_on_identity_change() {"):]
131+
fn = fn[:fn.index("\n}\n")]
132+
adhoc_check = fn.index("Signature=adhoc")
133+
reset_call = fn.index("tccutil reset Accessibility")
134+
assert adhoc_check < reset_call, (
135+
"the ad-hoc check must gate the reset, not follow it"
136+
)
137+
assert "return 0" in fn[adhoc_check:reset_call], (
138+
"a non-ad-hoc signature must return before resetting anything"
139+
)

0 commit comments

Comments
 (0)