English | 中文
On-device switch for the iPhone call-recording announcement — no computer needed.
A minimal SwiftUI app built on the same AirTraffic sandbox escape as
AirCard-iOS. It talks to the phone's own internal
services over a LocalDevVPN loopback tunnel and swaps the three audio files iOS plays when call
recording starts and stops.
Three actions, available on screen and from Shortcuts:
| Action | What it does |
|---|---|
| Silence the announcements | writes the bundled silent files over the three announcement files |
| Check | reads them back and reports silenced / original / unknown build |
| Restore the originals | writes the bundled originals back |
The interface is English by default, with a Simplified Chinese translation — see Languages.
AirCard-iOS's shipped AirliftFFI can only write. The escape itself can only move objects
around, so reading a file means:
- relocate the symlink into
Media, - ask ATAirlock to move the target file out to a
Mediapath (that's the same trick the reference PoC uses to verify its canary), - read it over AFC,
- write it straight back.
This repository adds al_exploit_read_file_hash() to the Rust core for step 1–4 and returns the
file's size + SHA-256 rather than raw bytes, so Swift never has to handle a binary buffer over
the C ABI. The write-back reuses the proven writer, so a read never leaves the device short a file.
The original write path is untouched on purpose.
- iOS 18.0 – 27.2 beta 2. Apple patched the underlying
airliftexploit in iOS 27.2 beta 3 — do not update past beta 2 or this stops working. - LocalDevVPN running in loopback mode (the app dials
10.7.0.1by default, then falls back through the other tunnel gateways). Never point it at127.0.0.1— on iOS that makesremotepairingdevicedreject the control channel and enter a drop state where every later connection fails, recoverable only by toggling Developer Mode off and on. - A pairing record. Either import one (AirDrop / Files → the app's Documents, as
airlift_pairing.plist), or use the built-in on-device pairing: Start pairing → note the PIN → Settings › Privacy & Security › Developer Mode › Pair with AirliftSilence. Allow notifications when asked: the PIN is also delivered as a notification, because the moment it appears you are inside Settings typing it and the app's own screen is out of sight. If Settings never offers a Pair with App entry, the broadcast never started: check Settings › AirliftSilence › Local Network. The app reports that instead of claiming to advertise. - The official Apple Books app installed and opened at least once —
com.apple.atcrefuses the sync otherwise, which is the #1 cause of "ReadyForSync not observed".
Once a record exists, Export pairing file hands it to the share sheet — AirDrop, Save to Files, and so on. Worth doing: re-signing a sideloaded build gives it a fresh container, so the record does not survive a reinstall.
Getting one back in has three routes, in rough order of reliability:
- Import pairing file — an in-app file picker. Works with the
.plist,.mobiledevicepairingand.mobilepairingnames the various sideloaders use. - Drop the file into the app's Documents folder (file sharing is on), named
airlift_pairing.plist. - Open a
.plistwith AirliftSilence from the share sheet (Open in AirliftSilence) — this goes through the app delegate, so it works even when the app is not running. Files with an undeclared extension (.mobiledevicepairing,.mobilepairing) are offered too.
The Rust core has to be compiled for iOS, so the build needs macOS. If you don't have a Mac,
let GitHub build it — .github/workflows/build-ios.yml runs on a macOS runner and uploads an
unsigned IPA (macOS runners are free for public repositories):
- Push this repository to GitHub.
- Actions → Build unsigned IPA → Run workflow.
- Download the
AirliftSilence-unsigned-ipaartifact.
On a Mac with Xcode:
brew install xcodegen
./build-ios.sh # rust-core -> AirliftFFI.xcframework
./build-ipa.sh Release # -> build/AirliftSilence.ipa (unsigned)The IPA is deliberately unsigned — sign it with whatever you normally use (AltStore, SideStore, Sideloadly, TrollStore, …).
Important
SilenceApp.swift force-references ALGetGrappaToken. The Rust side looks that symbol up with
dlsym, so without the hard reference the linker may drop it and every sync then fails with
"Grappa session could not be established" on iOS 27.0.1+. Keep that line.
All three recordings live in one directory, so a single write covers them:
/var/mobile/Library/CallServices/Greetings/default/StartDisclosureWithTone.m4a
/var/mobile/Library/CallServices/Greetings/default/StopDisclosure.caf
/var/mobile/Library/CallServices/Greetings/default/StartDisclosure.caf
The bundled silent-* files are plain ffmpeg anullsrc silence, matched to the duration and codec
of the versions shipping today (AAC 1.672 s, Opus 1.940 s, Opus 1.280 s), so playback timing is
unchanged. Greeting.caf and GreetingWithTone.m4a sit in the same folder and are deliberately
left alone — those are voicemail greetings, not recording announcements.
The original-* files hold Apple's current recordings so Restore the originals has something to
put back.
Those two are Apple's audio, bundled for your own device only — don't redistribute this repository
with them if that matters to you.
Both sets sit in one folder with a silent- / original- prefix, because XcodeGen flattens
directory contents into the bundle root and the two files share a name; the staging step copies each
one to its real filename before it is written to the device.
The app ships three AppIntents, so it shows up in the Shortcuts app as an action of its own:
| Shortcut action | Notes |
|---|---|
| Call-recording announcement | one step with an Action menu: Check / Silence / Restore |
| Silence the call-recording announcements | no parameters — the one to put on the Home Screen |
| Restore the call-recording announcements | no parameters |
Each one returns the outcome as text (Call-recording announcements: Silenced, …) so a Shortcut can
branch on it or show it in a notification. Shortcuts are queued in the app: a run that arrives while
the on-screen check is still going waits for it, because the device only tolerates one session at a
time.
The intents open the app when they run. That is deliberate — a run is a long device-side session and an iOS background intent gets cut off after a few seconds, which would tear the tunnel down mid-transfer. Siri phrases are provided in both languages, e.g. "AirliftSilence 去掉通话录音提示音".
The app also answers a URL scheme, for anything that can only open a URL (an Open URLs action, a launcher, a bookmark):
airliftsilence://check
airliftsilence://silence
airliftsilence://restore
iOS can undo this without telling you. The announcements are generated on the device by Siri TTS, and the daemon regenerates them whenever the configuration it expects changes: Apple ships a new voice or audio version (that is what happened on the test device, with nobody touching the phone), an iOS update rolls around, or you switch the system language. When that happens the sound is back, and Check reports Original (audible).
Two things are worth knowing before trying to fight it:
- Nothing can stop the rewrite. Every obvious block was tried on a device and every one of them
fails: an empty directory where the file should be, a read-only file, an immutable file, a read-only
directory. File modes do reach the device, but directory modes and file flags do not, so the daemon
can always replace the entry instead of writing through it.
docs/static-analysis.mdhas the measurements. - Blocking it breaks recording. The daemon refuses to record when it cannot produce the disclosure — even with a perfectly valid silent file sitting at the path. A block that holds is not a win, it is a phone that will not record. Don't.
So the answer is to notice and re-apply, which is either one tap or none at all:
- Shortcuts → Automation → + → Charger (or Time of Day, say 4 a.m.) → Run Immediately, with Ask Before Running turned off.
- Action: AirliftSilence › Silence the call-recording announcements.
- Done — a revert now repairs itself within a day.
The run takes 30–60 seconds and brings the app to the front, which is why a charger or an early-morning trigger is the pleasant choice. If you would rather stay in control, skip the automation: Check runs on every launch and turns red with Original (audible) when the announcement is back.
English is the development language; Simplified Chinese ships as zh-Hans. Strings are not in a
String Catalog — the .strings files are hand-written and the keys are the English strings exactly
as they appear in the Swift sources:
ios-app/en.lproj/Localizable.strings # English (development language)
ios-app/zh-Hans.lproj/Localizable.strings # Simplified Chinese
ios-app/{en,zh-Hans}.lproj/InfoPlist.strings # the Local Network permission prompt
To add a language: copy en.lproj to <language>.lproj, translate the values (keep the keys and
their %@ placeholders in the same order), add the language to CFBundleLocalizations in
Info.plist, and re-run xcodegen generate. en.lproj has to stay in place: XcodeGen only creates
localisation variant groups when it finds the development-language folder.
Nothing may drift silently:
python3 tools/check-strings.py # also runs in CIIt checks that every key still exists in the sources (after normalising \(..) to %@), that en
and zh-Hans hold the same keys in the same order with matching placeholder counts, that
InfoPlist.strings only overrides keys the plist defines, and reports any source string that looks
like UI text but has no translation.
- iOS can undo this, and it cannot be stopped. The announcements are generated on the device by
Siri TTS, and the daemon regenerates them when the configuration it expects changes — an Apple voice
or audio update (that is what happened on the test device, with no action from the owner), an iOS
update, or a language switch. In between, the silence is stable: ten days including calls and a
reboot is normal, and it is not a boot-time check, so rebooting is safe. See
Keeping the silence for the automation that
repairs it, and
docs/static-analysis.mdfor the evidence — including the four blocking tricks that were tested on a device and rejected, and why the daemon refuses to record while any of them holds. - The periodic "ding ding ding" reminder during a long recording is
recurringDisclosureTwinkle.cafinsideTelephonyUtilities.frameworkon the sealed system volume. This exploit cannot replace a sealed-volume file, so that one stays. Only the start/stop announcements are silenceable from the app. - A reboot does not undo it. Verified by byte-comparing both files before and after a restart.
- The announcement plays to both parties on the call. Removing it may be unlawful where recording requires all-party consent (Germany, several US states). Get consent before recording anyone — the law applies to you whatever your phone does or does not announce.
The app calls itself AirliftSilence everywhere it is visible: the home screen, the navigation
title, the pairing host advertised to Settings, and the pairing file it leaves in Documents
(airlift_pairing.plist). Nothing user-facing carries the upstream AirCard name.
The icon is drawn from scratch by tools/make-icon.py (Pillow) rather than shipped as a binary
blob, so the repository contains no borrowed artwork and the design is editable as numbers:
python3 tools/make-icon.pyIt renders a muted speaker — a "sound off" mark, which is what the app does — over a navy-to-blue gradient, and writes all five sizes the asset catalog expects.
- airlift — Johnny Franks: the original PoC and the ATAirlock analysis
- AirCard-iOS — Mak5er: the loopback tunnel, the Rust FFI core, the pairing host and the Grappa tokens this is built on
- idevice — the RSD/AFC/streaming_zip_conduit plumbing
MIT. See LICENSE.