[object Object]

← back to Opened Tagger

opened-tagger: blue Finder tag for everything opened/launched, with launchd agent

a25520e1ea4851d2dda4c98f642a8f11675fcf73 · 2026-06-24 13:48:46 -0700 · Steve

Files touched

Diff

commit a25520e1ea4851d2dda4c98f642a8f11675fcf73
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Jun 24 13:48:46 2026 -0700

    opened-tagger: blue Finder tag for everything opened/launched, with launchd agent
---
 .gitignore    |   2 +
 README.md     |  51 ++++++++++++++++++++++
 tag-opened.py | 135 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 188 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..05de3c6
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+tagger.log
+.DS_Store
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..8749f40
--- /dev/null
+++ b/README.md
@@ -0,0 +1,51 @@
+# opened-tagger
+
+Marks **everything you've ever opened or launched** on this Mac with the blue
+Finder tag. In any Finder window, a **blue dot = you've opened this before**;
+**no dot = never touched**. Your "what haven't I read yet?" system.
+
+## How it works
+
+macOS Spotlight records `kMDItemUseCount` (times opened) on every indexed file
+and app. `tag-opened.py` asks Spotlight for items with use-count > 0 and adds the
+system **Blue** tag to each, preserving any tags already present. Pure built-ins
+(`mdfind`, `xattr`, Python stdlib) — nothing to install.
+
+## Usage
+
+```sh
+python3 tag-opened.py            # tag opened items in the default surfaces
+python3 tag-opened.py --dry-run  # preview only, change nothing
+python3 tag-opened.py --untag    # remove the Blue tag (full undo)
+python3 tag-opened.py --dirs ~/Desktop ~/Projects   # custom surfaces
+```
+
+Default surfaces: `~/Desktop`, `~/Documents`, `~/Downloads`, `~/Projects`
+(skipping `node_modules`, `.git`, caches, Trash) + all launched apps Mac-wide.
+
+## Make it automatic (re-tag newly-opened items every 30 min)
+
+The LaunchAgent is at `~/Library/LaunchAgents/com.steve.opened-tagger.plist`.
+Enable it yourself (installing a background agent is intentionally gated):
+
+```sh
+launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.steve.opened-tagger.plist
+launchctl list | grep opened-tagger    # confirm a PID appears
+```
+
+Disable later:
+
+```sh
+launchctl bootout gui/$(id -u)/com.steve.opened-tagger
+```
+
+## Known limitations (honest)
+
+- **Blue dot, not blue text.** Finder renders tags as a colored dot/pill next to
+  the filename, never as recolored text — macOS has no setting for text color.
+  For literal light-blue text you'd need a custom viewer window (not real Finder).
+- **SIP-protected system apps can't be tagged.** Safari, Mail, and other Apple
+  apps in `/System/Applications` block attribute writes; the tag silently no-ops.
+  User-installed apps (in `~/Applications`, `/Applications`) tag fine.
+- **"Opened" = opened by an app Spotlight noticed.** Quick Look previews and some
+  background reads may not bump the use-count.
diff --git a/tag-opened.py b/tag-opened.py
new file mode 100644
index 0000000..ecc1091
--- /dev/null
+++ b/tag-opened.py
@@ -0,0 +1,135 @@
+#!/usr/bin/env python3
+"""
+tag-opened.py — Mark everything you've ever opened/launched with the blue Finder tag.
+
+How it works
+------------
+macOS Spotlight records `kMDItemUseCount` (times opened) on every indexed file and
+app bundle. We ask Spotlight (via `mdfind`) for items whose use-count > 0, then add
+the system "Blue" tag to each. Untouched items stay untagged, so in any Finder
+window the blue dot = "I've opened this before", no dot = "never touched".
+
+No third-party installs: uses the built-in `mdfind` and `xattr` commands plus the
+Python stdlib `plistlib`. Tags are written into com.apple.metadata:_kMDItemUserTags,
+exactly where Finder stores them.
+
+Existing tags are PRESERVED — Blue is appended only if not already present.
+
+Usage
+-----
+  python3 tag-opened.py            # tag opened items in the default surfaces
+  python3 tag-opened.py --dry-run  # show what WOULD be tagged, change nothing
+  python3 tag-opened.py --untag    # remove the Blue tag everywhere we'd add it
+  python3 tag-opened.py --dirs ~/Desktop ~/Projects   # custom surfaces
+"""
+
+import argparse
+import os
+import plistlib
+import subprocess
+import sys
+
+TAG = "Blue\n4"          # Finder tag name + color index (4 == blue)
+TAG_NAME = "Blue"
+XATTR = "com.apple.metadata:_kMDItemUserTags"
+
+# Default surfaces. Apps are handled separately (whole-Mac query) so launched
+# apps in /Applications, /System/Applications, etc. all get caught.
+DEFAULT_DIRS = [
+    os.path.expanduser("~/Desktop"),
+    os.path.expanduser("~/Documents"),
+    os.path.expanduser("~/Downloads"),
+    os.path.expanduser("~/Projects"),
+]
+
+# Folders we never want to tag inside — pure noise, never "things you read".
+EXCLUDE_SUBSTR = ("/node_modules/", "/.git/", "/Library/Caches/", "/.Trash/")
+
+
+def mdfind(query, only_in=None):
+    cmd = ["mdfind"]
+    if only_in:
+        cmd += ["-onlyin", only_in]
+    cmd.append(query)
+    try:
+        out = subprocess.run(cmd, capture_output=True, text=True, timeout=120).stdout
+    except subprocess.TimeoutExpired:
+        return []
+    return [l for l in out.splitlines() if l]
+
+
+def read_tags(path):
+    """Return the current list of Finder tags on a file (empty if none)."""
+    r = subprocess.run(["xattr", "-px", XATTR, path], capture_output=True, text=True)
+    if r.returncode != 0 or not r.stdout.strip():
+        return []
+    try:
+        raw = bytes.fromhex(r.stdout.replace("\n", "").replace(" ", ""))
+        val = plistlib.loads(raw)
+        return val if isinstance(val, list) else []
+    except Exception:
+        return []
+
+
+def write_tags(path, tags):
+    blob = plistlib.dumps(tags, fmt=plistlib.FMT_BINARY).hex()
+    subprocess.run(["xattr", "-wx", XATTR, blob, path], capture_output=True)
+
+
+def has_blue(tags):
+    # A tag may be stored as "Blue\n4" or bare "Blue"; match either.
+    return any(t.split("\n")[0] == TAG_NAME for t in tags)
+
+
+def gather_targets(dirs):
+    seen = set()
+    # Files/folders opened at least once in each surface.
+    for d in dirs:
+        if not os.path.isdir(d):
+            continue
+        for p in mdfind("kMDItemUseCount > 0", only_in=d):
+            if any(s in p for s in EXCLUDE_SUBSTR):
+                continue
+            seen.add(p)
+    # Launched apps, anywhere on the Mac.
+    for p in mdfind('kMDItemContentType == "com.apple.application-bundle" '
+                    '&& kMDItemUseCount > 0'):
+        seen.add(p)
+    return sorted(seen)
+
+
+def main():
+    ap = argparse.ArgumentParser(description="Blue-tag everything you've opened.")
+    ap.add_argument("--dry-run", action="store_true",
+                    help="list targets, change nothing")
+    ap.add_argument("--untag", action="store_true",
+                    help="remove the Blue tag from the same targets")
+    ap.add_argument("--dirs", nargs="*", default=None,
+                    help="override the default surfaces")
+    args = ap.parse_args()
+
+    dirs = [os.path.expanduser(d) for d in args.dirs] if args.dirs else DEFAULT_DIRS
+    targets = gather_targets(dirs)
+
+    changed = 0
+    for p in targets:
+        tags = read_tags(p)
+        if args.untag:
+            if has_blue(tags):
+                if not args.dry_run:
+                    write_tags(p, [t for t in tags if t.split("\n")[0] != TAG_NAME])
+                changed += 1
+                print(("WOULD UNTAG " if args.dry_run else "untagged ") + p)
+        else:
+            if not has_blue(tags):
+                if not args.dry_run:
+                    write_tags(p, tags + [TAG])
+                changed += 1
+                print(("WOULD TAG " if args.dry_run else "tagged ") + p)
+
+    verb = "would change" if args.dry_run else ("untagged" if args.untag else "tagged")
+    print(f"\n{len(targets)} opened items found · {changed} {verb}.", file=sys.stderr)
+
+
+if __name__ == "__main__":
+    main()

(oldest)  ·  back to Opened Tagger  ·  add light-blue-text web viewer (opened=blue, unopened=bold w bc8dd09 →