Skip to content

Instantly share code, notes, and snippets.

@kasrak
Created April 10, 2026 16:04
Show Gist options
  • Select an option

  • Save kasrak/986e9568417d5e77f0d44ca7f0561afd to your computer and use it in GitHub Desktop.

Select an option

Save kasrak/986e9568417d5e77f0d44ca7f0561afd to your computer and use it in GitHub Desktop.
Script to export Apples notes to plain text
#!/bin/bash
# Export Apple Notes to Markdown files with YAML frontmatter
# Uses JXA (JavaScript for Automation) to access Notes.app
OUTPUT_DIR="${1:-$(pwd)/apple-notes-export}"
mkdir -p "$OUTPUT_DIR"
echo "Exporting Apple Notes..."
osascript -l JavaScript <<'JSEOF' | OUTPUT_DIR="$OUTPUT_DIR" python3 -c "
import json, sys, os, re
output_dir = os.environ['OUTPUT_DIR']
def sanitize_filename(name):
# Remove or replace characters invalid in filenames
name = re.sub(r'[/\\\\:*?\"<>|]', '-', name)
name = re.sub(r'\\s+', ' ', name).strip()
if not name:
name = 'Untitled'
# Truncate to reasonable length
if len(name) > 200:
name = name[:200]
return name
data = json.load(sys.stdin)
print(f'Found {len(data)} notes')
# Track duplicate filenames
name_counts = {}
for note in data:
title = note['name'] or 'Untitled'
folder = note['folder'] or 'Unfiled'
created = note['created']
modified = note['modified']
body = note['body']
# Create folder subdirectory
folder_dir = os.path.join(output_dir, sanitize_filename(folder))
os.makedirs(folder_dir, exist_ok=True)
# Handle duplicate filenames
base_name = sanitize_filename(title)
key = os.path.join(folder_dir, base_name)
if key in name_counts:
name_counts[key] += 1
filename = f'{base_name} ({name_counts[key]}).md'
else:
name_counts[key] = 0
filename = f'{base_name}.md'
filepath = os.path.join(folder_dir, filename)
# Build frontmatter
frontmatter = '---\n'
frontmatter += f'title: \"{title.replace(chr(34), chr(92)+chr(34))}\"\n'
frontmatter += f'folder: \"{folder}\"\n'
frontmatter += f'created: \"{created}\"\n'
frontmatter += f'updated: \"{modified}\"\n'
frontmatter += '---\n\n'
content = frontmatter + (body or '')
with open(filepath, 'w', encoding='utf-8') as f:
f.write(content)
print(f'Exported to {output_dir}/')
for folder in sorted(os.listdir(output_dir)):
folder_path = os.path.join(output_dir, folder)
if os.path.isdir(folder_path):
count = len([f for f in os.listdir(folder_path) if f.endswith('.md')])
print(f' {folder}: {count} notes')
"
// JXA script to extract all notes as JSON
var Notes = Application('Notes');
Notes.includeStandardAdditions = true;
var results = [];
var accounts = Notes.accounts();
for (var i = 0; i < accounts.length; i++) {
var folders = accounts[i].folders();
for (var j = 0; j < folders.length; j++) {
var folderName = folders[j].name();
var notes = folders[j].notes();
for (var k = 0; k < notes.length; k++) {
try {
var note = notes[k];
var created = note.creationDate().toISOString();
var modified = note.modificationDate().toISOString();
results.push({
name: note.name(),
folder: folderName,
created: created,
modified: modified,
body: note.plaintext()
});
} catch(e) {
// Skip notes that can't be read (e.g. password protected)
results.push({
name: "Error reading note",
folder: folderName,
created: "",
modified: "",
body: "Could not export this note: " + e.message
});
}
}
}
}
JSON.stringify(results);
JSEOF
echo "Done!"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment