agentsclimarketplace

Notes janitor

Skill olevasyliev/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.

Install
npx -y skills add olevasyliev/notes-janitor

Assembled 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

  1. Confirm the host is macOS (uname = Darwin). This skill only works on macOS with Notes.app; there is no equivalent on other platforms.
  2. 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), osascript cannot send Apple Events at all and the call will hang or fail with no useful error. Any command in this skill that shells out to osascript - export_notes.js directly, or resort.py's folders/execute/verify/rollback actions, which call osascript internally - needs the sandbox disabled for that call (the no-sandbox / "dangerously skip sandbox" execution mode). resort.py plan and all the Python-only steps (normalize.py, make_batches.py, merge_results.py, mask_titles.py) do not touch Notes and never need this.
  3. 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/. Export WORKDIR to that path (all scripts also accept --workdir explicitly). 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.js walks 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.py strips each note's HTML body to plain text and writes one JSON object per line to notes.jsonl, assigning each note a stable short id (n0001, n0002, ...) used everywhere downstream.
  • make_batches.py packs notes.jsonl into batches/batch_NNN.json files (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.py validates coverage (every note has exactly one result), prints category/flag counts, and writes digest_input.md (stats + per-category useful listings + a revive shortlist, with sensitive notes excluded from all listings there) and a raw sensitive_notes.md inventory.
  • sensitive_notes.md is raw and unmasked - it still has real note titles, and titles can themselves be secrets. Never show, quote, or persist sensitive_notes.md anywhere. Always run mask_titles.py next; 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 - skim sensitive_notes_masked.md yourself before showing it to the user, and if anything still looks like a raw secret, redact it by hand before sharing.
  • From digest_input.md and sensitive_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 rollback exists 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 note throws error -1728. Querying a note's current folder via its own container property 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), and resort.py verifies 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 whose clauses. whose-filtered AppleScript queries against notes are 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.js fetches id, name, body, creationDate, modificationDate as 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 as note id "<coredata-id>", which is reliable across a session; that id is exactly what normalize.py carries 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.py has a separate verify action instead of assuming execute succeeding once is the end of the story - re-run verify after 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 body can contain inline base64-encoded image data. Raw per-folder JSON in raw/ being many megabytes for a modest note count is normal, not a bug - normalize.py strips it down to plain text on the way into notes.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.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.