Notes janitor
Claude Code skill: audit, digest, and reorganize your Apple Notes archive locally on macOS. Finds forgotten ideas and notes with leaked secrets, re-sorts 10 years of notes with a rollback log.
npx -y skills add olevasyliev/notes-janitorAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- 21 days oldThe repository was created 21 days ago. New is not bad, but a brand new repository carrying a familiar-sounding name is the shape a typosquat arrives in, and there has been no time for anyone else to find a problem with it.
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Audits and reorganizes a user's Apple Notes archive on macOS - exports every note via Notes automation, classifies each one with a cheap model, produces a digest and a secrets inventory, and optionally re-sorts notes into folders. Use when the user asks to clean up, organize, audit, declutter, or "do something with" their Apple Notes / Notes.app archive.
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
15.0 KB, ~3.5k tokens by cl100k_base, as published. Nobody here has run it
Notes Janitor
Audits a macOS Notes archive end to end: export every note, classify each one cheaply, hand back a human-readable digest plus a secrets inventory, and - only if the user asks for it - re-file notes into category folders. Everything runs locally except the classification calls themselves (those go to whatever model provider is configured; note text leaves the machine for that step only).
This skill has five phases. Phases 1-3 (export, classify, report) are the core of "clean up my notes" and should run without extra confirmation once the user has approved the plan. Phases 4 and 5 (re-sort, personal-context extraction) are optional and destructive-adjacent (they move notes / write to another file) - always ask the user explicitly before running either, even if they asked for a general cleanup.
Preflight
- Confirm the host is macOS (
uname=Darwin). This skill only works on macOS with Notes.app; there is no equivalent on other platforms. - Sanity-check Notes automation before doing anything else:
osascript -e 'tell application "Notes" to count notes'- If this is the first time anything has automated Notes, macOS shows a one-time permission prompt ("X wants access to control Notes"). Tell the user to click Allow, then re-run the command.
- If the calling shell is sandboxed (e.g. Claude Code's Bash tool with
its default sandbox),
osascriptcannot send Apple Events at all and the call will hang or fail with no useful error. Any command in this skill that shells out toosascript-export_notes.jsdirectly, orresort.py'sfolders/execute/verify/rollbackactions, which callosascriptinternally - needs the sandbox disabled for that call (the no-sandbox / "dangerously skip sandbox" execution mode).resort.py planand all the Python-only steps (normalize.py,make_batches.py,merge_results.py,mask_titles.py) do not touch Notes and never need this.
- Pick a local work directory and keep every artifact in it - never
scatter pipeline files across the user's projects. A dedicated
directory outside any git repo is ideal, e.g.
~/notes-janitor-work/. ExportWORKDIRto that path (all scripts also accept--workdirexplicitly). Nothing in this pipeline needs to be committed anywhere.
Phase 1 - Export
mkdir -p "$WORKDIR"
osascript -l JavaScript scripts/export_notes.js "$WORKDIR" # writes $WORKDIR/raw/folder_*.json
python3 scripts/normalize.py --workdir "$WORKDIR" # -> $WORKDIR/notes.jsonl
python3 scripts/make_batches.py --workdir "$WORKDIR" # -> $WORKDIR/batches/batch_*.json
export_notes.jswalks every folder in the default Notes account, skips "Recently Deleted", and bulk-fetches id/title/body/created/modified per folder (see Known gotchas - this is deliberately not a note-by-note loop).normalize.pystrips each note's HTML body to plain text and writes one JSON object per line tonotes.jsonl, assigning each note a stable short id (n0001,n0002, ...) used everywhere downstream.make_batches.pypacksnotes.jsonlintobatches/batch_NNN.jsonfiles (default: up to 40 notes or 70,000 chars per batch, whichever comes first, with each note's text truncated at 5,000 chars) - sized to fit comfortably in a single cheap-model call.- All output stays under
$WORKDIR. Nothing here leaves the machine.
Report the note count and batch count to the user before moving on.
Phase 2 - Classify
For each file in $WORKDIR/batches/, spawn one subagent on the
cheapest/fastest available model (e.g. Haiku) to classify that batch.
This is a bounded, mechanical task - a strong reasoning model is not
needed and would just cost more. Launch multiple batch subagents in
parallel rather than one at a time.
Each subagent must read its batch file and write one JSONL line per note
to $WORKDIR/results/<same-batch-filename-but-.jsonl> (e.g.
batch_007.json -> results/batch_007.jsonl), with exactly these keys:
{"n": "n0001", "cat": "project_idea", "gist": "...", "useful": false, "revive": false, "sensitive": false}
Classification prompt template
You are classifying a batch of Apple Notes for an archive cleanup. You will
receive a JSON array of notes, each: {"n", "folder", "title", "created", "text"}.
For EVERY note in the array, output exactly one JSON object per line (JSONL -
no markdown fences, no commentary, no summary) with these keys:
n - copy the note's "n" id unchanged
cat - exactly one of these 15 categories:
project_idea - an idea/pitch/plan for something not yet built
work - work tasks, meetings, analytics, client/business notes
personal_journal - diary-style reflection, feelings, personal events
reference - factual reference material, how-tos, notes-to-self
book_media - notes on books/movies/shows/podcasts, reading lists, quotes
humor - jokes, funny quotes, memes as text
health_fitness - health, medical, fitness, workout notes
travel - trip planning, itineraries, travel logs
finance - budgets, spending, non-secret financial notes
credentials_sensitive - passwords, API keys, tokens, card/account/ID numbers,
private keys, seed phrases, or other real secrets/PII
list_todo - checklists, to-do lists, shopping lists
learning - study notes, course notes, language learning
junk_obsolete - stale scratch notes, test notes, no-longer-relevant clutter
empty - blank or effectively blank
other - doesn't fit any category above
gist - <=120 chars, English (translate if the note is in another
language), describing what the note is about
useful - true if this is still relevant / worth keeping visible today,
false if it's stale/superseded/no longer needed
revive - true ONLY for a small, selective subset of useful notes that
are worth actively resurfacing (an unfinished idea worth
restarting, an open task, something genuinely worth acting on
soon). This is a shortlist, not a synonym for useful - most
useful notes should NOT be revive=true.
sensitive - true if the note contains real secret/PII material anywhere,
INCLUDING if the note's title itself is the secret (Apple
Notes uses the first line as the title, so titles often leak
keys/passwords/tokens). When in doubt, mark sensitive=true.
CRITICAL RULE for sensitive notes: NEVER copy any part of an actual secret
(key, token, password, card number, seed word, address) into the gist.
Describe it instead - e.g. "Contains a Stripe API key and a client's card
number", never the key or number itself. This rule applies even if the
secret is short or looks harmless.
Batch to classify:
<insert the batch JSON array here>
After all batches finish, verify coverage (every batch produced exactly one result file, one line per input note) before moving to Phase 3.
Phase 3 - Report
python3 scripts/merge_results.py --workdir "$WORKDIR" # -> digest_input.md, sensitive_notes.md (RAW, unmasked)
python3 scripts/mask_titles.py --workdir "$WORKDIR" # -> sensitive_notes_masked.md
merge_results.pyvalidates coverage (every note has exactly one result), prints category/flag counts, and writesdigest_input.md(stats + per-category useful listings + a revive shortlist, with sensitive notes excluded from all listings there) and a rawsensitive_notes.mdinventory.sensitive_notes.mdis raw and unmasked - it still has real note titles, and titles can themselves be secrets. Never show, quote, or persistsensitive_notes.mdanywhere. Always runmask_titles.pynext; it rebuilds the same listing with secret-looking titles replaced by a placeholder (bare emails/phones get partial masking instead). This is a fail-closed heuristic, not a guarantee - skimsensitive_notes_masked.mdyourself before showing it to the user, and if anything still looks like a raw secret, redact it by hand before sharing.- From
digest_input.mdandsensitive_notes_masked.md, synthesize a human digest for the user covering: total notes, category breakdown, useful vs stale split, a curated revive shortlist (don't just dump the whole revive list verbatim - pick out the ones actually worth mentioning), and a security section (count of sensitive notes found, by general type - e.g. "12 notes with API keys, 40 with card/account numbers" - never any actual values). Recommend the user move sensitive notes' contents to a password manager and delete the notes afterward (this skill does not delete anything itself).
This is where phases 1-3 end for a plain "audit my notes" request. Present the digest and stop unless the user asks for re-sorting.
Phase 4 (optional, ask first) - Re-sort
Ask the user explicitly before running this phase, even if they asked for a general cleanup - it moves real notes around, even though it never deletes anything.
python3 scripts/resort.py plan --workdir "$WORKDIR" # move_plan.tsv, no changes to Notes
python3 scripts/resort.py folders --workdir "$WORKDIR" # creates missing target folders
python3 scripts/resort.py execute --workdir "$WORKDIR" # moves notes, writes rollback_<ts>.tsv
python3 scripts/resort.py verify --workdir "$WORKDIR" # run again later (iCloud sync can revert moves)
Show the user the plan output (planned move counts per target folder)
before running execute. resort.py's default category-to-folder mapping
routes junk_obsolete/empty to "🗑 Review & delete" and
credentials_sensitive to "🔐 Secure & delete" for manual review/deletion
by the user - it never deletes a note itself. See the top of
scripts/resort.py for the full mapping and how to customize folder
names.
execute moves in chunks (default 40 notes per AppleScript call), retries
failures once, and appends every successful move to a timestamped rollback
TSV - resort.py rollback --rollback-file <that file> undoes it later if
needed, and resort.py verify re-checks (and quietly re-applies) any
moves iCloud sync reverted after the fact. Run verify again a few
minutes after execute, and once more in a later session, to catch drift.
Phase 5 (optional, ask first) - Personal-context extraction
Ask the user explicitly before running this phase.
Build a subset of notes worth mining for personal context: notes
classified personal_journal, health_fitness, travel, book_media,
humor, or finance, EXCLUDING anything sensitive=true. Batch that
subset the same way as Phase 2 (small JSON arrays, capped size), and spawn
cheap-model subagents to extract discrete facts (values, tastes,
recurring themes, biographical details) from each batch as structured
JSON - not verbatim quotes. Then synthesize the extracted facts into
whatever context file the user wants updated (e.g. a personal-context
document), writing prose, not a raw fact dump. This phase has no
dedicated script in scripts/ - build the batches with the same
truncation/size discipline as make_batches.py and reuse its patterns.
Safety rules
- Everything runs locally except the classification (and, if run, personal-context extraction) API calls - tell the user this up front. Note text for the batches being classified leaves the machine for that call only; nothing else does.
- Never quote, echo, or persist actual secret material anywhere - not in chat, not in any file. Always describe, never quote.
- Never delete or edit a note. This skill only ever reads, classifies, and (in Phase 4, with consent) moves notes between folders.
- Every move Phase 4 makes is logged to a rollback TSV before the user needs it - don't skip writing it "to save time."
- Classification is a cheap model doing bulk categorization - expect
roughly 95% accuracy, not perfection. Tell the user to expect a handful
of misfiled notes and that
resort.py rollbackexists if a batch of moves needs undoing.
Known platform gotchas (macOS Notes automation)
- NoteStore.sqlite is off-limits. Reading Notes' underlying SQLite database directly is blocked by macOS TCC even with Full Disk Access granted to the calling process. Notes.app automation (AppleScript/JXA) is the only reliable way to get note content out - there is no file-level shortcut.
container of notethrows error -1728. Querying a note's current folder via its owncontainerproperty fails unpredictably. Don't rely on it. This pipeline avoids it entirely: the export step records each note's folder once (from the folder loop, not from the note), andresort.pyverifies a move by listing the destination folder's contents afterward (id of every note of folder X) rather than asking the note where it lives.- Avoid
whoseclauses.whose-filtered AppleScript queries againstnotesare slow and flaky at real-archive scale (thousands of notes). Nothing in this pipeline uses one - folders are iterated directly and properties are bulk-fetched. - Bulk-fetch properties per folder, not per note.
export_notes.jsfetchesid,name,body,creationDate,modificationDateas parallel arrays off the folder's notes specifier in one call, falling back to smaller chunks and finally one-note-at-a-time only if the bulk call fails. One-note-at-a-time from the start is dramatically slower against a large archive. - Move/delete by note id works and is stable. All of
resort.py's automation addresses notes asnote id "<coredata-id>", which is reliable across a session; that id is exactly whatnormalize.pycarries through from the export. - iCloud sync can revert a move minutes later. A move can succeed,
verify as successful, and then silently bounce back to its original
folder once iCloud finishes syncing. This is why
resort.pyhas a separateverifyaction instead of assumingexecutesucceeding once is the end of the story - re-runverifyafter some time has passed (and again in a later session) and let it re-apply anything that drifted. - Note bodies embed images as base64. A note's HTML
bodycan contain inline base64-encoded image data. Raw per-folder JSON inraw/being many megabytes for a modest note count is normal, not a bug -normalize.pystrips it down to plain text on the way intonotes.jsonl.
Gives 0 of the 12 instructions most note taking skills give in ~3.5k tokens
Counted across 686 of the 876 authors here whose files we hold, read 2026-08-06
- include a visual element on every slidein 44 of 686, across 13 files
- use wikilinks for internal vault linksin 35 of 686, across 11 files
- commit to a single visual motif across every slidein 34 of 686, across 9 files
- read pptxgenjs guide before creating presentations from scratchin 30 of 686, across 6 files
- keep 0.5 inch minimum marginsin 30 of 686, across 7 files
- use subagents to visually inspect rendered slidesin 30 of 686, across 6 files
- re-verify affected slides after every fixin 27 of 686, across 5 files
- run content QA checks before declaring successin 26 of 686, across 3 files
- Use Markdown links for external URLs onlyin 26 of 686, across 10 files
- pick a bold topic specific color palettein 24 of 686, across 2 files
- read editing guide before editing existing presentationsin 23 of 686, across 1 file
- use one dominant color across all slidesin 23 of 686, across 1 file
Said here and by no other author read
- confirm the host is macOS
- keep all artifacts in one local work directory
- report note and batch counts before classifying
- use the cheapest available model for classification
- launch batch classification subagents in parallel
- write one JSONL line per note
Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once. Length counted with cl100k_base; the agent that loads this file may tokenize it differently.