Hazel Downloads Inbox Automation System
Hazel Downloads Inbox Automation System
Overview
This lab documents a local automation system built using Hazel to treat the Downloads folder as an inbox.
The system enforces deterministic file routing, time-based lifecycle handling, metadata-driven classification, and duplicate cleanup. The goal is to turn Downloads from a junk drawer into a controlled intake point for school, church, certificates, and finance files.
Environment
| Component | Detail |
|---|---|
| OS | macOS |
| Automation | Hazel |
| Metadata layer | Finder tags |
| Root target | ~/Documents |
| Inbox | ~/Downloads |
Folder Targets
Run once to provision the full directory tree:
mkdir -p ~/Documents/{Certificates,Church-Program,Career,Finance/{Receipts,Invoices},School/{Assignments,IT/{BAP115,IT125\ Intro\ to\ information\ Tech,IT255\ Network\ Fundamentals},LS303,Religion/REL275B}}
Verify the tree after provisioning:
find ~/Documents -type d | sort
Tagging Strategy
Finder tags serve as the metadata layer between Hazel rules. Tags allow rules to pass state to downstream rules without moving or renaming files prematurely.
| Tag | Applied By | Purpose | Consumed By |
|---|---|---|---|
excelclass | Manual (user) | Marks BAP115 Excel coursework | Rule 6 — BAP115 router |
company | Rule 5 | Marks vendor/company-origin files | Manual review or future rule |
processed | Any terminal rule | Marks file as routed — prevents re-evaluation | Rule 14 — duplicate catch-all guard |
Tagging Rules
- Tags are additive only — no rule removes a tag
- The
processedtag is applied at the end of any rule that moves a file - Manual tags (
excelclass) must be applied before the 7-day window closes - Tag names are lowercase, no spaces, no special characters
Applying Tags Manually in Finder
- Select file in Finder
- Right-click → Tags
- Type tag name and press Return
Or via terminal using the tag CLI:
## Install via Homebrew
brew install tag
## Apply a tag
tag --add excelclass ~/Downloads/excel-homework-week3.xlsx
## Verify tags on a file
tag --list ~/Downloads/excel-homework-week3.xlsx
Rule Order for Downloads
Rules execute top-to-bottom. Hazel stops at the first match. Order is the single most critical configuration decision.
| Priority | Rule | Condition | Action |
|---|---|---|---|
| 1 | Pictures | Existing rule | Keep — do not modify |
| 2 | Resume | Existing rule | Keep — do not modify |
| 3 | 3D Printing | Existing rule | Keep — do not modify |
| 4 | Certificate or CPE | Name or kind matches certificate/CPE pattern | Move → Certificates |
| 5 | Company tag | Name contains company identifiers | Add tag company |
| 6 | BAP115 | Has tag excelclass, last opened > 7 days ago | Move → School/IT/BAP115 |
| 7 | IT255 Packet Tracer | Name matches Packet Tracer pattern, last opened > 7 days ago | Move → School/IT/IT255 Network Fundamentals |
| 8 | IT125 | Name matches IT125 pattern, last opened > 7 days ago | Move → School/IT/IT125 Intro to information Tech |
| 9 | LS303 | Name matches LS303 pattern, last opened > 7 days ago | Move → School/LS303 |
| 10 | REL275B | Name matches REL275B pattern, last opened > 7 days ago | Move → School/Religion/REL275B |
| 11 | Generic school projects | Name matches school pattern, last opened > 7 days ago | Move → School/Assignments |
| 12 | Church programs | Name matches church pattern, last opened > 7 days ago | Rename + Move → Church-Program |
| 13 | Receipts | Name or kind matches receipt pattern | Rename + Move → Finance/Receipts |
| 14 | Duplicate cleanup | Hazel duplicate detection + has tag processed | Trash |
Steps
Before / After Workflow
Before — Unmanaged Downloads
~/Downloads/
├── excel-homework-week3.xlsx ← sitting for 3 weeks, never moved
├── IT255_Lab4_PacketTracer.pkt ← buried, never routed
├── Church-Bulletin-2026-04-13.pdf ← mixed with everything else
├── Amazon_Receipt_March.pdf ← lost in the noise
├── Resume_ChrisNewcomb_2026.pdf ← duplicated 3 times
├── Resume_ChrisNewcomb_2026 (1).pdf
├── Resume_ChrisNewcomb_2026 (2).pdf
└── random_download.zip ← unknown, untouched
After — Inbox Model Running
~/Downloads/
└── random_download.zip ← unmatched, stays for manual triage
~/Documents/
├── Certificates/
├── Church-Program/
│ └── 2026-04-13_Church-Bulletin.pdf
├── Finance/
│ └── Receipts/
│ └── 2026-03_Amazon-Receipt.pdf
└── School/
└── IT/
├── BAP115/
│ └── excel-homework-week3.xlsx
└── IT255 Network Fundamentals/
└── IT255_Lab4_PacketTracer.pkt
Files that match no rule remain in Downloads for manual triage. This is intentional — the system does not move unknown files.
Shell Script — Sunday Validation
Run weekly to surface files that should have been routed but were not. This is an observability tool, not a replacement for Hazel rules.
#!/usr/bin/env bash
## validate-downloads.sh
## Surfaces Downloads files older than 7 days that were not routed by Hazel.
## Usage: bash validate-downloads.sh
## Schedule: Weekly, Sunday 9:00 AM via launchd
set -euo pipefail
DOWNLOADS="$HOME/Downloads"
THRESHOLD_DAYS=7
REPORT_FILE="$HOME/Documents/hazel-validation-$(date +%Y-%m-%d).txt"
echo "=== Hazel Downloads Validation ===" | tee "$REPORT_FILE"
echo "Run date: $(date)" | tee -a "$REPORT_FILE"
echo "Threshold: files not opened in ${THRESHOLD_DAYS}+ days" | tee -a "$REPORT_FILE"
echo "" | tee -a "$REPORT_FILE"
UNROUTED=$(find "$DOWNLOADS" -maxdepth 1 -type f -atime +"$THRESHOLD_DAYS" 2>/dev/null)
if [[ -z "$UNROUTED" ]]; then
echo "✓ No unrouted files found. Downloads inbox is clean." | tee -a "$REPORT_FILE"
else
echo "⚠ Unrouted files older than ${THRESHOLD_DAYS} days:" | tee -a "$REPORT_FILE"
echo "" | tee -a "$REPORT_FILE"
while IFS= read -r file; do
LAST_ACCESS=$(stat -f "%Sa" -t "%Y-%m-%d" "$file")
echo " [$LAST_ACCESS] $(basename "$file")" | tee -a "$REPORT_FILE"
done <<< "$UNROUTED"
fi
echo "" | tee -a "$REPORT_FILE"
echo "Report saved to: $REPORT_FILE"
Scheduling with launchd
Save as ~/Library/LaunchAgents/com.newcomb-labs.validate-downloads.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.newcomb-labs.validate-downloads</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>/usr/local/bin/validate-downloads.sh</string>
</array>
<key>StartCalendarInterval</key>
<dict>
<key>Weekday</key>
<integer>0</integer>
<key>Hour</key>
<integer>9</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
<key>StandardOutPath</key>
<string>/tmp/validate-downloads.log</string>
<key>StandardErrorPath</key>
<string>/tmp/validate-downloads.err</string>
</dict>
</plist>
Load the agent:
launchctl load ~/Library/LaunchAgents/com.newcomb-labs.validate-downloads.plist
Verify it loaded:
launchctl list | grep newcomb-labs
Duplicate Handling Strategy
Rule 14 is a catch-all that runs last. It only fires on files that satisfy two conditions simultaneously: Hazel's built-in duplicate detection AND the presence of the processed tag.
Decision Logic
File in Downloads
│
▼
Is it a duplicate? (Hazel detection)
│
Yes │ No
│ └──→ Skip — not Rule 14's concern
▼
Does it have tag `processed`?
│
Yes │ No
│ └──→ Skip — upstream rules have not fired yet; do not trash
▼
Move to Trash
Why the Two-Condition Guard Matters
Without the processed guard, Rule 14 could trash a file that simply hasn't been opened yet and happens to share a name with another file. The two-condition check ensures only confirmed-routed duplicates are cleaned up. A file can be a duplicate and still be the version you want to keep.
Failure Modes and Edge Cases
| Scenario | Behavior | Resolution |
|---|---|---|
| File opened within 7-day window | Time-based rule does not fire | Expected — file stays in Downloads until threshold passes |
excelclass tag not applied before 7-day window | BAP115 rule never fires | Apply tag manually; rule fires on next Hazel evaluation cycle |
| File matches multiple rules | First matching rule wins | Review rule order; more specific rules must sit above broader ones |
Duplicate detected but not tagged processed | Rule 14 skips it | Correct behavior — upstream rules have not fired yet |
| Folder target does not exist | Hazel move fails silently | Re-run mkdir -p provisioning script; Hazel retries on next cycle |
| File renamed after landing in Downloads | Name-based rules may not match | Tag-based rules are more resilient; prefer tags for ambiguous filenames |
| Hazel not running (app quit) | No rules execute | Add Hazel to Login Items: System Settings → General → Login Items |
Large .pkt file slow to open | Last-opened timestamp may not update | Open file manually once to reset timestamp before 7-day window |
| Church bulletin named inconsistently | Church rule may not match | Standardize naming or broaden Hazel name condition to partial match |
Implementation Artifacts
Hazel does not export rules natively. Screenshots of each configured rule are the primary artifact format.
Store at:
website/static/img/labs/hazel-downloads/Naming convention:
rule-{priority:02d}-{slug}.png
website/static/img/labs/hazel-downloads/
├── rule-04-certificate.png
├── rule-05-company-tag.png
├── rule-06-bap115.png
├── rule-07-it255.png
├── rule-08-it125.png
├── rule-09-ls303.png
├── rule-10-rel275b.png
├── rule-11-school-generic.png
├── rule-12-church.png
├── rule-13-receipts.png
└── rule-14-duplicate-cleanup.png
Validation
-
mkdir -pprovisioning script runs without errors -
find ~/Documents -type dshows expected full folder tree - Each rule fires correctly on a test file matching its condition
- Rule 14 does not trash files missing the
processedtag -
validate-downloads.shruns and produces a dated report file - launchd agent loads without error (
launchctl list | grep newcomb-labs) - No files older than 7 days remain unrouted after a full Hazel evaluation cycle
Lessons Learned
- The inbox model forces intentional organization over passive accumulation
- Time-based automation prevents premature moves on files still in active use
- Rule order is the single most critical configuration decision in Hazel
- Finder tags enable cross-rule metadata passing without filesystem side effects
- Name-based matching is fragile — tags are more resilient for ambiguous filenames
- The validation script surfaces Hazel gaps that rules alone cannot catch
Future Improvements
- Add a Finance/Invoices rule to separate invoices from receipts
- Evaluate
tagCLI integration directly in Hazel shell action scripts - Export Hazel rules via AppleScript for version-controlled backup
- Add a Career rule to route resume variants and job application documents
- Integrate validation report with a macOS notification via
osascript - Consider a
quarantinetag for files that need manual review before routing - Explore Hazel's folder action chaining for multi-stage processing pipelines