Back up every Apple Voice Memo with one command
Resources
Voice Memos can export a recording to Finder, but Apple does not document a command for backing up the complete library with its titles and dates.
Dragging a recording is enough when there are only a few. With dozens or hundreds of memos, a reproducible export becomes more useful.
Why copying the folder is not enough
On macOS 26, Voice Memos data is currently stored in this directory:
~/Library/Group Containers/group.com.apple.VoiceMemos.shared/Recordings/
Audio files generally use an identifier rather than the title displayed by the application. The useful metadata is stored in the CloudRecordings.db SQLite database.
An attempt to access that directory from a terminal may return Operation not permitted or authorization denied. This is not fixed by sudo. macOS protects other applications’ data with TCC, and the terminal needs Full Disk Access.
Temporarily allow the terminal
- Open System Settings, then Privacy & Security.
- Open Full Disk Access.
- Add your terminal, such as Terminal, iTerm, or Ghostty.
- Quit the terminal completely with
Cmd+Q, then reopen it.
This permission grants broad access. You can remove it after the export.
The script
The following script copies recordings without deleting or modifying the originals. It restores titles when the local schema allows it, preserves dates, and keeps files missing from the database under their technical name.
Save it as export-voice-memos.sh, then run chmod +x export-voice-memos.sh.
#!/usr/bin/env bash
set -uo pipefail
SRC="$HOME/Library/Group Containers/group.com.apple.VoiceMemos.shared/Recordings"
DEST="$HOME/Downloads/VoiceMemos"
TO_MP3=0
say() { printf '%s\n' "$*"; }
die() { printf 'Error: %s\n' "$*" >&2; exit 1; }
while [ $# -gt 0 ]; do
case "$1" in
-o|--output)
[ $# -ge 2 ] || die "missing directory after $1"
DEST="$2"
shift 2
;;
--mp3)
TO_MP3=1
shift
;;
-h|--help)
printf 'Usage: %s [-o directory] [--mp3]\n' "$0"
exit 0
;;
*) die "unknown option: $1" ;;
esac
done
[ -d "$SRC" ] || die "Voice Memos directory not found"
command -v sqlite3 >/dev/null 2>&1 || die "sqlite3 is required"
if ! find "$SRC" -maxdepth 1 -type f -print -quit >/dev/null 2>&1; then
die "macOS denied access. Grant Full Disk Access to the terminal, then restart it"
fi
mkdir -p "$DEST"
work_dir=$(mktemp -d "${TMPDIR:-/tmp}/voice-memos-export.XXXXXX") || die "cannot create temporary directory"
manifest="$work_dir/exported-source-files"
: > "$manifest"
cleanup() {
rm -f "$manifest"
rmdir "$work_dir" 2>/dev/null || true
}
trap cleanup EXIT
slug() {
printf '%s' "$1" \
| iconv -f UTF-8 -t ASCII//TRANSLIT 2>/dev/null \
| sed 's#[/\\:]#-#g' \
| tr '\n\t' '--' \
| sed -E 's/[^A-Za-z0-9 ._-]//g; s/^ +| +$//g; s/ /-/g'
}
copy_named() {
source_file="$1"
target_base="$2"
target="$DEST/${target_base}.m4a"
suffix=2
while [ -e "$target" ]; do
if cmp -s "$source_file" "$target"; then
return 0
fi
target="$DEST/${target_base}_${suffix}.m4a"
suffix=$((suffix + 1))
done
cp "$source_file" "$target" || return 1
[ -n "${stamp:-}" ] && touch -t "$stamp" "$target" 2>/dev/null
say "Copied: $(basename "$target")"
}
DB="$SRC/CloudRecordings.db"
title_col=""
if [ -f "$DB" ]; then
for column in ZCUSTOMLABEL ZENCRYPTEDTITLE ZTITLE ZNAME; do
if sqlite3 "$DB" "SELECT $column FROM ZCLOUDRECORDING LIMIT 1;" >/dev/null 2>&1; then
title_col="$column"
break
fi
done
fi
exported=0
fallback=0
if [ -n "$title_col" ]; then
while IFS=$'\t' read -r label path apple_date; do
[ -n "$path" ] || continue
source_name=$(basename "$path")
source_file="$SRC/$source_name"
[ -f "$source_file" ] || continue
day=""
stamp=""
case "$apple_date" in
''|*[!0-9.]*) ;;
*)
unix_date=$(awk "BEGIN { printf \"%d\", $apple_date + 978307200 }")
day=$(date -r "$unix_date" +%Y-%m-%d 2>/dev/null || true)
stamp=$(date -r "$unix_date" +%Y%m%d%H%M.%S 2>/dev/null || true)
;;
esac
name=$(slug "${label:-memo}")
[ -n "$name" ] || name="memo"
[ -n "$day" ] && name="${day}_${name}"
if copy_named "$source_file" "$name"; then
printf '%s\n' "$source_name" >> "$manifest"
exported=$((exported + 1))
fi
done < <(
sqlite3 -separator $'\t' "$DB" \
"SELECT COALESCE($title_col,''), ZPATH, ZDATE
FROM ZCLOUDRECORDING
WHERE ZPATH IS NOT NULL;" 2>/dev/null
)
fi
while IFS= read -r -d '' source_file; do
source_name=$(basename "$source_file")
if ! grep -Fqx "$source_name" "$manifest"; then
cp -np "$source_file" "$DEST/$source_name" && fallback=$((fallback + 1))
fi
done < <(
find "$SRC" -maxdepth 1 -type f \
\( -iname '*.m4a' -o -iname '*.caf' -o -iname '*.wav' \) -print0
)
if [ "$TO_MP3" -eq 1 ]; then
command -v ffmpeg >/dev/null 2>&1 || die "ffmpeg is required for --mp3"
for source_file in "$DEST"/*.m4a; do
[ -e "$source_file" ] || continue
ffmpeg -nostdin -loglevel error -y -i "$source_file" \
-codec:a libmp3lame -q:a 2 "${source_file%.m4a}.mp3"
done
fi
say "Done: $exported named file(s), $fallback fallback file(s)"
say "Directory: $DEST"
Run the export
./export-voice-memos.sh
./export-voice-memos.sh -o "$HOME/Documents/VoiceMemos"
./export-voice-memos.sh --mp3
The resulting names follow the pattern 2026-08-31_Memo-title.m4a. If two recordings produce the same name but have different contents, a numeric suffix prevents overwriting.
The MP3 option requires ffmpeg, which can be installed with brew install ffmpeg. The original M4A files also remain in the export directory.
Limitations
The path and SQLite schema are internal macOS implementation details. Apple may change them in an update. The script looks for several known title columns and keeps every unmatched audio file as a fallback, but you should inspect the result before treating it as a backup.
Some recordings synchronised through iCloud may not be downloaded locally yet. Open Voice Memos and allow synchronisation to finish before exporting.
Finally, a copy on the same Mac does not protect against device failure or theft. After checking the result, copy the directory to another disk or your regular backup system.